第2章 策略模式

2.3 简单工厂实现

面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象具有相同属性和功能的对象的抽象集合才是类。打一折和打九折只是形式的不同,抽象分析出来,所有的打折算法都是一样的,所以打折算法应该是一个类。

//现金收费抽象类(纯虚类)
class CashSuper{
public:
 //现金收取超类的抽象方法,收取现金,参数为原价,返回(值)为当前价
 virtual double acceptCash(double money) = 0;
};

//正常收费子类
class CashNormal : public CashSuper{
public:
 //正常收费,原价返回
 double acceptCash(double money){
  return money;
 }
};

//打折收费子类
class CashRebate : public CashSuper{
public:
 //打折收费,通过构造函数初始化折扣率(在初始化时输入折扣率),如八折,就是0.8
 CashRebate(double moneyRebate){
  this->moneyRebate = moneyRebate;
 }
 
 double acceptCash(double money){
  return money * moneyRebate;
 }
 
private:
 double moneyRebate;
};

//返利收费子类
class CashReturn : public CashSuper{
public:
 //返利收费,初始化时输入返利条件和返利值,
 //比如满300返100,moneyCondition就是300,返利值就是100
 CashReturn(double moneyCondition, double moneyReturn){
  this->moneyCondition = moneyCondition;
  this->moneyReturn = moneyReturn;
 }
 
 double acceptCash(double money){
  double result = money;
  if(money >= moneyCondition){
   result = money - ((int)(moey/moneyCondition)) * moneyReturn;
  }
  
  return result;
 }
private:
 double moneyCondition;
 double moneyReturn;
};

//现金收费工厂类
class CashFactory{
public:
 //现金收费工厂
 static CashSuper* createCashAccept(string type){
    CashSuper* cs = NULL;
  //根据条件返回相应的对象
  if(type=="正常收费"){
   cs = new CashNormal;
  }else if(type=="满300返100"){
   cs = new CashReturn(300,100);
  }else if("打8折"){
   cs = new CashRebate(0.8);
  }
  return cs;
 }
};

工厂中创建对象的方法可使用if else分支来创建不同的对象,也可使用switch case分支来创建不同的对象。
if else和switch case的优缺点总结参考博客:https://www.cnblogs.com/zxrbky/p/9466649.html
关于两者的优缺点如下:
1.if else的优缺点
缺点:效率低(相对于switch)
优点:使用灵活,代码空间小
2.switch的优缺点
缺点:不够灵活,switch仅支持 int,char,unsigned char 等基本类型;代码空间大。
优点:代码结构清晰,效率高
其实,对于if else 作为关键字而言,效率还是可以的。

//客户端程序
void onBtnClicked(){
 //获取选择的收费方法字符串
 string acceptType = ...;
 CashSuper* csuper = CashFactory::createCashAccept(acceptType);
 //获取商品数量和单价,计算得到正常收费金额
 double money = 单价 * 数量;
 double result = csuper->acceptCash(money);
}

现在若需要增加一种商场促销手段,则只需要添加一个类,让它继承CashSuper,再在收费对象生成工厂中添加一个分支条件即可。
商场是可能经常性地更改打折额度和返利额度,以及促销模式,每次维护或扩展收费方式都要改动这个工厂,以致代码需重新编译部署,所以简单工厂有它的弊端。

2.4 策略模式

定义:
策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变换,不会影响到使用算法的客户。
商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成算法对象,这没有错,但算法本身只是一种策略,最重要的是这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式

//Strategy类,定义所有支持的算法的公共接口
//抽象算法类
class Strategy{
public:
 //算法方法
 virtual void AlgorithmInterface();
};
//ConcreteStrategy,封装了具体的算法或行为,继承于Strategy
//具体算法A
class ConcreteStrategyA : public Strategy{
public:
 //算法A实现方法
 void AlgorithmInterface(){
  std::cout<<"算法A实现"<<std::endl;
 }
};
//具体算法B
class ConcreteStrategyB : public Strategy{
public:
 //算法A实现方法
 void AlgorithmInterface(){
  std::cout<<"算法B实现"<<std::endl;
 }
};
//具体算法C
class ConcreteStrategyC : public Strategy{
public:
 //算法C实现方法
 void AlgorithmInterface(){
  std::cout<<"算法C实现"<<std::endl;
 }
};

//Context,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用
//上下文
class Context{
public:
 //初始化时,传入具体的策略模式
 Context(Strategy* strategy){
  this->strategy = strategy;
 }
 
