受到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() { }
}
问题是我想添加可以吠叫但不能进食的RobotDog类。
第一个解决方案是将RobotDog创建为Robot的子类,
public class RobotDog : Robot
{
public void Bark() { Console.WriteLine("Bark"); }
}
但是要给它一个Bark函数,我们必须复制并粘贴Dog's Bark函数,所以现在我们有了重复的代码。
第二种解决方案是使用Bark方法创建一个通用的父类(super class),然后从该类继承Animal和Robot类。
public class WorldObject
{
public void Bark() { Console.WriteLine("Bark"); }
}
public class Animal : WorldObject { ... }
public class Robot : WorldObject { ... }
但是现在,每个动物和每个机器人都将具有Bark方法,而大多数人都不需要。继续这种模式,子类将被加载不需要的方法。
第三种解决方案是为可以吠叫的类创建IBarkable接口(interface)
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接口(interface),但是创建一个Bark帮助器类,然后每个Dog和RobotDog接口(interface)实现都调用该类。
这似乎是最好的方法(以及视频似乎推荐的方法),但是我也可以看到该项目因助手而变得困惑不堪。
第五种建议的(hacky?)解决方案是将扩展方法卡在空的IBarkable接口(interface)上,这样,如果您实现IBarkable,则可以使用Bark
public interface IBarker { }
public static class ExtensionMethods
{
public static void Bark(this IBarker barker) {
Console.WriteLine("Woof!");
}
}
这个站点上的许多类似的已回答问题以及我已阅读的文章似乎都建议使用
abstract
类,但是,这与解决方案2不会有相同的问题吗?将RobotDog类添加到此示例的最佳面向对象的方法是什么?
最佳答案
首先,如果您要遵循“继承之上的构成”,那么一半以上的解决方案不合适,因为您仍在其中使用继承。
实际上,使用“继承之上的构成”来实现它有多种不同的方式,可能每种方式都有自己的优点和缺点。最初,这是一种可能的方式,但目前在C#中尚无法实现。至少没有一些重写IL代码的扩展程序。一种想法通常是使用mixin。因此,您具有接口(interface)和Mixin类。 Mixin基本上只包含“注入(inject)”到类中的方法。他们不是从中衍生出来的。因此,您可以拥有一个像这样的类(所有代码都在伪代码中)
class RobotDog
implements interface IEat, IBark
implements mixin MEat, MBark
IEat
和IBark
提供了接口(interface),而MEat
和MBark
将是具有一些默认实现的mixins,您可以将其注入(inject)。这样的设计可以在JavaScript中实现,但目前在C#中尚无法实现。这样做的好处是,最后有一个RobotDog
类,该类具有IEat
和IBark
的所有方法,并具有共享的实现。同时这也是一个缺点,因为您使用许多方法创建了大类。最重要的是,可能存在方法冲突。例如,当您要注入(inject)两个具有相同名称/签名的不同接口(interface)时。尽管这种方法看起来不错,但我认为缺点很大,因此我不鼓励这样的设计。由于C#不直接支持Mixins,因此您可以使用扩展方法以某种方式重新构建上面的设计。因此,您仍然具有
IEat
和IBark
接口(interface)。然后,您为接口(interface)提供扩展方法。但是它具有与mixin实现相同的缺点。所有方法都出现在对象上,方法名称冲突。最重要的是,组合的想法还在于您可以提供不同的实现。您也可以为同一接口(interface)使用不同的Mixins。最重要的是,mixin只是用于某种默认实现,想法仍然是您可以覆盖或更改方法。用扩展方法做这种事情是可能的,但是我不会使用这样的设计。从理论上讲,您可以创建多个不同的 namespace ,因此根据您加载的 namespace ,您将获得具有不同实现方式的不同扩展方法。但是这样的设计对我来说比较尴尬。所以我不会使用这样的设计。
我如何解决该问题的典型方法是通过期望您想要的每种行为的字段。所以您的RobotDog看起来像这样
class RobotDog(ieat, ibark)
IEat Eat = ieat
IBark Bark = ibark
因此,这意味着。您有一个包含两个属性
Eat
和Bark
的类。这些属性的类型为IEat
和IBark
。如果要创建RobotDog
实例,则必须传递要使用的特定IEat
和IBark
实现。let eat = new CatEat()
let bark = new DogBark()
let robotdog = new RobotDog(eat, bark)
现在,RobotDog会像猫一样吃东西,而Bark会像狗一样吃东西。您只可以调用您的RobotDog应该做什么。
robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()
现在,RobotDog的行为完全取决于您在构造对象时提供的注入(inject)对象。您还可以在运行时使用另一个类切换行为。如果您的RobotDog在游戏中并且Barking升级了,您可以在运行时用另一个对象和您想要的行为替换Bark
robotdog.Bark <- new DeadlyScreamBarking()
可以通过对其进行突变来进行创建,也可以通过创建新对象来实现。您可以使用可变或不可变的设计,具体取决于您。这样就可以共享代码了。至少我更喜欢这种样式,因为与拥有数百种方法的对象相比,您基本上拥有的是第一层包含不同对象的第一层,每种对象的能力都清晰地分开了。例如,如果将“移动”添加到RobotDog类,则可以添加“IMovable”属性,并且该接口(interface)可以包含多个方法,例如
MoveTo
,CalculatePath
,Forward
,SetSpeed
等。它们将在robotdog.Move.XYZ
下完全可用。碰撞方法也没有问题。例如,在每个类上可以存在具有相同名称的方法,而不会出现任何问题。并在顶部。您还可以具有相同类型的多个行为!例如,Health和Shield可以使用相同的类型。例如,一个简单的“MinMax”类型,其中包含一个最小值/最大值和当前值以及对其进行操作的方法。 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)
实际上,您只是编写了很多方法,这些方法仅委托(delegate)给字段上的其他一些方法。但是你赢了什么?基本上什么都没有。您只是无脑地遵循了一条规则。从理论上讲,您可以说。但是
EatFruit()
在调用Eat.Fruit()
之前可以做一些不同的事情或做其他事情。 Weel是的,可能是这样。但是,如果您想要其他不同的Eat行为,则只需创建另一个实现IEat
的类,然后在实例化该类时将其分配给robotdog。从这个意义上讲,得墨meter耳定律不是点算练习。
http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx/
作为结论。如果您遵循该设计,我会考虑使用第三个版本。使用包含“行为”对象的属性,您可以直接使用这些行为。
关于c# - 如何最大化此接口(interface)中的代码重用与继承C#示例,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/35368684/