本文讲述了组件协作模式中的策略模式,介绍了策略模式的动机、定义、结构、代码实例,最后进行了总结。
再次感谢GeekBand的李建忠老师、GOF等前辈
1. “组件协作”模式:
- 现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。
- 典型模式
- Template Method
- Strategy 策略模式
- Observer / Event
2. 动机(Motivation)
- 在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担[稍后解释]。
- 如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
3. 模型代码
代码strategy1.cpp
展示的是采用结构化软件设计方法实现的“税收计算”,首先使用枚举TaxBase
来标记不同地区,在SalesOrder
类的CalculateTax()
中通过判断标记类型来使用不同国家的税收计算方法。
// strategy1.cpp
enum TaxBase {
CN_Tax,
US_Tax,
DE_Tax,
FR_Tax //更改
};
class SalesOrder{
TaxBase tax;
public:
double CalculateTax(){
//...
if (tax == CN_Tax){
//CN***********
}
else if (tax == US_Tax){
//US***********
}
else if (tax == DE_Tax){
//DE***********
}
else if (tax == FR_Tax){ //更改
//...
}
//....
}
};
使用设计模式的目的就是为了隔离变化,因此首先要找到代码变化点,为此我们心中要有一个时间轴,不要静态的看待一个代码,而要考虑代码在未来可能发生的变化,或者新增需求,从而找到变化点,在这个地方使用设计模式才会起到效果。比如现在有新需求要添加其它国家税收计算方法,如果在strategy1.cpp
代码上继续扩展业务,则需要修改枚举类型,在CalculateTax()
中也要增加相应的判断条件。
上述的修改方法明显违背了我们前边所说的“开放封闭原则”[对扩展开放,对修改关闭,即类模块应该尽可能的使用扩展的方法来实现未来的变化,而不是直接修改编译好的代码]。
代码strategy2.cpp
展现的是我们使用面向对象软件设计方法实现的税务计算,通过观察发现strategy1.cpp
中造成代码变化的点就是“不同的税收计算策略”,因此定义了一个TaxStrategy
作为父类,为税收算法定义抽象接口Calculate()
,这样我们通过继承、重写虚函数不仅可以实现代码扩展,而且也隔离了变化,比如定义CNTax
、USTax
、DETax
、FRTax
分别表示不同国家税收计算方法。
SalesOrder
中使用策略比较灵活,该类中聚合一个TaxStrategy
对象指针[为了保证多态,必须使用指针或者引用,但是引用可能产生其它问题,没有指针方便]来保证调用正确的税务计算方案,另外我们在代码中还嵌入了工厂模式,使得我们在调用时不用直接new
对象,只需要传入合适的参数便可以创建指定类型的堆对象[关于工厂模式的使用之后详细说明]。
// strategy2.cpp
class TaxStrategy{
public:
virtual double Calculate(const Context& context)=0;
virtual ~TaxStrategy(){}
};
class CNTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class USTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class DETax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
//扩展
//*********************************
class FRTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//.........
}
};
class SalesOrder{
private:
TaxStrategy* strategy;
public:
// 也可以将工厂嵌入类内部,通过不同的参数类型来创建对象,这样在客户端我们也不用知道有工厂类存在
SalesOrder(StrategyFactory* strategyFactory){
this->strategy = strategyFactory->NewStrategy(); // 简单工厂
}
~SalesOrder(){
delete this->strategy;
}
public double CalculateTax(){
//...
Context context();
double val =
strategy->Calculate(context); //多态调用
//...
}
};
4. 模式定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(变化)。该模式使得算法[不同国家的税务计算方法]可独立于使用它的客户程序(稳定)[税务计算功能调用点]而变化(扩展,子类化)。
5. 结构
结合Template Method一文中提到的理解设计模式的方法,我们需要知道那些部分是变化的,那些部分是稳定的。在Strategy模式类图中,红色部分是相同算法的不同实现版本,可以相互替换,而绿色部分是稳定的,不会随实现版本的变更而发生任何变化。
6. 要点总结
- Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。
- Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合[注意当if-else/switch-case在保证绝对不变的情况下才会继续保持,否则我们会闻到代码的bad small,想一想此时是否可以使用strategy模式代替吧]。含有许多条件判断语句的代码通常都需要Strategy模式。
- 上面动机我们提到了Strategy模式的使用有两个优势,第一个就是算法的切换不会影响客户端,另一个就是可以提高性能,因为如果使用if-else等结构化的设计方法,我们会将大量不会使用的代码加载到高速缓存当中,这就导致效率的下降,因此要保证代码的本地化,使用策略模式可以保证只会加载相应的处理代码进缓存。
- 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。