设计一个鸭子游戏

V1 描述

设计一个鸭子游戏,游戏中有很多鸭子,每个鸭子有一些基本的行为特征,例如:游泳、呱呱叫等等。

解决

抽象出一个鸭子父类,父类中有鸭子的基本行为,例如:swim() 游泳,quack() 呱呱叫。

也定义个抽象方法行为,让鸭子自己去实现各自的特征,例如:display() 鸭子的颜色。

image.png

V2 描述

现在在 v1 的基础上,有一个新需求,需要为鸭子们添加一个 fly 的行为动作,可以在父类上添加这个行为,这样所有的鸭子也都有这个行为了,但是鸭子中有些鸭子是不能飞的,例如:橡皮鸭。这样就违背需求了,应该怎么办?

image.png
(在父类添加 fly() 行为,子类不需要这个功能的鸭子都覆盖)

继承的目的是为了更好的复用,但是这也引入了新的一些问题:

  1. 牵一发而动全身,改动会变得很大,代码在子类重复性很大。例如:几十个鸭子都不要飞,难道都要去覆盖?
  2. 鸭子不能同时又飞又叫
  3. 难以得知鸭子的所有行为

继承解决不了新加入的需求,而且以后还可能会添加其他行为动作,例如:散步,生蛋。如何做到【易扩展】而不对已有的行为造成影响呢?

解决

接口,针对上述的 fly 功能,我们可以设计一个 Flyable 接口,只让那些需要飞的鸭子实现这个接口即可。
image.png

新的问题

设计 Flyable 接口,确实可以做到对已有的做到不侵入。但是会引入新的问题,如果有20个鸭子需要飞,那么这20个鸭子都要去实现这个接口?


思考—思考—思考—思考—思考—思考—思考—思考—思考—思考—思考—思考


找出应用可能会变化的部分,把他们独立出来,不要和不变化的部分混合在一起。把变化封装起来,而不影响其他代码。

V3 描述

通过前2个版本的设计,我们总结出鸭子游戏中,鸭子有些会变,有些不会变。我们要做的就是将会变化的部分剥离出来,然后将他封装起来。做到不干扰,易扩展的目的。
image.png

解决

将行为封装为接口,在让具体的行为类去实现该行为接口,在 Duck 类中定义行为变量接口,让具体的行为在代码执行时再确定
image.png
代码如下:

  1. public abstract class Duck {
  2. private FlyBehavior flyBehavior;
  3. public void swim() {
  4. System.out.println("duck swim ...");
  5. }
  6. public abstract void display();
  7. public void fly() {
  8. flyBehavior.fly();
  9. }
  10. public void setFlyBehavior(FlyBehavior flyBehavior) {
  11. this.flyBehavior = flyBehavior;
  12. }
  13. }
  14. // Test.java
  15. public static void main(String[] args) {
  16. Duck mallardDuck = new MallardDuck();
  17. mallardDuck.setFlyBehavior(new FlyWithWings());
  18. // 输出 use wings fly ...
  19. mallardDuck.fly();
  20. }

上述的这个解决方案,从代码复杂度和代码量来看反而变的更复杂了。会产生这样的疑问:我仅仅是想加个 fly 功能,有必要这么麻烦吗?
**
日常开发中,常常面临这样的问题,是一劳永逸,还是缝缝补补?但在开发过程中发现这些都不是问题,问题是你敢不敢的问题,敢不敢重构?如何保证重构后的质量?

总结

再看这张图,为了在原来的功能中添加一个 fly 的功能,我们将代码扩展出了以下样子:

  1. 将行为抽出一个接口,例如这里的 fly 行为,抽出了 FlyBehavior 接口
  2. 再让具体的行为去实现这个接口,例如这里的 FlyWithWings 类
  3. 在具体的鸭子类中如果该鸭子需要什么行为可以动态的绑定,而不是硬编码,同时如果需要添加新的飞行行为只要实现 FlyBehavior 接口就行。完美的利用抽象、多态、封装、继承这几个面向对象思想
  4. 最后,这里面用到的是策略模式,但是这并不重要,循序渐进,希望能体会到整个进化的思想,感受到面向对象思想的强大,在设计中遵循一些设计原则

image.png

附录