策略模式的定义
先来看看什么是策略模式:
定义和封装一个算法家族,并使这个家族种的各个算法可以相互替换,以使得算法可以独立于使用它的客户端而演变。UML如下所示。
这里的算法属于同一个算法家族,比如都是一组寻路算法,但是策略各有不同,有A-star,有简单寻路等等。
需求实例及初步设计
现在有一组需求,设计和展现坦克、榴弹炮、自行火炮等武器。它们都是装甲车辆,具有很多相同点,很自然我们会设计一个装甲车基类,并让坦克等都派生于此。于是类的设计将是如下的形式:
class ArmoredVehicle{};
class Tank : public ArmoredVehicle{};
class Cannon : public ArmoredVehicle{};
class SPG : public ArmoredVehicle{};
下面还有很多工作要做。
1 首先来考察一下装甲车的基本构件,简单来讲它包含车身、炮台两大部件,于是使用组合是显而易见的事。
2装甲车还有锁定目标、攻击、被攻击后的反应等基本攻击和防御行为,这是它们的相同之处,但是对于坦克和自行火炮而言,它们的具体行为可能是不同的。这恰好符合OO设计的行为——是的,使用虚函数。
现在ArmoredVehilcle就是如下的表现形式。
- class ArmoredVehicle
- {
- public :
- virtual void LockTarget() = 0; // 锁定目标
- virtual void OnAttacked() = 0; // 对攻击的反应
- protected :
- Turret m_turret; // 炮台
- Body m_body; // 车身
- };
我们还可以为LockTarget和OnAttached加入缺省实现,当然这样做也有一定的优缺点,可以参见Effective C++之条款36。
到现在为止,这个设计可以工作的很好了,下面是这个设计的UML简化图。
应对新的需求
现在新的需求来了,我们需要加入自行防空炮和火箭炮两类新型武器,而且可以预见将来可能还会支持其它的新型武器。它们都属于装甲车辆的一种。
然而每加入一种新型的装甲车,都必须实现LockTarget和OnAttached方法;而且实际上火箭炮和加农炮的LockTarget方法可能是相 同的,可能还有其它的装甲车共享同样的LockTarget或者OnAttached方法。这样将无法实现代码复用的目标。
为了提高智能性,还准备引入升级机制,比如可以把锁定目标的方法从低级提升为高级,以使得锁定更加迅速、准确。
现在的设计来实现这一需求时,不可避免的要使用if else语句根据条件来判断使用哪种策略了。这将使得后续的维护性和修改性变得更加困难。
显然现在的设计不能算是完美的,需要改进。
改进的设计
在ArmoredVehicle的初步设计中,我们能很容易想到将Body和Turret作为ArmoredVehicle的组件使用组合模式来设计。策略模式拓展了这一思维,它将算法也封装起来,使用组合模式。
以Lock Target为例,根据策略模式,我们将Lock Target封装成一组算法。并引入新类ILockTarget作为所有具体Lock Target算法的基类。类的设计将如下所示。
- class ILockTarget
- {
- public :
- virtual void LockTarget() = 0;
- };
- Class ConcreteLockTargetMethodA : public ILockTarget // 具体实现类
- {
- …
- };
同时需要修改装甲车类,使用组合模式,将Lock Target算法作为一个成员变量。修改后的ArmoredVehicle类如下所示(省略了On Attacked部分):
- class ArmoredVehicle
- {
- public :
- inline void SetLockTarget(ILockTarget *lt) {m_lockTarget = lt;}
- inline void LockTarget() {m_lockTarget->LockTarget();}
- protected :
- Turret m_turret; // 炮台
- Body m_body; // 车身
- ILockTarget *m_lockTarget; // lock target算法
- };
改进设计的UML图将如下所示:
该设计将Lock Target算法独立出去,这样也就为Lock Target算法的独立演化创造了基础。
如果新加入的装甲车使用新的LockTarget或OnAttacked方法,只需要从ILockTarget (OnAttacked)方法中派生出新的实现类即可,而不必修改现有的实现。
如果两个装甲车类有相同的LockTarget策略,它们只需要使用LockTarget的同一个实现类即可,这也达到了代码复用的目标。
装甲车只需要调用SetLockTarget,就可以使用新的Lock Target算法了,而不必对代码做任何修改。
当然采用策略模式后,将会增加项目中的类的数量,这算是策略模式的缺点之一。而且事情并不是完美的,比如在实际使用中,算法之间在参数或者返回结果上可能会有细微差别,在这种情况下,就要做些额外的工作了。
策略模式和工厂模式经常会组合使用,比如在本例中创建不同的LockTarget算法,就可以使用工厂模式封装起来。
设计思维
策略模式也告诉了我们如下的设计思维。
组合VS继承 ,优先使用组合,然后才考虑继承;
封装变化的部分 ,将可变部分可能引起的问题局限在最小范围;
事实上这在很多设计模式中都有体现,比如工厂模式,封装了新对象的创建;比如桥式模式,封装了具体实现;这些都是对变化部分的封装。