大话设计模式解读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++编程,从基础的收费功能到后续需求的增加,一步步修改代码,来学习策略模式的使用,以及对比策略模式与简单工厂模式的不同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/696500.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【设计模式】创建型设计模式之 工厂模式

一、介绍 工厂模式可以分为 3 个小类 简单工厂模式工厂方法模式抽象工厂模式 工厂模式的工厂类,并不一定以 Factory 结尾,例如 DataFormat、Calender 他们都是工厂类,通过静态方法来创建实例。 除此之外,创建对象的方法名称一…

利用医学Twitter进行病理图像分析的视觉-语言基础模型| 文献速递-视觉通用模型与疾病诊断

Title 题目 A visual–language foundation model for pathology image analysis using medical Twitter 利用医学Twitter进行病理图像分析的视觉-语言基础模型 01 文献速递介绍 缺乏公开可用的医学图像标注是计算研究和教育创新的一个重要障碍。同时,许多医生…

Linux - 信号概念 信号产生

Linux - 信号概念 & 信号产生 信号概念信号产生软件信号killraiseabortalarm 硬件信号键盘产生信号硬件中断 信号概念 信号是进程之间事件异步通知的一种方式 在Linux命令行中,我们可以通过ctrl c来终止一个前台运行的进程,其实这就是一个发送信号的…

Java面试_数据库篇_优化,事务,Mysql

Java面试_数据库篇_优化,事务,Mysql 优化如何定位慢查询方案一: 开源工具方案二: Mysql自带慢日志 如何分析慢SQL语句索引介绍索引聚簇索引和非聚簇索引,回表查询覆盖索引,超大分页优化索引创建的原则索引失效 谈谈sql优化的经验 事务事务特性隔离级别un…

vue-2 组件传值

组件关系分类 父子关系非父子关系 父子通信流程 父组件通过props将数据传递给子组件 给子组件以添加属性的方式传值子组件内部通过 props 接收模板中直接使用 props 接收的值 父组件 Parent.vue <template><div class"parent" style"border: 3px s…

力扣 T62 不同路径

题目 连接 思路 思路1 &#xff1a; BFS爆搜 class Solution { public:queue<pair<int,int>>q;int uniquePaths(int m, int n) {q.push({1,1}); // 起始位置vector<pair<int, int>> actions;actions.push_back({0, 1}); // 向下actions.push_bac…

论文中eps格式图片制作

在提交论文终稿时&#xff0c;有时需要提交论文中图片的eps格式&#xff0c;这里记录一下eps格式图片制作的过程&#xff0c;方便以后查阅。 论文中eps格式图片制作 PPT绘制的图片转换为eps格式使用代码生成的图片Latex中显示的图片大小跟Ai中设定画板的大小不一致 PPT绘制的图…

ABB机械人模型下载

可以下载不同格式的 https://new.abb.com/products/robotics/zh/robots/articulated-robots/irb-6700 step的打开各部件是分开的&#xff0c;没有装配在一起&#xff0c;打开看单个零件时&#xff0c;我们会发现其各零件是有装配的定位关系的。 新建一个装配环境&#xff0c;点…

ctfshow-web入门-命令执行(web53-web55)

目录 1、web53 2、web54 3、web55 1、web53 这里的代码有点不一样&#xff0c;说一下这两种的区别&#xff1a; &#xff08;1&#xff09;直接执行 system($c); system($c);这种方式会直接执行命令 $c 并将命令的输出直接发送到标准输出&#xff08;通常是浏览器&#xff…

基于机器学习和深度学习的NASA涡扇发动机剩余使用寿命预测(C-MAPSS数据集,Python代码,ipynb 文件)

以美国航空航天局提供的航空涡扇发动机退化数据集为研究对象&#xff0c;该数据集包含多台发动机从启动到失效期间多个运行周期的多源传感器时序状态监测数据&#xff0c;它们共同表征了发动机的性能退化情况。为减小计算成本&#xff0c;需要对原始多源传感器监测数据进行数据…

软件测试--Mysql快速入门