 //上下文接口,根据具体的策略对象,调用算法的方法
 void ContextInterface(){
  strategy->AlgorithmInterface();
 }
};

//客户端代码
int main(){
 //由于实例化不同的策略,所以最终在调用context->ContextInterface();时,所获得的结构就不尽相同
 Context* context = new Context(new ConcreteStrategyA());
 context->ContextInterface();
 
 Context* context = new Context(new ConcreteStrategyB());
 context->ContextInterface();
 
 Context* context = new Context(new ConcreteStrategyC());
 context->ContextInterface();
 return 1;
}

2.5 商场收银系统的策略模式实现

//添加一个CashContext类,并改写一下客户端
class CashContext{
public:
 //通过构造方法,传入具体的收费策略
 CashContext(CashSuper* csuper){
  this->cs = csuper;
 }
 double GetResult(double money){
  return cs->acceptCash(money);
 }
private:
 CashSuper* cs;
};
//客户端程序
void onBtnClicked(){
 CashContext* cc = NULL;
 //获取选择的收费方法字符串
 string acceptType = ...;
 if(type=="正常收费"){
  cc = new CashContext(new CashNormal());
 }else if(type=="满300返100"){
  cc = new CashContext(CashReturn(300,100));
 }else if("打8折"){
  cc = new CashContext(CashRebate(0.8));
 }
 
 double result = cc->GetResult(money);
}

这样用,把判断的过程从简单工厂中移走,放在了客户端,客户端需要知道策略的类代码。
自己总结:策略模式的上下文其实就是在上下文类中添加引用策略类方法的方法(GetResult),同时把判断过程移出到客户端,策略类对象作为参数传入到上下文中。

2.6 策略与简单工厂结合

简单工厂就一定要是一个单独的类吗?难道不可以与策略模式的Context结合吗?

//改造后的Context
class CashContext(){
public:
 //注意不是具体的收费策略对象,而是一个字符串,表示收费类型
 CashContext(string type){
  //将实例化具体策略的过程由客户端转移到Context类中。简单工厂的应用。
  if(type=="正常收费"){
   cs = new CashNormal;
  }else if(type=="满300返100"){
   cs = new CashReturn(300,100);
  }else if("打8折"){
   cs = new CashRebate(0.8);
  }
 }
private:
 //声明一个CashSuper对象
 CashSuper* cs;
};
//客户端程序
void onBtnClicked(){
 CashContext* cc = NULL;
 //获取选择的收费方法字符串
 string acceptType = ...;
 CashContext* cc = new CashContext(acceptType);
 
 double result = cc->GetResult(money);
}

简单工厂模式并非只有建一个工厂类的做法,这样做比策略模式的写法要清楚多了,客户端代码简单明了。

//简单工厂客户端与策略与简单工厂结合的对比
//简单工厂
CashSuper csuper = CashFactory::createCashAccept(acceptType);
double result = csuper->GetResult(...);
//简单工厂与策略结合
CashContext* csuper = new CashContext(acceptType);
double result = csuper->GetResult(...);

使用简单工厂客户端需要知道两个类,CashSuper和CashFactory,而策略与简单工厂结合的方法,客户端只需要认识一个CashContext类就可以了耦合更加降低
客户端实例化CashContext的对象,调用的是CashContext的方法GetResult,使得具体的收费算法彻底地与客户端分离,算法的父类CashSuper都不让客户端认识

2.7 策略模式解析

策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类(各种算法类)之间的耦合

优点:
1.策略模式的Strategy类层次(算法顶层抽象类,提供接口函数的类)为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。通过继承,可以得到它们的公共功能(公共的函数接口,如获取计算结果的GetResult,这使得算法间有了抽象的父类CashSuper)。
2.另外一个策略模式的优点是简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。(可测试每个具体算法类的实现的父类的接口函数,也可以单独测试具体算法类中独有的各个函数)。
在最开始编程时(面向过程编程时),不得不在客户端的代码中为了判断用哪一个算法计算而用了switch条件分支或if判断(不同的条件处理不同)。因为,当不同的行为(实现)堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些类封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句(将判断交给简单工厂)。
3.策略模式封装了变化,策略模式就是用来封装算法的,但在实践中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
基本的策略模式中,选择所用具体实现(创建具体的策略类)的职责由客户端承担,并转给(传递给)策略模式的下上文Context对象,这本身并没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现(创建具体策略类ConcreteStrategy)的职责可以由Context来承担,这就很大化地减轻了客户端的职责
任何需求的变更都是需要成本的,面对同样的需求,改动越小越好

2.8 策略模式与简单工厂结合的实际项目应用

