大话设计模式解读02-策略模式

2024-06-10 20:28

本文主要是介绍大话设计模式解读02-策略模式,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本篇文章,来解读《大话设计模式》的第2章——策略模式。并通过Qt和C++代码实现实例代码的功能。

1 策略模式

策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法

策略模式的特点:

  • 定义了一组算法(业务规则)
  • 封装了每个算法
  • 这一类的算法可互换代替

策略模式的组成:

  • 抽象策略角色(策略类): 通常由一个接口或者抽象类实现
  • 具体策略角色:包装了相关的算法和行为
  • 环境角色(上下文):持有一个策略类的引用(或指针),最终给客户端调用

策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。

2 收银软件实例

题目:做一个商场收银软件,营业员根据用户所购买商品的单价数量,向客户收费

我们联想策略模式,对于收费行为,在不同的场景中(正常收费、打折收费、满减收费),对应不同的算法(或称策略)实现。

下面先来看版本一,还未使用策略模式,仅实现基础的收费计算。

2.1 版本一:基础收费

这里使用Qt设计一个收费系统的界面,每次可以输入单价和数量,点确定按钮之后,会在信息框中展示此次的合计价格,支持多个商品的多次计算,多次计算的总价在最下面的总计栏中展示。

对应的代码实现如下:

  • on_okBtn_clicked 为Qt点击确定按钮后的槽函数:该函数实现为,此次的价格合计等于价格x数量,多次的价格累加是总计价格。
  • on_resetBtn_clicked 为Qt点击重置按钮后的槽函数:该函数实现为,清空相关的显示和各种数据
void Widget::on_okBtn_clicked()
{// 此次的价格合计:价格*数量float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();// 总计m_fTotalPrice += thisPrice;// 窗口中展示明细ui->showPanel->append("price:" + ui->priceEdit->text()+ ", num:" + ui->numEdit->text()+ " -> (" + QString::number(thisPrice) + ")");// 显示总计ui->totalShow->setText(QString::number(m_fTotalPrice));
}void Widget::on_resetBtn_clicked()
{m_fTotalPrice = 0;ui->showPanel->clear();ui->totalShow->clear();ui->priceEdit->clear();ui->numEdit->clear();
}

实际的演示效果如下,仅实现单价x数量功能:

如果在此基础上,需要增加打折收费功能,需要怎么做呢?下面来看版本二。

2.2 版本二:增加打折

对于打折功能,在界面上,只需要增加一个打折率的下拉框即可,然后在计算公式上在加一步乘以打折率即可,代码改动不大:

void Widget::on_okBtn_clicked()
{// 根据下拉框获取对应的打折率float rebate = 1.0;if (ui->calcSelect->currentIndex() == 1) rebate = 0.8;else if (ui->calcSelect->currentIndex() == 2) rebate = 0.7;else if (ui->calcSelect->currentIndex() == 3) rebate = 0.5;// 此次的价格合计:价格*数量*打折率float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt() * rebate;// 总计m_fTotalPrice += thisPrice;// 窗口中展示明细ui->showPanel->append("price:" + ui->priceEdit->text()+ ", num:" + ui->numEdit->text()+ ", rebate:" + QString::number(rebate)+ " -> (" + QString::number(thisPrice) + ")");// 显示总计ui->totalShow->setText(QString::number(m_fTotalPrice));
}

演示效果如下,可以支持正常收费、八折收费、七折收费和五折收费。

目前看起来代码也还可以,但如果此时需要增加满减活动呢?比如满300减100这种。

因为满减这种方式,不像打折那样简单的乘以一个打折率就行了,它需要两个参数,**满减的价格条件,**的,满减的优惠值,,对于满300减100的方式,如果是700,满足了2次,就要减200了,这种计算方式需要单独再写一套计算逻辑。

下面来看版本三是如何实现的。

2.3 版本三:简单工厂

联想上次介绍的简单工厂模式,对于目前收费的需求,实际可以将其分类三类:

  • 正常收费类:不需要参数
  • 打折收费类:需要1个参数(打折率)
  • 满减收费类(返利收费类):需要2次参数(满减的价格条件的满减的优惠值)

因此,可以将这3钟方式分别封装为单独的收费类,并通过简单工厂的方式,在不同的收费需求下,实例化对应的收费计算对象,进行收费的计算。

2.3.1 收费类相关代码

对应的代码如下,设计了现金收费类CashSuper以及对应的具体子类:

  • 正常收费类:CashNormal,将原价原路返回
  • 打折收费类:CashRebate,初始化时输入打折率,计算时返回打折后的价格
  • 返利收费类:CashReturn,初始化时输入满减的条件和满减的值,计算时返回满减后的值
