模拟鸭子游戏的需求
SimUDuck
游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫。
通过标准的OO技术,设计一个超类。
需求增加
此程序需要会飞 的鸭子竞争者抛在后头
方法一:在 Duck 超类加上 fly() 方法
但问题发生了:并非 Duck 所有的子类都会飞(玩具鸭子)
注意:当涉及到维护是,为了“复用”(reuse)目的而使用继承,结局并不完美
方法二:覆盖超类的方法
当有新的鸭子出现,要检查每个鸭子可能要覆盖 fly() 和 quark() ...
- 利用继承来提供行为,导致的缺点
- 代码在多个子类中重复
- 运行时的行为不容易改变
- 很难知道所有鸭子的全部行为
- 改变引发全身,造成其他鸭子不想要的改变
方法三:利用接口
可以把 fly() 从超类中提取出来,放进一个“Flyable接口”中。这么一来,只有会飞的鸭子才实现此接口。
看似不错(只适应用不多,且飞的方式不同的情况),但是在会飞的鸭子种类很多后,要稍微修改一下飞行的行为,就是一个灾难...
分析
- 不是所有的子类都具有飞行和呱呱叫的行为,所以继承不合适。
- 虽然 Flyable 和 Quackable 可以解决“一部分”问题,但是却造成代码无法复用,这只是从一个坑进入另一个坑。
- 甚至,在会飞的鸭子中,飞行动作可能还有多种变化。。。
解决之道--“采用良好的OO软件设计原则”
软件开发的一个不变的真理 --> 需求和改变
第一个设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起
把会变化的部分取出来并“封装”起来,好让其它部分不会受到影响
为了把这两个行为从Duck 类中分开,将它们从 Duck 类中取出来,建立一组新类来代表每个行为。
设计鸭子的行为
让鸭子类中的行为可以动态的改变就好了。我们应该在鸭子类中包含设定行为的方法,这样就可以在“运行时”动态的“改变”飞行行为。
第二个设计原则:针对接口编程,而不是针对实现编程
针对超类型编程
利用接口代表每个行为,比方说 FiyBehavior 和 QuackBehavior ,而行为的每个实现都将实现其中的一个接口。由行为类而不是Duck类来实现接口。
- 以前的做法:行为来自 Duck 超类的具体实现,或是继承某个接口并由子类自行实现而来。都依赖于“实现”,被实现绑的死死的,没办法更改行为(除非写更多代码)。
vs
- 新设计中:鸭子的子类将使用接口(FlyBehavior 与 QuackBehavior)所标示的行为,实际的“实现”不会被绑死在鸭子的子类中。即特定的具体行为编写在实现了FlyBehavior 与 QuackBehavior 的子类中。
这样的设计,可以放飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关了。
而我们可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类。
整合鸭子的行为
关键在于:鸭子现在会将飞行和呱呱叫的动作“委托”(delegate)别人处理,而不是使用定义在 Duck 类(或子类)内的呱呱叫和飞行方法。
做法:
- 首先,在 Duck 类中加入“两个实例变量”,分别为“flyBehavior”与“quackBehavior”,声明为接口类型(而不是具体类实现类型),每个鸭子的对象都会动态地设置这些变量以在运行时引用正确的行为类型。
- 实现 performQuack();
public class Duck{
QuackBehavior quackBehavior;
// 还有更多
public void performQuack(){
quackBehavior.quack();//鸭子对象不亲自处理呱呱叫行为,而是委托给quackBehavior引用的对象。
}
}
- 如何设定 flyBehavior 与 quackBehavior的实例变量
public class MallardDuck extends Duck{
public MallardDuck(){
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
//别忘了,因为 MallarDuck 继承了 Duck 类,所以具有 flyBehavior 与 quackBehavior 实例变量
}
我们正在构造器里面制造了具体的Quack实现类的实例(在后面的模式中,可以修正这一点)
动态设定行为
通过 set 方法来设定鸭子的行为,而不是在鸭子的构造器内实例化。
Duck 类中 加入 setFlyBehavior(FlyBehavior fly);