【问题标题】:Design approach for class objects with many common and some unique methods具有许多常见和一些独特方法的类对象的设计方法
【发布时间】:2016-09-22 14:08:43
【问题描述】:

在下面的示例中,我们有两个Terrier 类的实例,它派生自Dog
一个实例被Terrier 类型的变量引用。
使用此变量,您可以访问 Terrier 类的所有成员。
另一方面,类型为Dog 的变量只能引用Dog 类的成员,即使该引用指向Terrier 的实例。

Terrier bubba = new Terrier("Bubba", 2, "Happy");
bubba.Growl();    // Can call Terrier.Growl

Dog jack = new Terrier("Jack", 17, "Surly");
jack.Growl();     // ERROR: Can't call Growl method

我需要实现一个类MyPets,它有一个List<Pets>,它可以容纳Cat 对象或Dog 对象。
这两个对象都有一些常见的方法,如MakeNoise(),但也有一些独特的方法,它们不在基类中,如 Cat 有方法 ClimbTree()
这个MyPets 类还将有一个方法,它将遍历List<animals> 并调用MakeNoise() 方法和ClimbTree() 方法。

使用抽象基类或其他方法来实现这一目标的最佳方法应该是什么?

【问题讨论】:

  • 我怀疑Petsanimals 是指同一个类?你应该解释MyPets调用MakeNoise()ClimbTree()的语义上下文是什么。如果是,任何Pet 都具有一个关键能力,您可以将其抽象到您的基类中。如果您的宠物能力模型不同,还有其他方法。
  • 使用Pet 的抽象基类(或接口)来实现DogCat 都使用makeNoise() 方法从它继承。然后,在您的迭代过程中,您可以调用makeNoise() 并检查它们是否是Cat 的子类,如果是,则调用方法climbTree()See here for dealing with casting the objectsee here for checking the type

标签: c# class oop


【解决方案1】:

关于我的评论,以下内容应该可以解决您的问题:

public class Visitor
{
    public void doItterate(Cat c)
    {
        Console.WriteLine(c.ToString());
        c.makeNoise();
        c.climbTree();
    }

    public void doItterate(Dog d)
    {
        Console.WriteLine(d.ToString());
        d.makeNoise();
    }
}

public abstract class Pet
{
    public Pet(string name, int age, Mood mood)
    {
        this.MoodOfPet = mood;
        this.Name = name;
        this.Age = age;
    }

    public string Name
    {
        get;
        private set;
    }

    public int Age
    {
        get;
        private set;
    }

    public Mood MoodOfPet
    {
        get;
        private set;
    }

    public abstract void makeNoise();
    public override string ToString()
    {
        return this.Name + " is " + this.Age +
            " years old  and feels " + this.MoodOfPet;
    }

    public abstract void accept(Visitor v);
}

public enum Mood
{
    Surly,
    Happy
}

public abstract class Dog : Pet
{
    public Dog(string name, int age, Mood mood): base (name, age, mood)
    {
    }

    public override void makeNoise()
    {
        Console.WriteLine(this.Name + " is woofing");
    }

    public override void accept(Visitor v)
    {
        v.doItterate(this);
    }
}

public class SheepDog : Dog
{
    public SheepDog(string name, int age, Mood mood): base (name, age, mood)
    {
    }
}

public class Cat : Pet
{
    public Cat(string name, int age, Mood mood): base (name, age, mood)
    {
    }

    public void climbTree()
    {
        Console.WriteLine(this.Name + " is climbing");
    }

    public override void makeNoise()
    {
        Console.WriteLine(this.Name + " is meowing");
    }

    public override void accept(Visitor v)
    {
        v.doItterate(this);
    }
}

public class Terrier : Dog
{
    public Terrier(string name, int age, Mood mood): base (name, age, mood)
    {
    }

    public void growl()
    {
        Console.WriteLine(this.Name + " is growling");
    }

    public override void makeNoise()
    {
        growl();
    }
}

public class MyPets
{
    private Visitor visitor = new Visitor();
    public MyPets()
    {
        Pets = new List<Pet>();
    }

    public List<Pet> Pets
    {
        get;
        private set;
    }

    public void addPet(Pet p)
    {
        Pets.Add(p);
    }

    public void itterate()
    {
        foreach (Pet p in Pets)
        {
            p.accept(visitor);
        }
    }
}

Fiddle

这是使用抽象方法的标准 OOP 设计,稍后会在更具体的类中重载。

编辑现在它使用访问者模式

运行以下代码:

MyPets pets = new MyPets();
pets.addPet(new Cat("Bob", 2, Mood.Surly));
pets.addPet(new Terrier("Jack", 17, Mood.Surly));
pets.addPet(new SheepDog("Bubba", 2, Mood.Happy));
pets.itterate();

产生这些结果:

Bob 2 岁了,感觉很乖

鲍勃在喵喵叫

鲍勃正在攀爬

杰克今年 17 岁,感觉很乖

杰克在咆哮

布巴 2 岁了,感觉很开心

巴巴在发牢骚

【讨论】:

  • 如果更多具有特殊能力的动物加入队伍,itterate 部分会变得非常丑陋。
  • 是的,非常正确。然而,这是 OP 希望它做的事情,如果你觉得你有更好的解决方案,我(我假设 OP)愿意接受建议
  • 在我开始将访问者模式、能力抽象或注释/反射投入池中之前,我将等待 OP 的澄清。您的答案很好,因为 OP 没有说明更具体的要求。
  • 访客模式会非常有用,可惜我从来都不喜欢它,除非我必须使用它
【解决方案2】:

