属于类别
- 组件协作模式
引言
假设一个场景:计算税额,每个国家的税额计算方法可能不同,这个由该国家的税法决定。
目前我们要支持中国、美国、德国三个国家的税务计算。分解的思维
将不同类别分而治之:用枚举类型进行分类,再根据类别在代码中分流。 ```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){
// //...
//}
//....
}
};
【思考】如此看起来很像没有什么问题,这个做法也无可厚非,但这只是在静态的条件下思考,如果静态的来思考,很多问题是暴露不出来的。<br />接触软件设计以后,我们应该更多的在动态的角度来审视一个软件设计,也就是考虑到未来的变化。
【变化】如果以后要支持新的国家的税法,如法国,那应该怎么做?<br />新增一个类别,并增加分支即可。
【存在问题】
1. 设计原则方面:违背“开放封闭”原则(对扩展开放,对更改封闭)。模块应该尽可能的用扩展的方式来支持未来的变化,而不是通过修改源代码的方式。
1. 最直接的影响:更改源代码后,这个模块要重新编译,重新测试,重新部署。
<a name="1bKmY"></a>
#### 抽象的思维
如果用抽象的思维来看待这个问题:
```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 SalesOrder{
private:
TaxStrategy* strategy; //基态指针
public:
SalesOrder(StrategyFactory* strategyFactory){
//推荐使用工厂模式来创建TaxStrategy
//外部传入一个工厂,由这个工厂决定它创建的具体类型
//如果外部传入的工厂是一个CNTaxStrategyFactory,则它创建的具体类就是CNTax
this->strategy = strategyFactory->NewStrategy();
}
~SalesOrder(){
delete this->strategy;
}
public double CalculateTax(){
//构造一个计算税法的上下文
Context context();
double val = strategy->Calculate(context); //多态调用
//...
}
};
说明:以上每一个类都在一个.h、.cpp中。写完之后,对以上内容进行编译、测试、部署。
【变化】假设未来要支持法国的税额计算,那我们只需在写一个FRTax,然后将FRTaxStrategyFactory传入即可
//未来要支持法国的税额计算
class FRTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//.........
}
};
【优势】
- 不需要重新编译之前的代码,只需要编译FRTax、FRTaxStrategyFactory。而且只需要测试这个类别的即可
- 达到了二进制文件层级的复用
- 满足了开闭原则
【复用】面向对象、设计模式中所说的复用性,并不是说源代码级别的复用,而是编译单位、二进制层面的复用性。
- 粘贴代码,拷贝代码,这不能说是复用,只能说是复制,或者说在源代码级别的“复用”
- 真正的复用是,你的代码编译之后,测试之后,部署之后,原封不动原先的这些编译结果,就能完成新的需求,这个叫做复用。能够复用之前的编译结果(如dll)
- 其实,在一段代码里面,新增一个分支,在工程应用中,并不能保证其他地方不出错。
动机
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题?
模式定义
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换(就是支持变化)。该模式使得算法可独立使用它的客户程序(稳定)而变化(用扩展的方式,子类化的方式来支持这些变化)。——《设计模式》GOF。
结构
要点总结
- Stategy及其子类组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行转换。
- Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有很多条件判断语句的代码通常都需要Strategy模式。
- if-else是分而治之的思维,是可以用抽象思维来代替它的。看到if-else,就闻到了strategy设计模式的味道。
- 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。
- 比如你的软件只装在中国的话,你只需要中国的税法计算策略。那么你可能只需要一个Strategy对象。
- 如果是第一种思维模式,用分而治之的方式(if-else)写了很多种情况,但此时你的软件只装在中国,那很多多余的代码段就会被迫装载到CPU的高级缓存里,这样会大大的影响程序的性能。如果使用Strategy模式,就会缓解这个问题。