欢迎来到设计模式的设计(第一篇)
在本章,你将学到为何(以及如何)利用开发人员的经验与智慧。本章结束前,我们会看到设计模式的用途与优点。再看一些关键的的OO设计原则,并通过一个实例来了解模式的用途与优点,并通过实列来了解设计模式是如何运作的。使用模式最好的方式是:把设计模式装到脑子里,在你的设计和应用中,寻找何处使用它们。以往是代码复用,现在是经验复用。
先从简单的模拟鸭子的应用做起
我们来做一款模拟鸭子的游戏给小朋友们玩,游戏中会出现各种鸭子,一边游戏戏水,一边嘎嘎叫。游戏中的技术使用了标准的OO,设计了一个鸭子超类,并让各种鸭子继承此超类。
我经常去西溪湿地国家公园跑步,有时候突然飞速的跑到鸭子跟前,吓唬它们一下。其实,鸭子每次发现我就跟兔子发现狐狸,吓得半死。没错,你猜猜鸭子怎么跑掉的,野外的鸭子都会飞,它们竟然学会了水上漂,在水上连飞带跑,我接着吼一声,它们竟然飞起来了。我要加一个飞行功能,你觉得加再哪里号?
但是,可怕的问题发生了。。。。。。
并非所有的鸭子都能够飞行,在Duck超类中加上新的行为,橡皮鸭飞了起来,没有生命的橡皮鸭飞行或许是一种特色,给它加入飞行行为显然不合适。使得某些并不适合该行为的子类也具有该行为,对代码所作的局部修改,影响层面可不止局部。
从中我们体会到为了复用(reuse)目的而使用继承,结局并不完美。
不久之后,加入了诱饵鸭(DecoyDuck),又会如何?诱饵鸭是木头假鸭,不会飞也不会叫。
在以后的维护中,每当由新的鸭子类出现,就要被迫检查并尽可能覆盖fly()和quard().....这真是无穷无尽的恶梦。
所以,需要一个更清晰的方法,让某些鸭子类型可飞或者可叫。我可以把fly从超类中取出来,放进一个Flyable接口中。这么一来,只有会飞的鸭子才实现此接口。同样的方式,也可以用来设计一个Quackable接口,因为不是所有的鸭子都叫叫。
你觉得这个设计如何?
如果你要设计,你会如何设计?
我们知道,并非所有的子类都有飞行和嘎嘎叫的方法,所以继承并不是适当的解决办法。虽然Flyable与Quackable 可以解决一部分问题,不会再有会飞的橡皮鸭,但是却造成代码无法复用,这只能算是从一个恶梦跳进另一个恶梦。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化......
此时,你可能正期盼着设计模式能骑着白马来解救你离开苦难的一天。但是,如果直接告诉你答案,这又有什么乐趣呢?我们会用老方法找出一个解决之道:采用良好的OO软件设计原则。
如果有一种对代码影响最小的方式来修改软件该有多好,我们就可以花较少的事件重构代码,花更多的时间去做更酷的事情。。。。。
不管当初软件设计得多好,一段时间过后,总是需要成长和改变。否则,软件就会死亡。
驱动改变的因素有很多,找出你的应用中需要该改变代码的原因,一 一列出来。
比如:我们的用户需要新功能
我们在程序中写了个Bug,需要修复
还有很多,等等
设计原则
找出应用中可能需要变化之处,把它独立出来,不要和那些不需要变化的代码混合在一起。这是我们的第一个设计原则,以后其它的设计原则会陆续出现。
换句话说,每次新的需求以来,某方面的代码就会发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其它稳定的代码有所区分。
下面是这个原则的另一种思考方式:把会变化的部分取出并封装起来,以便以后可以轻易的改变或扩充此部分,而不影响不需要变化的部分。
现在我们知道使用继承并不能很好的解决问题,因为鸭子的行为在子类里不断的改变,并且让所有的子类都有这些行为是不恰当的。Flyable和Quackable接口一开始挺不错的,解决了问题,但是Java接口不具有实现代码,所以继承接口无法达到代码复用。
好,该是把鸭子的行为从Duck类中取出的时候了!
分开变化和不会变化的部分
从哪里开始呢?就我们目前所知,除了fly()和quack()的问题之外,Duck类还算一切正常,似乎没有特别需要经常变化和修改的地方。所以,除了某些小改变之外,我们不打算对Duck类做太多处理。
现在,为了要分开变化和不会变化的部分,我们准备建立两组类(完全远离鸭子类),一个是fly相关的,一个是quack相关的,每一组类都将实现各自的动作。比方说,我们可能有一个类实现嘎嘎叫,另一个类实现吱吱叫,还有一个类是实现安静。
我们知道Duck类内的fly()和quack()会随着鸭子的不同而改变。
为了要把这两个行为从Duck类中分开,我们将把它们从Duck类中取出来,建立一组新类来代表每种行为(一种行为代表一个类)
如何实现那组飞行和呱呱叫的行为的类呢?
我们希望一切能够弹性,毕竟,正式因为一开始鸭子行为没有弹性,才让我们走到现在这条路。我们还想指定行为到鸭子的实列。比方说,我们想要产生一个新的绿头鸭实列,并指定特定类型的行为实例给它。干脆让鸭子的行为可以动态改变好了。
有了这些目标要实现,接着看看第二个设计原则:
针对接口编程,而不是针对实现编程。
我们利用接口代表每个行为,比方说,FlyBehavior 与 QuackBechavior ,而行为的每个实现都将实现其中一个接口。
所以这次鸭子类不会负责实现Flying与Quacking接口,反而是由我们制造一组其它类专门实现FlyBehavior 与 QuackBechavior,这就称为 行为类。由行为类而不是Duck类来实现行为接口。
这样的作法不同以往,以前的做法是:行为来自Duck超类的具体实现,我们被实现绑的死死的,没办法更改行为。
针对接口编程,真正的意思是针对超类型(supertype)编程
针对超类型编程这句话,可以更明确的说成 变量的类型声明应该是超类型,通常是一个抽象类或者一个接口。
这里所谓的接口有多个含义,接口是一个概念,针对接口编程,关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际情况执行到真正的行为,不会绑死在超类型行为上。只要实现具体超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后执行时的真正对象类型。
实现鸭子的行为
问:用一个类代表一个行为,感觉似乎有点奇怪。类不是应该代表某种东西吗?类不是同时具备状态和行为吗?
答:再OO系统中,是的,类代表的事务一般都是既有状态(实列变量),又有方法。只是在本列中,碰巧事务是行为。但是,即使是行为,也仍然有状态和方法,例如,飞行的行为可以具有实列变量,记录飞行行为的属性(每秒翅膀拍动击下,最大飞行高度和飞行速度等)。
问:Duck是不是也该设计成一个接口?
答:在本例中,这么做并不恰当。如你所见,我们已经让一切都整合妥当,而且让Duck称为一个具体类,这样可以让衍生的特定类(例如绿头鸭)具有Duck共同的属性和方法。我们已经从Duck的继承结构中删除了变化的部分,原先的问题都已经解决了,所以不需要把Duck类设计成接口。
整合鸭子的行为
关键在于,鸭子现在会将飞行和呱呱叫的动作委托别人处理,而不是使用定义在Duck类(或子类)的呱呱叫和飞行方法。
做法是这样:
首先,在Duck类中加入两个实列变量,分别为FlyBehavior 与 QuackBehavior,声明为接口类型,每个鸭子对象都会动态设置这些变量在运行时引用正确的类型。
我们也必须将Duck类与其所有子类中的fly()与quack()删除,因为这些行为已经搬到FlyBehavior与QuackBehavior类中了。
我们用两个相似的方法performFly()和performQuack()取代Duck类中的fly()和quack()。稍后你就会知道为什么。
现在我们来实现performQuack()
package head.first.strategy;
public class Duck {
QuackBehavior quackBehavior;
public void perfomQuack() {
quackBehavior.quack();
}
}
很容易,是吧?想进行呱呱叫的动作,Duck对象只要叫quackBehavior对象去呱呱叫就可以了。在这部分代码中,我们不在乎quackBehavior接口的对象到底是什么,我们只关心该对象如何进行呱呱叫就够了。
package head.first.strategy;
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
@Override
public void display() {
System.out.println("我是一只绿头鸭");
}
}
所以,绿头鸭会真的 呱呱叫 ,而不是 吱吱叫,或叫不出声。这是怎么办到的?当MallardDuck实例化时,它的构造器会把继承来的quackBehavior实列变量初始化成Quack类型的新实列。
同样的处理方式也可以用在飞行行为上:MallardDuck的构造器将flyBehavior实列变量初始化成FlyWithWings类型的实列。
测试Duck的代码
package head.first.strategy;
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public Duck() {
}
public void perfomQuack() {
quackBehavior.quack();
}
public void perfomFly() {
flyBehavior.fly();
}
public abstract void display();
public void swim() {
System.out.println("所有的鸭子都会游泳,包括假鸭!");
}
}
FlyBehavior接口与两个行为实现类
package head.first.strategy;
public interface FlyBehavior {
void fly();
}
package head.first.strategy;
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("煽动翅膀飞行");
}
}
package head.first.strategy;
public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
System.out.println("不会飞");
}
}
QuackBehavior 接口及其三个实现类
package head.first.strategy;
public interface QuackBehavior {
void quack();
}
package head.first.strategy;
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("呱呱叫");
}
}
Squeak
package head.first.strategy;
public class MuteQuack implements QuackBehavior {
@Override
public void quack() {
System.out.println("不出声");
}
}
package head.first.strategy;
public class Squeak implements QuackBehavior {
@Override
public void quack() {
System.out.println("吱吱叫");
}
}
编译测试类
package head.first.strategy;
public class MiniDucksSimulator {
public static void main(String[] args) {
Duck duck=new MallardDuck();
//这会调用MallardDuck继承来的performQuack()方法,进而委托quackBehavior对象处理。
duck.perfomFly();
duck.perfomQuack();
}
}
运行代码:
煽动翅膀飞行
呱呱叫
动态设定行为
在Duck类中,动态增加两个新方法
//我们可以随时调用这两个方法改变鸭子的行为
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
从此以后,我们可以 随时 调用这两个方法改变鸭子的行为。
制造一个新的鸭子类型
package head.first.strategy;
public class ModelDuck extends Duck {
public ModelDuck() {
//一开始,我们的模型鸭是不会飞的
flyBehavior = new FlyNoWay();
quackBehavior = new MuteQuack();
}
@Override
public void display() {
System.out.println("我是一只模型鸭子");
}
}
建立一个新的飞行行为
package head.first.strategy;
/**
* 我们建立一个火箭动力飞行的行为
*/
public class FlyRocketPowered implements FlyBehavior {
@Override
public void fly() {
System.out.println("火箭动力飞行");
}
}
改变测试类
package head.first.strategy;
public class MiniDuckSimplator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.perfomQuack();
mallard.perfomFly();
System.out.println("-----------------------------------");
Duck model=new ModelDuck();
model.perfomFly(); //调用此方法会被委托给flyBehavior对象,也就时FlyNoWay实例,该对象是在模型鸭
//构造器中设置的
model.perfomQuack();
model.setFlyBehavior(new FlyRocketPowered()); //这回调用继承来的setter方法,把火箭动力飞行行为设定到模型鸭中。
//哇!模型鸭突然具有了火箭动力飞行行为!
model.perfomFly(); //如果成功了,就意味着可以动态改变它的飞行行为。如果把行为的实现绑死在鸭子中,就无法做到这样了。
}
}
运行:
呱呱叫
煽动翅膀飞行
-----------------------------------
不会飞
不出声
火箭动力飞行
封装行为的大局观
我们已经深入研究了鸭子模拟器的设计,该是将头探出水面,呼吸空气的时候了。现在就来看看整体的格局。
下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck,飞行行为实现FlyBehavior接口,呱呱叫行为实现QuackBehavior接口。
也请注意,我们描述事情的方式也有所改变。不要把鸭子的行为说成是一组行为,我们开始把行为想象成一簇算法。想想看,在MiniDuckSimplator的设计中,算法代表鸭子能做的事,这样的做法也能很容易地用一群类计算不同地区的销售金额。
请特别注意类之间的关系。拿起笔,把下面图像中的每个箭头标上适当的关系,关系可以实IS-A(是一个),HAS-A(有一个)或IMPLEMENTS(实现)。
有一个可能比是一个更好
有一个关系相当有趣,每一个鸭子都有一个FlyBehavior和QuackBehavior,好将飞行和呱呱叫委托给它们代为处理。
当你将两个类结合起来使用,如同本例一般,这就是组合。这中做法和“继承”不同的地方在于,鸭子的行为不是继承而来的,而是和适当的行为对象组合来的。
这是一个很重要的技巧,其实是使用了我们的第三个设计原则:
多用组合,少用继承
如你所见,使用组合建立系统有很大的弹性,不仅可见算法族封装成类,更可以"在运行时动态改变行为",只要组合的行为对象符合正确的接口标志即可。
恭喜你学会了第一个设计模式,为了介绍这个设计模式,我们走了很长一段路。下面是此模式的正式定义:
策略模式 定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立与使用算法的客户。
模式让我们用更少的词汇做更充分的沟通。
当你用模式描述的时候,其它开发人员很容易理解你对设计的想法。
共享词汇可以帮助你的团队快速充电
对于设计模式有深入了解的团队,彼此之间对于设计模式的看法不容易产生误解。
如果设计模式这么棒,怎么没人建立相关的库呢?那样我们就不必自己动手了。
答:设计模式比库的等级更高。设计模式告诉我们如何组织类和对象以解决某种问题,而且采纳这些设计并使它们适合我们特定的应用,是我们的责任。
库和框架不也是设计模式吗?
答:库和框架提供了特定功能的实现,让我们的代码可以轻易引用和实现某种功能,但是这并不是设计模式。有些时候,库和框架用到了设计模式,这样很好,一旦你了解设计模式,能够更好的使用API.
记住,知道抽象,封装,继承和多态这些概念,并不会马上让你变成好的面向对象设计者。设计大师关心的是建立弹性的设计,可以维护,可以应付变化
你几乎快要读完了,你已经在你的设计工具箱内放进了几样工具,在我们进入第二章之前,先将这些工具一一列出。
项目地址:https://github.com/githubwwj/DesignPattern