我们有一个模拟鸭子的游戏,需要维护更新,让我们先看看目前的代码
所有的鸭子都会呱呱叫(Quack)也会游泳(Swim),所以由父类负责处理
public class Duck
{
public virtual void quack()
{
Console.WriteLine("呱呱呱");
}
public virtual void swim()
{
Console.WriteLine("仰泳");
}
// 因为每一种鸭子的外观都不同,所以将display()作为抽象方法
// 每个鸭子子类负责实现自己的display()行为在屏幕上显示其外观
public abstract void display();
}
//绿头鸭
public class MallardDuck:Duck
{
public void display()
{
Console.WriteLine("外观是绿头");
}
}
public class RedheadDuck:Duck
{
public override void display()
{
Console.WriteLine("外观是红头");
}
}
现在为了鸭子模拟游戏有更高的可玩性,我们要让所有的鸭子都会飞。
大家脑中首先想到的是不是去父类添加一个fly()(我首先想到的就是去父类添加fly(),TAT 其实这个是一个错误的想法,继续阅读下去吧),好了我们先添加一个fly()方法看看吧。
重写后的Duck父类代码
public class Duck
{
public virtual void quack()
{
Console.WriteLine("呱呱呱");
}
public virtual void swim()
{
Console.WriteLine("仰泳");
}
public abstract void display();
// 新添加的fly方法
public virtual void fly()
{
Console.WriteLine("飞起来了");
}
}
//添加新的橡皮鸭子类
public class RubberDuck:Duck
{
public override quack()
{
//橡皮鸭子不会呱呱叫,所有把quack()方法覆盖成吱吱叫
Console.WriteLine("吱吱吱");
}
public override void display()
{
Console.WriteLine("外观是橡皮鸭子");
}
}
看到这里我相信大家都发现了,所有继承Duck父类的鸭子子类都会飞了。
如果我们有一个橡皮鸭子子类继承了Duck父类,那就是说橡皮鸭子也会飞了这显然是错误的。
当然我们也可以把橡皮鸭子类中的fly()方法覆盖掉,就好像覆盖quack()的做法一样
public class RubberDuck:Duck
{
public override void quack()
{
Console.WriteLine("吱吱吱");
}
public override display()
{
Console.WriteLine("外观是橡皮鸭子");
}
public override fly()
{
// 覆盖,变成什么事情都不做
}
}
如果以后加入诱饵鸭子,不会叫也不会飞怎么办?覆盖掉父类的quack()方法,fly()方法吗?
这样利用继承来提供Duck的行为,会导致很多缺点,例如
当大家读到这里应该发现
乳沟以后每六个月更新(会更新新的方法等),每当有新的鸭子子类出现,我们就要被迫检查并可能需要覆盖fly()和quark()…这简直就是无穷无尽的噩梦。
所以,我们需要一个更清晰的方法,让“某些”(而不是全部)鸭子类型可飞可叫。
这个问题,我们肯定有更好的方法去解决那就是
我们可以把fly()从超类中取出来,放进一个"Flyable接口"中。这么一来,只有会飞的鸭子才实现此接口。同样的方式,也可以用来设计一个“Quackable接口”,因为不是所有的鸭子都会叫。
来看看改造后的代码吧
public interface Flayable
{
void fly();
}
public interface Quackable
{
void quack();
}
public class Duck
{
public virtual void swim()
{
Console.WriteLine("仰泳");
}
public abstract void display();
}
public class MallardDuck:Duck,Flyable,Quackable
{
public override display()
{
Console.WriteLine("绿头鸭子");
}
public void fly()
{
Console.WriteLine("飞起来了");
}
public void quack()
{
Console.WriteLine("呱呱呱");
}
}
public class RedheadDuck:Duck,Flyable,Quackable
{
public override display()
{
Console.WriteLine("红头鸭子");
}
public void fly()
{
Console.WriteLine("飞起来了");
}
public override quack()
{
Console.WriteLine("呱呱呱");
}
}
public RubberDuck:Duck,Quackable
{
public override display()
{
Console.WriteLine("橡皮鸭子");
}
public void quack()
{
Console.WriteLine("吱吱吱");
}
}
// 因为诱饵鸭子不会飞,也不会叫 所以只需要继承Duck父类就好了
public DecoyDuck:Duck
{
public override display()
{
Console.WriteLine("诱饵鸭子");
}
}
现在来看是实现了我们的目的:让“某些”(而不是全部)鸭子类型可飞可叫。但是我们又发现了另外几个问题
我们知道,并非“所有”的子类都具有飞行和呱呱叫的行为,所以继承并不是适当的解决方式。虽然Flyable与Quackable可以解决“一部分”问题(不会再有会飞的橡皮鸭),但是却造成了代码无法复用,这只能算是从一个噩梦跳进了另一个噩梦。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化…
现在我们知道使用继承并不能很好地解决问题,因为鸭子的行为在子类里不断改变,并且让所有的子类都会有这些行为不恰当的。Flyable与Quackable接口一开始似乎还挺不错,解决了问题(只有会飞的鸭子才继承Flyable),但是接口具有实现代码,所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,你必须的往下追踪并在每一个定义此行为的类中修改它,一不小心,可能会造成新的错误!
幸运的事,有一个设计原则,恰好适用于此状况。
换句话说,如果每次新的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其他稳定的代码所区分。
下面是这个原则的另一种思考方式:“把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部分”。
我们从上面的代码中可以分析得到fly()和quack()是需要变化,所以我们就把fly和quack中抽离出来,建立一组新类来代表每个行为。
如何设计那组实现飞行和呱呱叫的行为的类呢?
我们希望一切都有弹性,毕竟,正是因为一开始鸭子行为没有弹性,才让我们走上现在这条路。我们还想能够“指定”行为到鸭子的实例。比方说,我们想要产生一个新的绿头鸭实例,并指定特定“类型”的飞行行为给它。干脆顺便让鸭子的行为可以动态的改变好了。换句话说,我们应该在鸭子类中包含特定行为的方法,这样就可以在“运行时”动态地“改变”绿头鸭的飞行行为。
从现在开始,鸭子的行为将被放在分开的类中,此类专门提供某行为接口的实现。
这样,鸭子类就不再需要知道行为的实现细节。
我们利用接口代表每个行为,比方说,FlyBehavior与QucakBehavior,而行为的每个实现都将实现其中的一个接口。
所以这次鸭子类不会负责实现Flying与Quacking接口,反而是由我们制造一组其他的类专门实现FlyBehavior和QuackBehavior,这个称为“行为”类。由行为类而不是Duck类型来实现行为接口。
这样的作法迥异于以往,以前的做法是:行为来自Duck超类的具体实现,或是继承某个接口并由子类自行实现而来。这两种做法是依赖于”实现“,被实现绑的死死的,没办法更改行为(除非写更多代码)。
在我们的新设计中,鸭子的子类将使用接口(FlyBehavior与QuackBehavior)所表示的行为,所以实际的”实现“不会被绑死在鸭子的子类中。(换句话说,特定的具体行为编写在实现了FlyBehavior与QuackBehavior的类中)
下面就来看看FlayBehavior接口以及实现
public interface FlyBehavior
{
void fly();
}
public class FlyWithWings:FlyBehavior
{
public void Fly()
{
Console.WriteLine("飞起来了");
}
}
public class FlyNoWay:FlyBehavior
{
public void FlyNoWay()
{
Console.WriteLine("不会飞");
}
}
这里所谓的接口“接口”有多个含义,接口是一个“概念”,“针对接口编程”,关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上。“针对超类型编程”这句话,可以更明确的说成“变量的声明类型应该是超类型, 通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类是不用理会以后执行时的真正对象类型!”
在此,我们实现两个接口,FlyBehavior和QuackBehavior,还有它们对应的类,负责实现具体的行为:
//这是一个借口,所有飞行类都实现它,所有新的飞行类都必须实现fly()方法
public interface FlyBehavior
{
void fly();
}
public class FlyWithWings:FlyBehavior
{
public void fly()
{
Console.WriteLine("飞起来了");
}
}
public class FlyNoWay:FlyBehavior
{
public void fly()
{
Console.WriteLine("不会飞");
}
}
//呱呱叫行为也一样,一个接口只包含一个需要实现的quack()方法
public interface QuackBehavior
{
void quack();
}
public class Quack:QuackBehavior
{
Console.WriteLine("呱呱呱");
}
public class Squeak:QuackBehavior
{
Console.WriteLine("吱吱吱");
}
public class MuteQuack:QuackBehavior
{
Console.WriteLine("不会叫");
}
这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关了。
而我们也可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类
做法是这样的:
public class Duck
{
//行为变量被行为 行为“接口”类型
//每只鸭子都会引用实现FlyBehavior和QuackBehavior接口的对象
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
//取代了原quack方法
public void performQuack()
{
//鸭子对象不亲自处理呱呱叫行为,而是委托给quackBehavior引用的对象
quackBehavior.quack();
}
public void performFly()
{
flyBehavior.fly();
}
public virtual void swim()
{
Console.WriteLine("仰泳");
}
public abstract void display();
}
想进行呱呱叫的动作,Duck对象只要叫quackBehavior对象去呱呱叫就可以了。这部分的代码中,我们不在乎quackBehavior接口的对象到底是什么,我们只关心该对象如何进行呱呱叫就够了
2. 鸭子子类的实现
public class MallardDuck:Duck
{
public MallardDuck()
{
/*
绿头鸭使用Quack类处理呱呱叫,所以当performQuack()被调用时,叫
的职责委托给Quack对象,而我们得到了真正的呱呱叫。
*/
quackBehavior = new Quack();
// 使用FlyWithWings作为其FlyBehavior类型。
flyBehavior = new FlyWithWings();
}
public void display()
{
Console.WriteLine("绿头鸭")
}
}
当MallardDuck实例化时,它的构造器会把继承来的quackBehavior实例变量初始化成Quack类型的新实例(Quack是QuackBehavior的具体实现类)。
同样的处理方式也可以用在飞行行为上:MallardDuck的构造器将flyBehavior实例变量初始化成FlyWithWings类型的实例(FlyWithWings是FlyBehavior的具体实现类)。
但是这样做弹性还是不够大并且也没有实现动态指定飞行和呱呱叫的行为。
我们把quackBehavior和flyBehavior字段作为属性就可以了
public class Duck
{
public FlyBehavior flyBehavior{get;set;}
public QuackBehavior quackBehavior{get;set;}
......
}
//Main方法
static void Main()
{
Duck mallarDuck = new MallarDuck();
mallarDuck.performQuack();
mallarDuck.performFly();
mallarDuck.flyBehavior = new FlyNoWay();
mallarDuck.quackBehavior = new Squeak();
mallarDuck.performQuack();
mallarDuck.performFly();
}
不再把鸭子的行为说成“一组行为”,而是想成是“一簇算法”。算法代表鸭子能做的屎(不同的叫法和飞行法)。
请特别注意类之间“关系”。关系可以是一个IS-A(是一个)、HAS-A(有一个或Implements(实现)
“有一个”关系相当有趣:每一个鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和呱呱叫委托给它们代为处理。
当你将两个类结合起来使用,如同本例一般,这就是组合(composition)。这做法和“继承”不同的地方在于,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的。
使用组合建立系统具有很大的弹性,不仅可将算法簇封装成类,更可以“在运行时动态地改变行为”,只要组合的行为对象符合正确的接口标准即可。
码字不易,觉得有用的话点个赞吧。