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

chr*_*rge 11 c# oop inheritance interface

灵感来自关于使用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() { }
}
Run Code Online (Sandbox Code Playgroud)

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


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

public class RobotDog : Robot
{
    public void Bark() { Console.WriteLine("Bark"); }
}
Run Code Online (Sandbox Code Playgroud)

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


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

public class WorldObject
{
    public void Bark() { Console.WriteLine("Bark"); }
}
public class Animal : WorldObject { ... }
public class Robot : WorldObject { ... }
Run Code Online (Sandbox Code Playgroud)

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


第三种解决方案是为可以Bark的类创建一个IBarkable接口

public interface IBarkable
{
    void Bark();
}
Run Code Online (Sandbox Code Playgroud)

并在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"); }
}
Run Code Online (Sandbox Code Playgroud)

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


第四种方法是再次使用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!"); 
    }
}
Run Code Online (Sandbox Code Playgroud)

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


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

Dav*_*aab 5

起初,如果您想遵循“组合优于继承”,那么超过一半的解决方案不适合,因为您仍然在这些解决方案中使用继承。

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

class RobotDog 
    implements interface IEat, IBark
    implements mixin MEat, MBark
Run Code Online (Sandbox Code Playgroud)

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


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

用扩展方法做那种事情是可能的,但我不会使用这样的设计。理论上,您可以创建多个不同的命名空间,因此根据加载的命名空间,您将获得具有不同实现的不同扩展方法。但是这样的设计我觉得比较别扭。所以我不会使用这样的设计。


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

class RobotDog(ieat, ibark)
    IEat  Eat  = ieat
    IBark Bark = ibark
Run Code Online (Sandbox Code Playgroud)

所以这意味着。您有一个包含两个属性EatBark. 这些属性的类型为IEatIBark。如果你想创建一个RobotDog实例,那么你必须在一个特定的通过IEatIBark实施,你要使用。

let eat  = new CatEat()
let bark = new DogBark()
let robotdog = new RobotDog(eat, bark)
Run Code Online (Sandbox Code Playgroud)

现在 RobotDog 会像猫一样吃东西,像狗一样吠叫。您只需调用 RobotDog 应该执行的操作即可。

robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()
Run Code Online (Sandbox Code Playgroud)

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

robotdog.Bark <- new DeadlyScreamBarking()
Run Code Online (Sandbox Code Playgroud)

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

robotdog.Health.Increase(10)
robotdog.Shield.Increase(10)
Run Code Online (Sandbox Code Playgroud)

以前的设计可能会略有改变,但我不认为它会变得更好。但是很多人无脑地采用每一种设计模式或法则,希望它能自动让一切变得更好。我想在这里提到的是Law-of-Demeter我认为很糟糕的经常被称为,特别是在这个例子中。其实好与不好的讨论很多。我认为这不是一个好的规则,在这种情况下它也变得显而易见。如果你遵循它,你必须为你拥有的每个对象实现一个方法。所以代替

robotdog.Eat.Fruit()
robotdog.Eat.Drink()
Run Code Online (Sandbox Code Playgroud)

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

robotdog.EatFruit()
robotdog.EatDrink()
Run Code Online (Sandbox Code Playgroud)

您还需要再次解决碰撞,例如

robotdog.IncreaseHealt(10)
robotdog.IncreaseShield(10)
Run Code Online (Sandbox Code Playgroud)

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

从这个意义上说,得墨忒耳定律不是点数练习。

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


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