如何最大化此接口中的代码重用与继承C#示例

时间:2016-02-12 17:25:48

标签: c# oop inheritance interface

a great video启发,主题为“使用JavaScript示例赞成对象组合”;我想在C#中尝试一下来测试我对这个概念的理解,但它并没有像我希望的那样好。

/// PREMISE
// Animal base class, Animal can eat
public class Animal
{
    public void Eat() { }
}
// Dog inherits from Animal and can eat and bark
public class Dog : Animal
{
    public void Bark() { Console.WriteLine("Bark"); }
}
// Cat inherits from Animal and can eat and meow
public class Cat : Animal
{
    public void Meow() { Console.WriteLine("Meow"); }
}
// Robot base class, Robot can drive
public class Robot
{
    public void Drive() { }
}

问题是我想添加可以Bark和Drive的RobotDog类,但不能吃。

第一个解决方案是创建RobotDog作为Robot的子类,

public class RobotDog : Robot
{
    public void Bark() { Console.WriteLine("Bark"); }
}

但要给它一个Bark函数,我们必须复制并粘贴Dog's Bark函数,所以现在我们有重复的代码。

第二个解决方案是使用Bark方法创建一个公共超类,然后动画和Robot类继承自

public class WorldObject
{
    public void Bark() { Console.WriteLine("Bark"); }
}
public class Animal : WorldObject { ... }
public class Robot : WorldObject { ... }

但是现在每个动物和每个机器人都会有一个Bark方法,大部分都不需要。继续这种模式,子类将充满他们不需要的方法。

第三个解决方案是为可以Bark

的类创建一个IBarkable接口
public interface IBarkable
{
    void Bark();
}

并在Dog和RobotDog类中实现它

public class Dog : Animal, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}
public class RobotDog : Robot, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}

但我们又一次有重复的代码!

第四种方法是再次使用IBarkable接口,但创建一个Bark助手类,然后每个Dog和RobotDog接口实现调用。

这个感觉就像最好的方法(以及视频似乎推荐的内容),但我也可以看到项目中的问题与帮助者混在一起。

第五个建议(hacky?)解决方案是将一个扩展方法挂在空的IBarkable接口上,这样如果你实现了IBarkable,那么就可以Bark

public interface IBarker {    }
public static class ExtensionMethods
{
    public static void Bark(this IBarker barker) { 
        Console.WriteLine("Woof!"); 
    }
}

本网站上有很多类似的回答问题,以及我读过的文章,似乎建议使用abstract类,但是,问题与解决方案2不同吗?

将RobotDog类添加到此示例中的最佳面向对象方法是什么?

2 个答案:

答案 0 :(得分:4)

首先,如果你想遵循"组合而不是继承"然后超过一半的解决方案不合适,因为你仍然使用继承。

实际上用"组合而不是继承"来实现它。存在多种不同的方式,可能每种方式都有自己的优势和劣势。首先是一种可能的方式但目前不在C#中。至少没有一些扩展重写IL代码。一个想法通常是使用mixins。所以你有接口和Mixin类。 Mixin基本上只包含注入""进入一个班级。他们不是从中衍生出来的。所以你可以有这样一个类(所有代码都是伪代码)

class RobotDog 
    implements interface IEat, IBark
    implements mixin MEat, MBark

IEatIBark提供接口,而MEatMBark将是mixins,您可以注入一些默认实现。这样的设计可以在JavaScript中实现,但目前不在C#中。它的优势在于,您最终拥有RobotDog类,其中包含IEatIBark的所有方法以及共享实现。而且这也是一个缺点,因为你用很多方法创建大类。最重要的是可能存在方法冲突。例如,当您要注入两个具有相同名称/签名的不同接口时。尽管这种方法首先看起来很好,但我认为缺点很大,我不会鼓励这样的设计。

由于C#不直接支持Mixins,您可以使用扩展方法以某种方式重建上面的设计。所以你仍然有IEatIBark接口。并为接口提供扩展方法。但它与mixin实现具有相同的缺点。所有方法都出现在对象上,方法名称冲突的问题。同样最重要的是,组合的想法也是你可以提供不同的实现。您也可以为同一界面使用不同的Mixins。最重要的是,mixins只适用于某种默认实现,我们仍然可以覆盖或更改方法。