// 现金收费类
class CashSuper
{
public:virtual float acceptCash(float money){return money;}
};// 正常收费类
class CashNormal : public CashSuper
{
public:// 原价返回float acceptCash(float money){return money;}
};// 打折收费类
class CashRebate : public CashSuper
{
private:float m_fMoneyRebate = 1.0;public:// 初始化时输入打折率CashRebate(float rebate){m_fMoneyRebate = rebate;}// 返回打折后的价格float acceptCash(float money){return money * m_fMoneyRebate;}
};// 返利收费类
class CashReturn : public CashSuper
{
private:float m_fMoneyCondition = 0;float m_fMoneyReturn    = 0;
public:// 初始化时输入满减的条件和满减的值CashReturn(float moneyCondition, float moneyReturn){m_fMoneyCondition = moneyCondition;m_fMoneyReturn    = moneyReturn;}public:// 返回满减后的值(满足满减倍数,按倍数满减)float acceptCash(float money){float result = money;if (money >= m_fMoneyCondition){result -= ((int) money / (int) m_fMoneyCondition) * m_fMoneyReturn;}return result;}
};//现金收费工厂类
class CashFactory
{
public:CashSuper *createCashAccept(int combIdx) // 参数为下拉列表中的索引{CashSuper *pCS = nullptr;switch (combIdx){case 0: // "正常收费"{pCS = (CashSuper *)(new CashNormal());break;}case 1: // "打8折"{pCS = (CashSuper *)(new CashRebate(float(0.8)));break;}case 2: // "满300返100"{pCS = (CashSuper *)(new CashReturn(float(300), float(100)));break;}default:break;}return pCS;}
};

2.3.2 Qt界面上点击确定的槽函数的修改

Qt界面上点击确定,客户端的处理逻辑如下:

  • 计算此次的价格原价:价格x数量
  • 根据下拉框当前选择的策略,获取对应的索引值,目前代码中写了3种:
    • 索引0:正常收费
    • 索引1:打8折
    • 索引2:满300返100
  • 调用现金计算工厂,传入索引值,实例化对应的现金计算对象
  • 调用现金计算对象,得到此次的计算结果,展示在窗口明细中
  • 计算总计值,显示在总计框
void Widget::on_okBtn_clicked()
{// 此次的价格原价:价格*数量float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();// 下拉框不同计算策略的索引值int idx = ui->calcSelect->currentIndex();// 现金计算工厂CashFactory cashFactory;CashSuper *pCS = cashFactory.createCashAccept(idx);if (pCS != nullptr){// 传入原价,根据结算规则,得到计算后的实际价格thisPrice = pCS->acceptCash(thisPrice);delete pCS;}// 总计m_fTotalPrice += thisPrice;// 窗口中展示明细ui->showPanel->append("price:" + ui->priceEdit->text()+ ", num:" + ui->numEdit->text()+ ", method:" +  ui->calcSelect->currentText()+ " -> (" + QString::number(thisPrice) + ")");// 显示总计ui->totalShow->setText(QString::number(m_fTotalPrice));
}

演示效果如下,可以支持正常收费、八折收费、满300减100收费。

上述代码,使用了简单工厂模式后,如果再需要增加一种新类型的促销手段,比如满100元则有10个积分,则只需要再增加一个现在收费类即可,接收2个参数(满足积分的条件和对应的积分值),继承于CashSuper类。

不过,虽然简单工厂模式实现了对不同的收费计算对象的创建管理,但对于本案例,商场可能经常更改打折的额度和返利额度,而每次维护或扩展收费方式都要改动这个工厂,然后代码需要重新编译部署,好像不是一种很好的方式。

下面来看版本四是如何实现的。

2.4 版本四:策略模式

版本四用到了本篇的主题——策略模式。

策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。

对于本例,商场的促销手段:打折、返利这些,对应的就是算法。

用工厂来生成算法对象,本身也没有问题,但算法只是一种策略,而这些策略是随时可能互相替换的,这就是变化点。

策略模式的作用就是来封装变化点,设计的UML类图如下,与简单工厂的主要区别是将简单工厂类换成了上下文类

  • 上下文类,或称环境类,维护对具体策略的引用
  • 现金收费类,在这里对应的是策略类(父类)
  • 3种具体收费类,在这里对应的是具体的策略类(子类)

策略模式和简单工厂模式初看可能比较像,下面来看下代码实现的区别。

2.4.1 现金收费上下文类

收费类相关代码。相比较版本三,收费类和具体的收费类都不需要动,只需要把简单工厂类改为现金收费上下文类即可

现金收费上下文类有一个CashSuper的指针,实现对具体策略的引用

在初始化CashContext时,传入CashSuper的指针的指针,通过其提供的GetResult方法,可以得到其算法的计算结果。

