受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类添加到此示例中的最佳面向对象方法是什么?
答案 0 :(得分:4)
首先,如果你想遵循"组合而不是继承"然后超过一半的解决方案不合适,因为你仍然使用继承。
实际上用"组合而不是继承"来实现它。存在多种不同的方式,可能每种方式都有自己的优势和劣势。首先是一种可能的方式但目前不在C#中。至少没有一些扩展重写IL代码。一个想法通常是使用mixins。所以你有接口和Mixin类。 Mixin基本上只包含注入""进入一个班级。他们不是从中衍生出来的。所以你可以有这样一个类(所有代码都是伪代码)
class RobotDog
implements interface IEat, IBark
implements mixin MEat, MBark
IEat
和IBark
提供接口,而MEat
和MBark
将是mixins,您可以注入一些默认实现。这样的设计可以在JavaScript中实现,但目前不在C#中。它的优势在于,您最终拥有RobotDog
类,其中包含IEat
和IBark
的所有方法以及共享实现。而且这也是一个缺点,因为你用很多方法创建大类。最重要的是可能存在方法冲突。例如,当您要注入两个具有相同名称/签名的不同接口时。尽管这种方法首先看起来很好,但我认为缺点很大,我不会鼓励这样的设计。
由于C#不直接支持Mixins,您可以使用扩展方法以某种方式重建上面的设计。所以你仍然有IEat
和IBark
接口。并为接口提供扩展方法。但它与mixin实现具有相同的缺点。所有方法都出现在对象上,方法名称冲突的问题。同样最重要的是,组合的想法也是你可以提供不同的实现。您也可以为同一界面使用不同的Mixins。最重要的是,mixins只适用于某种默认实现,我们仍然可以覆盖或更改方法。
使用扩展方法做这种事情是可能的,但我不会使用这样的设计。理论上,您可以创建多个不同的命名空间,因此根据您加载的命名空间,您可以获得具有不同实现的不同扩展方法。但是这样的设计对我来说更加尴尬。所以我不会使用这样的设计。
我如何解决它的典型方法是期望你想要的每个行为的字段。所以你的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会像猫一样吃,而树皮像狗一样。你可以调用RobotDog应该做的事情。
robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()
现在,RobotDog的行为完全取决于您在构造对象时提供的注入对象。您还可以在运行时使用另一个类切换行为。如果您的RobotDog在游戏中并且Barking升级,您可以在运行时将Bark替换为另一个对象以及您想要的行为
robotdog.Bark <- new DeadlyScreamBarking()
通过改变它或创建一个新对象。您可以使用可变或不可变的设计,这取决于您。所以你有代码共享。至少我更喜欢这种风格,因为你不是拥有一个拥有数百种方法的对象,而是基本上拥有一个具有不同对象的第一层,每个对象都清晰地分开。例如,如果您添加移动到您的RobotDog类,您只需添加一个&#34; IMovable&#34;属性和该接口可以包含多个方法,如MoveTo
,CalculatePath
,Forward
,SetSpeed
等。在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之间的区别在哪里?