鸭鸭公司准备设计一款鸭子模拟程序,需要它能创建出各式各样的鸭子。下图我们给出的初版设计方案,很简单的继承方式。其中Duck是抽象类,因为绿头鸭和红头鸭叫声一样,所以quack是具体方法,两者都继承到了相同的叫声,swim同理;display代表外观,因为每种鸭子的外观都不一样,所以我们display现在是抽象类,子类都需要实现这个方法;

好吧,这设计方案看起来差强人意,他能满足红头鸭和绿头鸭的创建,并在实现类中实现了外观方法。但新需求接踵而至,客户想让这些鸭子能飞,我们顺理成章地在Duck父类中加入fly方法:

但是问题来了,我们的鸭子子类中也有橡皮鸭这样不会飞也不会嘎嘎叫的鸭,这时我们该如何实现这个类结构呢?现在我们姑且按下图一样在子类中将fly和quack方法覆盖掉:

但是利用继承来提供Duck子类的行为显然会出现下面几种问题:
问题1:行为覆盖导致的额外代码和可复用性降低:所有不能飞和不能嘎嘎叫的鸭子子类都需要覆盖fly和quack方法,用基本相同的逻辑重复实现它,这增加了额外代码并降低了可复用性。
问题2:运行时不容易改变行为。子类中覆盖的方法在运行时也没有修改的余地,这点与后文优化后的结构对比后能有一个直观的感受。
问题3:牵一发而动全身。继承后,父类所有的行为都会直接继承给子类,子类往往会继承到一些自己并不想要的方法(比如橡皮鸭继承到fly方法)。
了解了继承带来的问题后,我们尝试用接口来实现鸭子模拟器,这是我们接口设计的初版方案,让鸭子子类直接实现quackable和flyable接口对应的具体行为:

这样实现同样有几方面的问题:
问题1:所有实现了Quackable接口和Flyable接口的鸭子子类都需要重新实现对应方法,依然没有实现代码的复用。
问题2:同继承一样,运行时不容易改变行为。
遵循以下原则,我们将鸭子模拟器进行重新设计:
原则1:封装变化:将会变化的代码封装起来,好让其他部分不会受到影响。
这样我们需要将行为类单独提取出来。
原则2:针对接口编程,不针对实现编程。
我们想要鸭子实例的行为在运行时能动态的改变,所以我们将FlyBehavior和QuackBehavior接口实现的任务交给了行为实现类,把鸭子的子类从这个职责中解放出来,并在鸭子类中单独开辟属性来存放行为实现类,这样就能在运行时动态的改变鸭子实例的行为。

这样的设计,可以让飞行和呱呱叫的动作被其他对象复用,因为这些行为已经与鸭子类无关了。而且我们可以增加一些行为类,也不会影响到使用飞行行为类的鸭子子类。

当然,为了实现运行中动态改变鸭子的行为,我们需要一个方法来动态设定具体的行为类。

原则3:多用组合,少用继承。
上面的设计方案我们将行为从由Duck类继承的方式过度到了在Duck类中组合实现了行为接口的具体类,这样的好处是让鸭子实例在运行时也能动态改变行为,增加了代码的弹性。
最终设计方案中,我们不再把鸭子的行为说成是“一组行为”,而是把行为想成是“一组算法”,从而让其能在运行时相互切换。
策略模式:定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
发表回复