文章目录 软件测试-mysql快速入门sql主要划分mysql常用的数据类型sql基本操作常用字段的约束&#xff1a;连接查询mysql内置函数存储过程视图事务索引 软件测试-mysql快速入门 sql主要划分 sql语言主要分为&#xff1a; DQL&#xff1a;数据查询语言&#xff0c;用于对数据进…

SpringBoot中实现一个通用Excel导出功能

SpringBoot中实现一个通用Excel导出功能 文章目录 SpringBoot中实现一个通用Excel导出功能这个导出功能的特色看效果代码解析1、依赖2、Excel 入参(ExcelExportRequest)3、Excel 出参(ExcelExportResponse)4、ExcelExportField5、ExcelExportUtils 工具类6、ExcelHead 头部…

鸿蒙开发接口安全:【@ohos.userIAM.userAuth (用户认证)】

用户认证 说明&#xff1a; 本模块首批接口从API version 6开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import userIAM_userAuth from ohos.userIAM.userAuth;完整示例 // API version 6 import userIAM_userAuth from ohos.use…

AI全栈工程师的新舞台:Coze(扣子)

前言 在当前科技飞速发展的背景下&#xff0c;Coze作为一款引领潮流的AI应用平台&#xff0c;正以破竹之势重塑着我们对于智能应用的认知。Coze不仅仅是一个工具&#xff0c;它是一个集合了前沿AI技术、高效开发环境与创意无限的应用生态于一体的创新平台&#xff0c;旨在让每…

RabbitMQ-工作模式(Topics模式RPC模式Publisher Confirms模式)

文章目录 Topics模式topic代码示例 RPC模式客户端界面回调队列关联ID总结RPC代码示例 Publisher Confirms模式概述在通道上启用发布者确认单独发布消息批量发布消息异步处理发布者确认总结总体代码示例 更多相关内容可查看 Topics模式 在Topics中&#xff0c;发送的消息不能具…

QT 信号和槽 信号关联到信号示例 信号除了可以绑定槽以外,信号还可以绑定信号

信号除了可以关联到槽函数&#xff0c;还可以关联到类型匹配的信号&#xff0c;实现信号的接力触发。上个示例中因为 clicked 信号没有参数&#xff0c;而 SendMsg 信号有参数&#xff0c;所以不方便直接关联。本小节示范一个信号到信号的关联&#xff0c;将按钮的 clicked 信号…

---java 抽象类 和 接口---

抽象类 再面向对对象的语言中&#xff0c;所以的对象都是通过类来描述的&#xff0c;但如果这个类无法准确的描述对象的 话&#xff0c;那么就可以把这个类设置为抽象类。 实例 这里用到abstract修饰&#xff0c;表示这个类或方法是抽象方法 因为会重写motifs里的show方法…

【已解决】FileNotFoundError: [Errno 3] No such file or directory: ‘xxx‘

&#x1f60e; 作者介绍&#xff1a;我是程序员行者孙&#xff0c;一个热爱分享技术的制能工人。计算机本硕&#xff0c;人工制能研究生。公众号&#xff1a;AI Sun&#xff0c;视频号&#xff1a;AI-行者Sun &#x1f388; 本文专栏&#xff1a;本文收录于《AI实战中的各种bug…

游戏服务器工程实践一:百万级同时在线的全区全服游戏

我应该有资格写这篇文章&#xff0c;因为亲手设计过可以支撑百万级同时在线的全区全服类型的游戏服务器架构。 若干年前我在某公司任职时&#xff0c;参与研发过一款休闲类型的游戏&#xff0c;由 penguin 厂独代。研发的时候&#xff0c;p 厂要求我们的游戏服务器要能支撑百万…

如何自我认同?是否需要执着于社会性认同?

一、自我认同与社会性认同 自我认同与社会性认同是两个相关但又有所区别的概念&#xff0c;它们分别反映了个体在内心深处对自身价值的认知&#xff0c;以及外界&#xff08;尤其是社会&#xff09;对个体价值的评价与接纳。 自我认同 自我认同是指个体基于自身的价值观、能…