假设你所在的公司开发了一套鸭子模拟游戏,它可以模拟各种不同的鸭子,在水上游泳,同时还能发出“嘎嘎”的叫声,相当真实,因此卖的很不错。这个游戏是用标准的OO技术来设计的,一个抽象的Duck基类,有发出“嘎嘎”叫声的Quack方法和在水里游泳的Swim方法,同时它还有一个抽象的Display方法,每一个Duck子类(如MallardDuck、RedheadDuck)都将之重写,以便实现自己与众不同的外观。如下图:
你们公司有很多竞争对手,他们可不是吃素的,在日益增大的市场压力下,你们老板做出了一个决定,要改进这个游戏,让游戏里的鸭子飞起来,成功的话,一定可以打败所有的人。哦,这个艰巨的任务就交给你了,你是名很好的OO程序员,不是吗?
接到任务,你马上就开始了。这还不容易?在Duck基类里加一个Fly方法,这样所有的Duck子类都可以获得这个方法,所有的鸭子都可以飞了。太简单了,这就是OO的威力呀。
第二天,你高高兴兴地到公司上班,昨天已经把改好的程序交给老板了,他看过之后肯定会说,这小子,做事情做的真麻利。你还在想象着老板会怎样夸奖自己的时候,电话响了,老板打来的:“我现在在董事会上,刚把你的程序做了一个演示,鸭子能飞了,但是我怎么看见一只橡皮鸭也在飞?玩笑开大了吧?小心点,不要让我炒了你!”真吓人,稍稍平静了一下受惊的心情,仔细想想,确实是自己考虑的不够周到。并不是所有的鸭子都可以飞,可是把Fly方法放在基类里,那所有种类的鸭子都能飞了呀。怎么办?
你灵机一动,又想了一个方法,在橡皮鸭的子类中,把Fly方法重写掉,让它什么都不做!一阵高兴过后,你又想了“假如以后我又要加一个木制的诱饵鸭的时候,我该怎么办,它们不能叫也不能飞。”是呀,怎么办?又把Fly和Quack方法重写掉?一两个还好,假如有几十个,上百个,一个一个去重写?太可怕了,刚刚变好的心情一下子又变的糟糕起来。
你认识到,把Fly和Quack方法放在基类中,然后通过继承来达到老板想要的效果的方法是不行的。你需要一个清晰明了的解决方案。这时,你猛地拍了一下脑袋,用接口怎么样?
使用接口
把Fly方法从Duck基类中提取出来,放在一个叫IFlyable的接口中,同样地,把Quack方法也提取出来,放到IQuackable接口中。这样只有那些需要飞行动作的Duck子类才去实现IFlyable接口,需要能发出叫声的Duck子类才去实现IQuackable接口。比如RubberDuck不能飞,所以不实现IFlyable接口,但它可以发出吱吱声,所以要实现IQuackable接口,又如DecoyDuck不能飞也不能叫,所以它只用从Duck基类继承就可以了。UML图如下:
这个方法似乎把问题解决了,但是再仔细想想,确实是这样吗?
由于C#的接口中只允许声明成员签名,而不允许有任何代码实现,所以每一个实现IFlyable接口的子类都需要提供自己的Fly方法实现,这样就造成了大量的重复代码。试想一下,假如程序里一共有48个实现了IFlyable接口的子类,实然有一天老板说“我们需要改变一下Fly方法的实现(比如需要鸭子能飞的更高一些)”,那你该怎么办?一个一个地把那48个子类全部改一遍?天啊,这是不可想象的。这个方法只能说把你从一个噩梦带到了另一个噩梦,并没有解决问题。
事实是这样的,用户的需求每天都在变(天知道他们想干嘛?),假如我们能够尽可能的减少因用户需求改变而造成的程序改动,那我们的活儿要轻松的多,我们可以把更多的时间放在我们自己感兴趣的事上(踢踢球,打打游戏),而不是下班后还在老板的眼皮下加班加班再加班。
应该怎么办呢?这里会提到一个准则,它几乎是所有设计模式的精髓,那就是:
把变化的部分提取出来,将之封装
简而言之,就是把预期会发生变化的代码分离出来,单独放在一块儿,以后我们就可以很容易地修改它,而不会影响到已有的代码。
我们看看,在这里的这种情况下,我们应该怎么做?
对接口而非对实现编程
先分析一下造成麻烦的原因,老板需要程序做一些改动,在这里,就是两个方法,Fly和Quack方法。这两个方法需要有很多种不同的实现,而且以后还有可能会增加、修改、删除,那么按照上面的原则,我们就应该把这部分会变动的代码提取出来,将之与Duck基类分离。这样,以后这部分代码的改动就不会影响到Duck类。清楚了问题所在,那就快点动手解决吧!
等等,还需要了解一个东西——多态。
简要说一下多态。
什么叫多态?多态就是指为同名的方法提供不同的实现的能力,它使得我们不用关心方法的具体实现而仅仅依靠其名称来进行调用操作。
通过多态我们能干嘛?多态的用处相当大!概括地说,通过它,我们可以使代码更加清晰、简练,具体可以看看Allen的文章《今天你多态了吗?》
好了,你是多么好的一名OO程序员,多态应该是了如指掌了。我们来看看怎么用多态来解决这个问题。
把变化的部分提取出来:提取Fly方法,把它放到IFlyBehavior接口中,不同的Fly行为类(子类型)都实现这个接口,比如FlyWithWings,FlyNoWay等。同样地,提取Quack方法,放到IQuackBehavior接口中,不同的Quack行为类都实现这个接口,如Quacks,Squeak,MuteQuack等。如下图:
如果想在运行时动态的设置Duck子类的行为,还可以加入两个Set方法,SetFlyBehavior、SetQuackBehavior,在里面改变flybehavior和quackbehavior变量的值。这样,就可以让一个诱饵鸭摇身一变,变成红头鸭,嘎嘎的飞上天了。
下面是Duck基类代码:
namespace DesignPatterns
{
public abstract class Duck
{
protected IFlybehavior flybehavior;
protected IQuackbehavior quackbehavior;
public void Swim()
{
Console.WriteLine("Swim");
}
public void PerformFly()
{
this.flybehavior.Fly();
}
public void PerformQuack()
{
this.quackbehavior.Quack();
}
public abstract void Display();
}
}
举一个子类的例子:RedheadDuck子类代码:
namespace DesignPatterns
{
public class RedheadDuck : Duck
{
public RedheadDuck()
{
// 红头鸭可以飞
this.flyable = new FlyWithWings();
// 红头鸭可以叫
this.quackable = new Quack();
}
public override void Display()
{
Console.WriteLine("I'm a RedheadDuck");
}
}
}
好了,这样完成后,代码的稳定性得到了空前的提高。以后如果需要增加一些更为有趣的行为,比如:与火箭一起飞,那么你应该怎么做呢?只需建一个叫FlyWithRocket类,让它实现IFlybehavior接口,在Fly方法中定义自己的实现就可以了,不用去改动现有的代码。这可太棒了!以后你的工作可就轻松咯。
好了,让我们换个角度想想,把鸭子的一系列行为想象成一系列的算法,比如员工工资计算算法,菜单的绘制算法等。我们把这些算法封装起来,那么其它类就可以调用不同的算法来计算出最后的结果。如果算法有所改变,只需改动这些包含算法的类,而不用改动使用这些算法的类。
最后引出了策略模式的定义:针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换,它使得算法可以在不影响客户端的情况下发生变化。