大话设计模式解读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

相关文章

Go语言实现桥接模式

《Go语言实现桥接模式》桥接模式是一种结构型设计模式,它将抽象部分与实现部分分离,使它们可以独立地变化,本文就来介绍一下了Go语言实现桥接模式,感兴趣的可以了解一下... 目录简介核心概念为什么使用桥接模式?应用场景案例分析步骤一:定义实现接口步骤二:创建具体实现类步骤三:定义抽象类步骤四:创建扩展抽象类步

SQL 注入攻击(SQL Injection)原理、利用方式与防御策略深度解析

《SQL注入攻击(SQLInjection)原理、利用方式与防御策略深度解析》本文将从SQL注入的基本原理、攻击方式、常见利用手法,到企业级防御方案进行全面讲解,以帮助开发者和安全人员更系统地理解... 目录一、前言二、SQL 注入攻击的基本概念三、SQL 注入常见类型分析1. 基于错误回显的注入(Erro

Java序列化之serialVersionUID的用法解读

《Java序列化之serialVersionUID的用法解读》Java序列化之serialVersionUID:本文介绍了Java对象的序列化和反序列化过程,强调了serialVersionUID的作... 目录JavChina编程a序列化之serialVersionUID什么是序列化为什么要序列化serialV

C++中的解释器模式实例详解

《C++中的解释器模式实例详解》这篇文章总结了C++标准库中的算法分类,还介绍了sort和stable_sort的区别,以及remove和erase的结合使用,结合实例代码给大家介绍的非常详细,感兴趣... 目录1、非修改序列算法1.1 find 和 find_if1.2 count 和 count_if1

Redis中群集三种模式的实现

《Redis中群集三种模式的实现》Redis群集有三种模式,分别是主从同步/复制、哨兵模式、Cluster,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1. Redis三种模式概述2、Redis 主从复制2.1 主从复制的作用2.2 主从复制流程2

深入理解MySQL流模式

《深入理解MySQL流模式》MySQL的Binlog流模式是一种实时读取二进制日志的技术,允许下游系统几乎无延迟地获取数据库变更事件,适用于需要极低延迟复制的场景,感兴趣的可以了解一下... 目录核心概念一句话总结1. 背景知识:什么是 Binlog?2. 传统方式 vs. 流模式传统文件方式 (非流式)流

Redis中删除策略的几种实现方式

《Redis中删除策略的几种实现方式》本文详细介绍了Redis的过期键删除策略和内存淘汰策略,过期键删除策略包括定时删除、惰性删除和定期删除,具有一定的参考价值,感兴趣的可以了解一下... 目录前言一、设计背景:为什么需要删除策略?二、第一类:过期键的 3 种核心删除策略1. 定时删除(Timed Dele

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

前端缓存策略的自解方案全解析

《前端缓存策略的自解方案全解析》缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,:本文主要介绍前端缓存的自解方案,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、为什么“清缓存”成了技术圈的梗二、先给缓存“把个脉”:浏览器到底缓存了谁?三、设计思路:把“发版”做成“自愈”四、代码

MySQL之搜索引擎使用解读

《MySQL之搜索引擎使用解读》MySQL存储引擎是数据存储和管理的核心组件,不同引擎(如InnoDB、MyISAM)采用不同机制,InnoDB支持事务与行锁,适合高并发场景;MyISAM不支持事务,... 目录mysql的存储引擎是什么MySQL存储引擎的功能MySQL的存储引擎的分类查看存储引擎1.命令