这里的GetResult方法,调用的是具体策略的acceptCash方法。

//现金收费上下文类
class CashContext
{
private:CashSuper *m_pCS = nullptr;public:CashContext(CashSuper *pCsuper){m_pCS = pCsuper;}~CashContext(){if (m_pCS) delete m_pCS;}float GetResult(float money){return m_pCS->acceptCash(money);}
};

2.4.2 Qt界面上点击确定的槽函数的修改

Qt界面上点击确定,客户端的处理逻辑如下:

  • 计算此次的价格原价:价格x数量
  • 根据下拉框当前选择的策略,获取对应的索引值(0:正常收费,1:打8折,2:满300返100)
  • 然后将具体的算法类作为参数来创建一个上下文类
  • 再调用上下文类的GetResult方法,得到此次的计算结果,展示在窗口明细中
  • 计算总计值,显示在总计框
void Widget::on_okBtn_clicked()
{// 此次的价格原价:价格*数量float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();// 下拉框不同计算策略的索引值int idx = ui->calcSelect->currentIndex();CashContext *pCC = nullptr;switch (idx){case 0: // "正常收费"{pCC = new CashContext(new CashNormal());break;}case 1: // "打8折"{pCC = new CashContext(new CashRebate(float(0.8)));break;}case 2: // "满300返100"{pCC = new CashContext(new CashReturn(float(300), float(100)));break;}default:break;}// 计算后的价格if (pCC != nullptr){// 传入原价,根据结算规则,得到计算后的实际价格thisPrice = pCC->GetResult(thisPrice);delete pCC;}// 总计m_fTotalPrice += thisPrice;// 窗口中展示明细ui->showPanel->append("price:" + ui->priceEdit->text()+ ", num:" + ui->numEdit->text()+ ", method:" +  ui->calcSelect->currentText()+ " -> (" + QString::number(thisPrice) + ")");// 显示总计ui->totalShow->setText(QString::number(m_fTotalPrice));
}

该代码的演示效果和版本三的一样,这里不再贴图。

下面再来分析下版本四的策略模式和版本三的简单工厂模式的区别:

  • 简单工厂模式:通过简单工厂来得到具体的计算对应对象,调用具体对象的acceptCash方法得到结果。
  • 策略模式:通过上下文类来维护对具体策略的引用,调用上下文类的GetResult方法得到结果(本质也是调用其维护的具体策略的acceptCash方法)。

对比发现,两种模式区别就在于;

  • 简单工厂模式是,根据你的需求,给你创建一个对应的收费计算对象,后续的收费计算你和这个对象来对接即可。
  • 而策略模式是,根据你的需求,上下文类帮你和具体的策略对象对接,你需要计算时,仍然通过上下文类的接口获取即可。

对于版本四的代码,Qt界面上客户端的处理代码又变得复杂了,如何将客户端的那些判断逻辑移走呢?下面来看版本五。

2.5 版本五:策略模式+简单工厂

版本四的代码,CashContext上下文类在初始化时,接收的参数是具体的策略类的指针。

在版本五中,将参数改为Qt界面收费类型下拉框的索引值,然后在CashContext内部,根据索引值,利用简单工厂模式,CashContext自己创建对应的策略对象,代码如下;

2.5.1 在策略模式内加入简单工厂