前提背景:
软件工程在启动时要连接数据库,第一版的实现是针对具体的数据库类型MySql做的数据库连接bool Sjfw_LinkDatasource();

class CDataSource{
public:
 //连接关系库
 bool Sjfw_LinkDatasource(iniFilePath){
  //解读ini配置文件,获取连接oracle需要的连接参数,然后做连接操作
  //连接oracle数据库
  ...
 }
 //连接空间库
 bool Sjfw_LinkSdeDatasource(iniFilePath){
  //通过解读ini配置文件获取连接空间库的连接参数
  //连接oracle空间库
  ...
 }
};

这样使用的话,当软件工程需要适配MySql、KingBase、DM等其他类型数据库时,则需要修改Sjfw_LinkDatasource、Sjfw_LinkSdeDatasource两个函数(或者添加连接其他类型数据库的新连接函数),这时需要修改Sjfw_LinkXXXDatasource、Sjfw_LinkXXXSdeDatasource,对修改开放了。
由于连接不同类型数据库需要的连接参数不同,连接方法有差异,所以考虑使用策略模式,将不同的地方(连接参数和连接方法)封装到具体的子类中,上层提供连接数据库的接口函数。

//接口类
class IDBLinker{
public:
 //连接关系库
 bool Sjfw_LinkDatasource(iniFilePath) = 0;
 //连接空间库
 bool Sjfw_LinkSdeDatasource(iniFilePath) = 0;
};

//连接具体类型数据库的操作类,如连接oracle
/**
* 连接oracle数据库
*/
class COracleLinker : public IDBLinker
{
public:
 COracleLinker();
 virtual ~COracleLinker();
    
public:
 bool Sjfw_LinkDatasource(iniFilePath){
  ReadIni(...);
  //连接oracle关系库
  ...
 }
 bool Sjfw_LinkSdeDatasource(iniFilePath){
  ReadIni(...);
  //连接oracle空间库
  ...
 }
private:
 bool ReadIni(const QString& iniFilePath){
  //解读ini配置文件获取连接参数
  ...
 }

private:
 QString m_ip;
 QString m_port;
 QString m_dbname;
 QString m_user;
 QString m_pswd;
 };

/**
 * 数据库连接操作上下文类
 */
class CDBLinkerContext
{
public:
 CDBLinkerContext(const QString& dbType){
  m_pDBLinker = NULL;
  if (dbType=="Oracle")
  {
   m_pDBLinker = new COracleLinker;
  }else if (dbType=="MySql")
  {
   //...
  }else if (dbType=="DM")
  {
   //...
  }
 }
 virtual ~CDBLinkerContext(){
  if (m_pDBLinker!=NULL)
  {
   delete m_pDBLinker;
   m_pDBLinker = NULL;
  }
 }
 //连接关系库
 bool Sjfw_LinkDatasource(iniFilePath){
  if (m_pDBLinker==NULL)
  {
   return false;
  }
  return m_pDBLinker->Sjfw_LinkDatasource(iniFilePath);
 }
 //连接空间库
 bool Sjfw_LinkSdeDatasource(iniFilePath){
  if (m_pDBLinker==NULL)
  {
   return false;
  }
  return m_pDBLinker->Sjfw_LinkSdeDatasource(iniFilePath);
 }
private:
 IDBLinker* m_pDBLinker;
};

//客户端应用
bool ConnectDatasource()
{
 QString iniFilePath = "...";
 QSettings settings(iniFilePath, QSettings::IniFormat);
 QString sDBType = settings.value("DBType/Type").toString();
 /*if (sDBType=="Oracle")
 {
  QString sIP = settings.value("Oracle/IP").toString();
  QString sPort = settings.value("Oracle/Port").toString();
  QString sDatabaseName = settings.value("Oracle/Database").toString();
  QString sUsername = settings.value("Oracle/Username").toString();
  QString sPassword = settings.value("Oracle/Password").toString();
  //连接关系库、空间库
  return;
 }*/
 CDBLinkerContext dbLinkerContext(sDBType);
 if (!dbLinkerContext.Sjfw_LinkDatasource( filePath))
  return;
 if (!dbLinkerContext.Sjfw_LinkSdeDatasource( filePath))
  return;
 return;
}

这时客户端只需要知道CDBLinkerContext就可以,客户端与连接具体类型数据库的实体类解耦;若需要适配新类型数据库,只需要添加一个连接具体类型数据库的实体连接类,并且修改根据类型创建实体类对象代码即可,达到了容易扩展的效果,同时也达到了容易维护的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值