Head First 设计模式之第一章——策略模式

内容回顾

第一章中,主要讲述了鸭子模拟器的实现,具体需求如下:

要实现一个鸭子模拟器来模拟各种鸭子的行为,如叫、游泳等等的行为。

针对这个需求,平常稍为有点OO思想的人,首先想到的方案就是:先设计一个鸭子的基类,在基类中设计若干个函数来表示鸭子的行为,如叫、游泳等等,然后用这个基类派生出若干个子类,每个子类代表一种鸭子,如唐老鸭、周黑鸭、北京烤鸭、橡皮小黄鸭等等,在这些基类中分别重载这些行为函数即可,那样在使用时,直接创建各个子类的实例,然后直接调用各自的行为函数,那样各个实例都会表示出各自应有的行为。
Head First 设计模式之第一章——策略模式

这样的设计一眼看上去,感觉挺不错的,充分应用了OO中的继承与多态的思想。但是这种方案是有一定的缺陷的,因为这种方案不够的设计灵活,甚至会给开发者带来麻烦。如果后面程序要进一步升级,要给鸭子添加飞行的行为,那如何是好呢?此时有人就会说,那不简单,直接有基类中添加一个fly的函数来代表鸭子的飞行行为,然后在子类中分别实现fly()这个函数即可。

这个主意看起来不错,但实际上这是一个馊主意,因为这个方案有如下若干个缺点:

  1. 不是所有的鸭子都会飞的,如唐老鸭、周黑鸭、北京烤鸭是会飞的,但橡皮小黄鸭不会飞, 当然在实现时,将这橡皮小黄鸭所对应的fly()函数实现为不会飞即可;
  2. 有一些鸭子的飞行方式是相同的,如周黑鸭、北京烤鸭都是低空飞行,那实现时,在周黑鸭与北京烤鸭所对应的fly函数中分别写相同的实现(即低空飞行),那样的代码实在是有点丑陋,也不利于代码复用;
  3. 如果想在运行时改变鸭子的飞行行为,例如橡皮鸭子起初是不会飞的,后来与火箭绑定后,就可以进行极速飞行,那在这个框架下,这种需要基本是很难实现的。

鉴于此,这种设计方案是十分不可取的。在OO设计中,我们整天强调封装,其实在封装时,需要对易变的、不变的东西分别进行封装,不变的东西封装成一个类,里面的东西基本保持不变,而变的东西则封装成一个接口,如果东西变了,则直接针对该接口进行额外的实现即可。这就遵循了OO中的一个原则:面向接口编程而不是面向实现编程。例如针对上述的鸭子模拟器的例子,可以将鸭子的基本不变的东西封装成一个类,如鸭子的毛色、品种名称等等,这个类很大概率是不用修改的,而将鸭子的行为(如飞行、叫声)这个易变的东西封装成另一个接口,例如叫行为接口(IBehavious) ,针对不同的行为,对该接口进行不同的实现,具体的鸭子如果需要使用哪种行为,则通过该接口将具体的行为传递给该鸭子类的实例。那样就可以实现如下目的:

  1. 鸭子类基本可以保持不变;
  2. 鸭子子类如果想拥有不同的行为,那只需将对应的行为通过接口传到类实体处即可;
  3. 实现动态调整实例的行为;
  4. 不同的鸭子子类如果具有相同的行为,则只需将相同的行为子类传进去即可,而无须在每个子类中重复实现。

上述的设计就是所谓的策略模式,每种行为相当于一种策略,而鸭子这个实例想要不同的行为,那就采用不同的策略。最终的实现如下图所示:
Head First 设计模式之第一章——策略模式

举一反三

场景

假设要编写一个CS(反恐精英)游戏,游戏中的角色有警察与*,他们使用武器进行战斗,而武器则多种多样,如手枪、步枪、匕首等等。

实现方案

采用策略模式来进行设计。因为武器是多种多样的,是变化的,而角色的其他特性相对不变,所以根据OO的原则,需要对变化进行封装,所以在方案中,角色与武器分别进行封装。角色使用武器,武器相当于被角色使用的一种策略。而这种策略是千变万化的。所以使用策略模式来实现上述功能,方案的如下图所示。
Head First 设计模式之第一章——策略模式

从上图可以看到,Police与Terrorist都是派生于IActor这个接口,而IAcotor与IWeapon是聚合关系,即has-a,一个IActor可以拥有多个武器,当IActor的子类调用fight()方法时,即可以使用具体的Weapon进行战斗。各种具体的武器均是对IWeapon接口的实现,这样的好处是,当有新的武器产生时,可以直接新建一个类来实现IWeapon,并将该新Weapon的对象传入Police或Terrorist对象即可,而不用修改原有的类,这样符合OO中的开放-封闭原则。

总结:

  1. 策略模式的精髓是:面向接口编程而不是面向实现编程,将一些经常发生的变化封装成接口,如果变化了,那只需要根据变化来对接口写一个新的实现即可,而不用修改原来的代码。
  2. has-a 优于is-a,即组合优于继承。继承的话,如果基类修改了,那么子类很可能通通都要变, 这是一个十分糟糕的设计,而组合的话,只修改对应的组成成为即可,将变化的影响限制到很少的范围内。