//现金收费上下文类
class CashContext
{
private:CashSuper *m_pCS = nullptr;public:CashContext(int combIdx){switch (combIdx){case 0: // "正常收费"{m_pCS = (CashSuper *)(new CashNormal());break;}case 1: // "打8折"{m_pCS = (CashSuper *)(new CashRebate(float(0.8)));break;}case 2: // "满300返100"{m_pCS = (CashSuper *)(new CashReturn(float(300), float(100)));break;}default:break;}}~CashContext(){if (m_pCS) delete m_pCS;}float GetResult(float money){if (m_pCS){return m_pCS->acceptCash(money);}return money;}
};

2.5.2 Qt界面上点击确定的槽函数的修改

Qt界面上点击确定,客户端的处理逻辑如下:

  • 计算此次的价格原价:价格x数量
  • 根据下拉框当前选择的策略,获取对应的索引值(0:正常收费,1:打8折,2:满300返100)
  • 然后将索引值作为参数来创建一个上下文类
  • 再调用上下文类的GetResult方法,得到此次的计算结果,展示在窗口明细中
  • 计算总计值,显示在总计框

可以看到如下代码中,版本五的Qt确定按钮的逻辑,又变得清爽起来。

但实际上,只是把这部分判断的代码移动到了CashContext中,如果后续需要新增一种算法,还是要修改CashContext中的判断的,但有需求就会有修改,任何需求的变更都是有成本的,只是变更成本高低的不同,继续降低目前CashContext的修改成本,可以利用反射技术,这在后续介绍抽象工厂模式时会提到。

void Widget::on_okBtn_clicked()
{// 此次的价格原价:价格*数量float thisPrice = ui->priceEdit->text().toFloat() * ui->numEdit->text().toInt();// 下拉框不同计算策略的索引值int idx = ui->calcSelect->currentIndex();CashContext cc = CashContext(idx);// 传入原价,根据结算规则,得到计算后的实际价格thisPrice = cc.GetResult(thisPrice);// 总计m_fTotalPrice += thisPrice;// 窗口中展示明细ui->showPanel->append("price:" + ui->priceEdit->text()+ ", num:" + ui->numEdit->text()+ ", method:" +  ui->calcSelect->currentText()+ " -> (" + QString::number(thisPrice) + ")");// 显示总计ui->totalShow->setText(QString::number(m_fTotalPrice));
}

版本五的演示结果与版本三、版本四的效果一样,这里不再贴图。

3 总结

本篇介绍了设计模式中的策略模式,并通过商场收费计算软件的实例,使用Qt和C++编程,从基础的收费功能到后续需求的增加,一步步修改代码,来学习策略模式的使用,以及对比策略模式与简单工厂模式的不同。

这篇关于大话设计模式解读02-策略模式的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1049184

相关文章

如何开启和关闭3GB模式

https://jingyan.baidu.com/article/4d58d5414dfc2f9dd4e9c082.html

十五.各设计模式总结与对比

1.各设计模式总结与对比 1.1.课程目标 1、 简要分析GoF 23种设计模式和设计原则,做整体认知。 2、 剖析Spirng的编程思想,启发思维,为之后深入学习Spring做铺垫。 3、 了解各设计模式之间的关联,解决设计模式混淆的问题。 1.2.内容定位 1、 掌握设计模式的"道" ,而不只是"术" 2、 道可道非常道,滴水石穿非一日之功,做好长期修炼的准备。 3、 不要为了

十四、观察者模式与访问者模式详解

21.观察者模式 21.1.课程目标 1、 掌握观察者模式和访问者模式的应用场景。 2、 掌握观察者模式在具体业务场景中的应用。 3、 了解访问者模式的双分派。 4、 观察者模式和访问者模式的优、缺点。 21.2.内容定位 1、 有 Swing开发经验的人群更容易理解观察者模式。 2、 访问者模式被称为最复杂的设计模式。 21.3.观察者模式 观 察 者 模 式 ( Obser

服务器雪崩的应对策略之----SQL优化

SQL语句的优化是数据库性能优化的重要方面,特别是在处理大规模数据或高频访问时。作为一个C++程序员,理解SQL优化不仅有助于编写高效的数据库操作代码,还能增强对系统性能瓶颈的整体把握。以下是详细的SQL语句优化技巧和策略: SQL优化 1. 选择合适的数据类型2. 使用索引3. 优化查询4. 范式化和反范式化5. 查询重写6. 使用缓存7. 优化数据库设计8. 分析和监控9. 调整配置1、

设置Nginx缓存策略

详细信息 Nginx服务器的缓存策略设置方法有两种:add_header或者expires。 1. add_header 1)语法:add_header name value。 2)默认值:none。 3)使用范围:http、server、location。 配置示例如下: add_header cache-control "max-age=86400";#设置缓存时间为1天。add

从《深入设计模式》一书中学到的编程智慧

软件设计原则   优秀设计的特征   在开始学习实际的模式前,让我们来看看软件架构的设计过程,了解一下需要达成目标与需要尽量避免的陷阱。 代码复用 无论是开发何种软件产品,成本和时间都最重要的两个维度。较短的开发时间意味着可比竞争对手更早进入市场; 较低的开发成本意味着能够留出更多营销资金,因此能更广泛地覆盖潜在客户。 代码复用是减少开发成本时最常用的方式之一。其意图

Builder模式的实现

概念 在创建复杂对象时,将创建该对象的工作交给一个建造者,这个建造者就是一个Builder。在日常的开发中,常常看到,如下这些代码: AlertDialog的实现 AlertDialog.Builder builder = new AlertDialog.Builder(context);builder.setMessage("你好建造者");builder.setTitle

[分布式网络通讯框架]----ZooKeeper下载以及Linux环境下安装与单机模式部署(附带每一步截图)

首先进入apache官网 点击中间的see all Projects->Project List菜单项进入页面 找到zookeeper,进入 在Zookeeper主页的顶部点击菜单Project->Releases,进入Zookeeper发布版本信息页面,如下图: 找到需要下载的版本 进行下载既可,这里我已经下载过3.4.10,所以以下使用3.4.10进行演示其他的步骤。

springboot学习02-[热部署和日志]

热部署和日志 热部署 热部署