先从模拟鸭子游戏说起,游戏中会出现各种鸭子,一边游泳,一边瓜瓜叫,由此设计了一个superclass,并让各种鸭子继承此超类。
现在要增加会飞的鸭子,如果在Duck类增加了fly(),则出现问题,不是所有鸭子都会飞。当然,你可以再不会飞的鸭子中把fly覆盖掉,并且方法体为空。但是以后你每加不会飞的鸭子,都必须重新覆盖一遍。
我们这里意识到:
当涉及“维护”时,为了复用而使用“继承”,结局并不完美。
利用接口如何?
注意我们上面的超类是有问题的,并不是所有的鸭子都会叫,而且quack方式不同。所以我们想到写2个接口。
Flyable { fly(){ } }
Quackable{ quack(){ } )
用接口是一个stupid的注意,想想看,这一来重复的代码比覆盖的方法还要多,对于48会飞的鸭子,必须实现接口类,如果要修改一下飞行的行为,这些都要改变。
软件开发的一个不变真理:Change
不管当初软件设计得多好,一段时间后,总是需要成长和改变。否则软件就会“死亡”。
把问题归零
现在我们意识到采用接口不是一个很好的办法,因为java接口不具有代码实现,所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,你必须往下追踪并在定义此行为的类中修改它,一不小心,可能会造成新的错误。幸运的是,有一个设计原则,恰好适用于此状况:
设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
即把变化的部分取出并“封装”起来,好让其他部分不受影响。使系统变得更有弹性。这个概念很简单,几乎是每个设计模式背后的精神所在,所以的模式都提供了一套方法让“系统中的某部分改变不会影响其他部分”。
现在来分开变化和不变的部分。为了分开,我们建立了2组类(完全远离Duck类),一个和fly相关,一个和quack相关。每一组类将实现各自的动作。
为了把这两个行为从Duck类分开,我们从Duck去取出来,建立一组新类来代表每个行为。
设计鸭子的行为
如何设计那组实现fly和quack的行为的类呢?
我们希望一切具有弹性。正因为一开始鸭子行为没有弹性,才让我们走上现在这条路,我们还想能够“指定”行为到鸭子的实例。换句话说,我们应该在鸭子类中包含设定行为的方法,这样,我们就可以在“运行时”动态地“改变”飞行行为。
设计原则: 针对接口编程,而不是针对实现编程。
我们利用接口代表每个行为,比方说,FlyBehavior与QuackBehavior,而行为的每个实现都放在其中的一个接口。
所以这次鸭子类不会复制实现Flying与Quacking接口,反而是由我们制造的一组其他类专门实现FlyBehavior与QuackBehavior,这称为“行为”类,由行为类而不是Duck类实现行为接口。
这样的做法不同于以往,以前的做法是:行为来自Duck超类的具体实现,或是继承某个接口并由子类自行实现而来,这两种做法都是依赖于“实现,我们被实现绑的死死的,没办法更改行为(除非写更多代码)。
从现在开始,鸭子的行为将被放在分开的类中,此类专门提供某行为接口的实现。这样,鸭子类就不在需要知道行为的实现细节了。
为什么非要把FlyBehavior设计成接口,而不是使用抽象超类,这样不就可以使用多态了吗?
针对”接口编程“真正的意思是”针对超类型supertype编程“;
这里的”接口“有多个函数,接口是一个”概念“,也是一种java的interface构造,你可以在不涉及java interface的情况下,”针对接口编程“。关键在于多态,利用多态,程序可以针对超类型编程,执行时会根据实际情况来执行真正的行为,不会被绑死在超类型的行为上。”针对超类型编程“这句话,可以更明确说成”变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量,这也意味着,声明类时不用理会以后执行时的真正对象类型。
举个简单例子,假设有一个抽象类Animal,有2个具体的实现Dog和Cat继承Animal,做法如下:
“针对实现编程”
Dog d=new Dog();
d.bark();
但是,针对接口/超类型编程做法如下:
Animal animal=new Dog();
animal.makeSound();
更好的是,子类实例化的动作不在需要在代码中硬编码,例如new Dog(),而是在运行时才指定具体实现的对象。
a=getAnimal();
a.makeSound(); 我们不知道实际子类型是什么, 我们只关心她知道如何正确地进行makesound就够了。
问题:用一个类代表一个行为,感觉是否有点奇怪?类不是应该代表某种“东西”吗?
在oo中,类代表的东西一般都是既有专题(实例变量)又有方法,不过在本例中,恰好“东西”是个“行为”,但是即使是行为,也可以由状态和方法,例如,飞行的行为可以有实例变量,记录飞行行为的属性等等。
整合鸭子的行为
关键在于,鸭子现在将fly和quack的动作“委托”delegate别人除了,而不是使用定义在Duck或子类的方法。
做法是这样的:首先在Duck中加入两个实例变量,flyBehavior与quackBehavior,声明为接口类型(而不是具体实现类型),
将原来在Duck类中的fly与quack换成performFly和performQuack。
package headfirst.strategy; public abstract class Duck { FlyBehavior flyBehavior; //实例变量在运行时持有特定的行为的引用,每只鸭子都是引用实现接口的对象 QuackBehavior quackBehavior; public Duck() { } abstract void display(); public void performFly() { flyBehavior.fly(); // } public void performQuack() { quackBehavior.quack();//鸭子不亲自处理quakc行为,而是委托给quackBehavior引用的对象 } public void swim() { System.out.println("All ducks float, even decoys!"); } }
接着,来设定flyBehavior与quackBehavior的实例变量
package headfirst.strategy; public class MallardDuck extends Duck { public MallardDuck() { quackBehavior = new Quack(); flyBehavior = new FlyWithWings(); } public void display() { System.out.println("I'm a real Mallard duck"); } }
当mallardDuck实例化时,构造器会把继承而来的实例变量quackBehavior与flyBehavior实例化。
FlyWithWings是FlyBehavior的具体实现类。
等等,不是说过我们将不对具体实现编程吗?但是我们的构造函数里做了什么,制造一个具体的Quack实现类的实例。
暂时我们是这么做的,在以后的学习中,有跟好的方法来修正这一点。仍请注意,我们把行为设定成具体的类,但是还是可以在运行时“轻易地”改变它。
所以,目前的做法还是很有弹性的,只是初始化实例变量的做法不够弹性罢了。
编写2个行为实行类FlyWithWings.java和FlyNoway.java
public interface FlyBehavior { public void fly(); }
public class FlyWithWings implements FlyBehavior { public void fly() { System.out.println("I'm flying!!"); } }
public class FlyNoWay implements FlyBehavior { public void fly() { System.out.println("I can't fly"); } }
最后编写测试类:
public class MiniDuckSimulator { public static void main(String[] args) { Duck mallard=new MallardDuck(); mallard.performFly(); mallard.performQuack(); } }
我们可以增加功能,能够动态设定行为:
在Duck增加设setter method 来设定实例变量:
public void setFlyBehavior (FlyBehavior fb) { flyBehavior = fb; } public void setQuackBehavior(QuackBehavior qb) { quackBehavior = qb; }
封装行为的大局观
我们设计了Duck类,飞行行为类实现FlyBehavior接口,呱呱叫行为类实现QuackBehavior接口。注意,我们描述事情的方式稍微改变,不再把鸭子的行为说成是“一组行为”,我们开始把行为想成是“一簇算法”,想想看,在我们设计中,算法代表鸭子能做的事(不提的叫法和飞行法)。
has-a 可能比is -a 更好
"has-a"关系非常有趣:每一组鸭子都有一个flyBehavior和一个QuackBehavior,好将fly和quack委托给他们代为处理。
当你将2个类结合起来使用,如同本例,这就是”组合“composition,这种做法和”继承“不同之处在于:鸭子的行为不是继承来的,而是和适当的行为对象”组合“而来的。
这是一个很重要的技巧,其实是使用了我们的第3个设计原则:
设计原则;多用组合,少用继承。
组合用在”许多“设计模式中。
上面我们用到的是”策略模式“,strategy pattern定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于是用算法的客户。