使用扩展方法做这种事情是可能的,但我不会使用这样的设计。理论上,您可以创建多个不同的命名空间,因此根据您加载的命名空间,您可以获得具有不同实现的不同扩展方法。但是这样的设计对我来说更加尴尬。所以我不会使用这样的设计。

我如何解决它的典型方法是期望你想要的每个行为的字段。所以你的RobotDog看起来像这样

class RobotDog(ieat, ibark)
    IEat  Eat  = ieat
    IBark Bark = ibark

所以这意味着。您有一个包含两个属性EatBark的类。这些属性的类型为IEatIBark。如果您要创建RobotDog实例,则必须传入要使用的特定IEatIBark实施。

let eat  = new CatEat()
let bark = new DogBark()
let robotdog = new RobotDog(eat, bark)

现在RobotDog会像猫一样吃,而树皮像狗一样。你可以调用RobotDog应该做的事情。

robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()

现在,RobotDog的行为完全取决于您在构造对象时提供的注入对象。您还可以在运行时使用另一个类切换行为。如果您的RobotDog在游戏中并且Barking升级,您可以在运行时将Bark替换为另一个对象以及您想要的行为

robotdog.Bark <- new DeadlyScreamBarking()

通过改变它或创建一个新对象。您可以使用可变或不可变的设计,这取决于您。所以你有代码共享。至少我更喜欢这种风格,因为你不是拥有一个拥有数百种方法的对象,而是基本上拥有一个具有不同对象的第一层,每个对象都清晰地分开。例如,如果您添加移动到您的RobotDog类,您只需添加一个&#34; IMovable&#34;属性和该接口可以包含多个方法,如MoveToCalculatePathForwardSetSpeed等。在robotdog.Move.XYZ下,它们可以干净利落地使用。碰撞方法也没有问题。例如,每个类可能有相同名称的方法没有任何问题。在顶部。您还可以拥有相同类型的多个行为!例如,Health和Shield可以使用相同的类型。例如,一个简单的&#34; MinMax&#34;包含最小/最大和当前值的类型以及对它们进行操作的方法。 Health / Shield基本上具有相同的行为,并且您可以使用此方法轻松地在同一个类中使用其中两个,因为没有方法/属性或事件发生冲突。

robotdog.Health.Increase(10)
robotdog.Shield.Increase(10)

之前的设计可能会略有改变,但我认为它不会让它变得更好。但是很多人无脑地采用每种设计模式或法律,希望它能自动使一切变得更好。我想在这里提到的是我认为很糟糕的Law-of-Demeter,特别是在这个例子中。实际上存在很多关于它是否好的讨论。我认为这不是一个好的规则,在这种情况下它也变得很明显。如果你遵循它,你必须为你拥有的每个对象实现一个方法。而不是

robotdog.Eat.Fruit()
robotdog.Eat.Drink()

你在RobotDog上实现了在Eat字段上调用某个方法的方法,那么你最终得到了什么?

robotdog.EatFruit()
robotdog.EatDrink()

你还需要再次解决像

这样的碰撞
robotdog.IncreaseHealt(10)
robotdog.IncreaseShield(10)

实际上你只是写了很多方法,只是委托给一个字段上的其他方法。但是你赢得了什么?基本上什么都没有。你只是遵循无脑规则。你理论上可以说。但是EatFruit()可以在调用Eat.Fruit()之前做一些不同的事情或做些额外的事情。 Weel是可能的。但是如果你想要其他不同的Eat行为,那么你只需要创建另一个实现IEat的类,并在实例化它时将该类分配给robotdog。

从这个意义上讲,得墨忒耳法不是点数运动。

http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx/

作为结论。如果您遵循该设计,我会考虑使用第三个版本。使用包含Behavior对象的Properties,您可以直接使用这些行为。

答案 1 :(得分:2)

我认为这更像是一个概念上的困境,而不是一个构成问题。

当你说: 并在Dog和RobotDog类中实现它

public class Dog : Animal, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}
public class RobotDog : Robot, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}

但我们又一次有重复的代码!

如果Dog和RobotDog具有相同的Bark()实现,则它们应该从Animal类继承。但是如果他们的Bark()实现不同,那么从IBarkable接口派生是有意义的。否则,Dog和RobotDog之间的区别在哪里?