在这个答案中,我提出了一个基于属性的解决方案。在某些方面,它可能比@Draken 提出的解决方案更容易维护,但在其他方面,它的用处不大。

思路:给宠物方法注解一个属性,以便将其标记为一种能力。我将属性保持得很简单,它只是一个没有任何额外元信息的标记。

IteratePetAbilities() 方法是获取宠物能力的关键方法。它使用反射来找到标记有能力属性的方法并调用它们。

// Note: Target method is required to have an empty parameter list
[AttributeUsage(AttributeTargets.Method)]
public sealed class PetAbilityAttribute : Attribute
{
}


public class MyPets
{
    public MyPets()
    {
        Pets = new List<Pet>();
    }

    public ICollection<Pet> Pets { get; set; }

    // Discover PetAbilityAttribute methods on the concrete pet type and invoke them dynamically
    public void IteratePetAbilities()
    {
        foreach (var pet in Pets)
        {
            Console.WriteLine("Pet '" + pet.PetName + "' enters the stage");
            var abilities = pet.GetType().GetMethods().Where(x => x.GetCustomAttributes(typeof(PetAbilityAttribute), true).Any());
            foreach (var abilityMethod in abilities)
            {
                Console.Write("# {0,12}() # ", abilityMethod.Name);
                abilityMethod.Invoke(pet, new object[] { });
            }
            Console.WriteLine();
        }
    }
}


public abstract class Pet
{
    public string PetName { get; set; }

    [PetAbility]
    public abstract void MakeNoise();

    // Note: this is not marked as an ability here
    public abstract void GoSleep();
}


public class Dog : Pet
{
    [PetAbility] // no effect, since base already has this attribute
    public override void MakeNoise()
    {
        Console.WriteLine("Says woof");
    }

    // not marked as an ability
    public override void GoSleep()
    {
        Console.WriteLine("Goes to the dogs house and sleeps");
    }
}


public class Terrier : Dog
{
    [PetAbility]
    public void HuntACat()
    {
        Console.WriteLine("Starts running after a poor little cat");
    }


    [PetAbility] // Unlike a regular dog, the Terrier goes to sleep by ability :)
    public override void GoSleep()
    {
        base.GoSleep();
    }
}


public class Cat : Pet
{
    public override void MakeNoise()
    {
        Console.WriteLine("Says meow");
    }

    [PetAbility]
    public void ClimbTree()
    {
        Console.WriteLine("Climbs a tree and is to scared to return on its own");
    }

    [PetAbility] // makes GoSleep an ability only for cats, even though the method exists in base class
    public override void GoSleep()
    {
        Console.WriteLine("Refuses to sleep and starts sharpening its claws");
    }
}



class Program
{
    static void Main(string[] args)
    {
        var myPets = new MyPets();
        myPets.Pets.Add(new Cat() { PetName = "Super Cat" });
        myPets.Pets.Add(new Dog() { PetName = "Little Dog" });
        myPets.Pets.Add(new Terrier() { PetName = "Hunter" });

        myPets.IteratePetAbilities();
    }

}

输出

Pet 'Super Cat' enters the stage
#    MakeNoise() # Says meow
#    ClimbTree() # Climbs a tree and is to scared to return on its own
#      GoSleep() # Refuses to sleep and starts sharpening its claws

Pet 'Little Dog' enters the stage
#    MakeNoise() # Says woof

Pet 'Hunter' enters the stage
#     HuntACat() # Starts running after a poor little cat
#      GoSleep() # Goes to the dogs house and sleeps
#    MakeNoise() # Says woof

专业版

  • 在任何Pet 子类中添加新功能都很容易,无论它在子类层次结构中嵌套多深(单点代码更改)

骗子

  • 使用反射
    • 当必须强制执行有效性时变得更加复杂(例如,如果将属性应用于具有输入参数的方法怎么办?)。
    • 有人说它比编译时绑定慢(我自己没有测量过,但我倾向于相信他们)
  • 有限控制(例如,如果要按一定顺序输出宠物能力,则需要额外的工作)
    • 这通常适用于多种能力之间的任何相互依赖关系

编辑

旁注:

如果在IteratePetAbilities() 中调用x.GetCustomAttributes(typeof(PetAbilityAttribute), false)(而不是true),则不会发现Cat.MakeNoise,因为覆盖方法没有标记属性。

【讨论】:

    【解决方案3】:

    如果子类的数量相对较少,您可以简单地使用is 和/或as 运算符。

    List<Pet> pets = new List<Pet>
    {
        new Cat("cat1", 1, "Happy"),
        new Cat("cat2", 2, "Happy"),
        new Dog("dog", 1, "Surly"),
        new Terrier("dog", 3, "Happy")
    };
    
    // in your Iterate() method
    foreach (var pet in pets)
    {
        // common methods here
        pet.MakeNoise();
    
        if (pet is Cat)
        {
            var animal = (Cat)pet;
            animal.ClimbTree();
        }
        else if (pet is Terrier)
        {
            var animal = (Terrier)pet;
            animal.Growl();
        }
    }
    

    当数据应该与操作分开时,访问者模式是值得使用的,这不是你的情况。

    由于反射,应避免使用属性解决方案。

    【讨论】:

    • 其实数据和操作是分开的,因为Pet类层次结构包含数据(我也认为能力是数据),访问能力的操作属于MyPets,不是数据层次结构的一部分。但是,我同意一个简单的 if-else 解决方案也属于这里。不幸的是,@Draken 从他的答案中编辑了这个,而不是添加访问者模式。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-23
    • 2011-10-17
    • 1970-01-01
    • 2016-09-02
    • 2015-10-08
    相关资源
    最近更新 更多