策略模式

如果看过一些关于如何干掉 if...else 的文章,大部分都会介绍到 策略模式,而今天主要就是带大家更全面的了解什么是 策略模式

意图

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户端而变化

动机

先上个例子,我去小吃店买了一个炸鸡,一瓶可乐,店家会根据我们是新用户,还是会员用户,还是一般用户等用户的身份来决定是否需要打折或者打折的力度,很显然这里就出现了一个 if...else

判断用户的身份,来决定是否打折,例如新用户第一次购买立减10块,一般用户价格不变,老用户打9折,可以看一下伪代码:

原价 = 20;
if 身份 == 新用户:
    最终 = 原价 - 10;
if 身份 == 老用户:
    最终 = 原价 * 0.9;
else:
    最终 = 原价;

此时如果折扣有了新的变化,那么这一部分的代码就需要,或者说根据用户身份我们送上一些不同的小礼品,那么客户端这边改动量就相当大了

最理想的是什么呢?将不同的用户对应的打折策略的封装成一个类,客户端这边不直接接触到具体的打折行为,客户最后只需要知道一个结算的接口即可,例如:

原价 = 20;

if 身份 == 新用户:
    策略 = 新用户策略;
if 身份 == 老用户:
    策略 = 老用户策略;
else:
    策略 = 默认策略;

策略.总价(原价);
最终 = 策略.结算结果();
礼品 = 策略.赠送();

这时候不止是结算结果,赠送礼品,我们甚至可以将针对用户的对话啊等等都封装到对应的策略中,等于说策略中封装了原本判断之后需要做的所有行为,比如折扣方式,礼品等等,客户端在使用的时候简洁了很多,之后就算出现新的策略,实现一种新的策略然后在加入到策略选择中即可,整体的逻辑清晰了很多

但是都说能干掉 if...else, 这样写还是没有完全干掉啊,对,这就是本模式一个潜在的缺点,就是一个客户要选择一个合适的策略,而这选择的过程其实就是一个判断的过程,并且他还需要知道这些策略到底有哪些不同,不过这部分是可以使用之前提到一种创建型模式来弥补的–简单工厂模式,传入当前用户,由它去判断选择何种策略,例如:

原价 = 20;
策略 = 简单工厂模式.获取策略(当前用户);
策略.总价(原价);
最终 = 策略.结算结果();
礼品 = 策略.赠送();

先看一下,具体的 UML 图,标准的策略模式和这个例子中还是有点出入,我会进一步解释

UML 图

uml

  • Strategy : 定义所有支持的算法的公共接口,Context 使用这个接口来调用某 ContextStrategy 定义的算法
  • ConcreteStrategy : 具体策略,以 Strategy 接口实现某具体算法
  • Context : 上下文
    • 用一个 ConcreteStrategy 对象来配置
    • 维护一个对 Strategy 对象的引用
    • 可定义一个接口让 Stategy 访问它的数据

客户端通常创建并传递一个 ConcreteStrategy 对象给 Context; 客户端仅与 Context 交互
当算法被调用时,Context 可以将该算法所需要的的所有数据都传递给该 Strategy ,或者 Context 可以将自身作为一个参数传递给 Strategy 操作,这就让 Strategy 在需要时可以回调 Context

这里相对于前面例子里的策略多了一层 Context 的中间件,那么客户端不直接操作 Strategy 呢? 下面我提几点我认为这样设计的优势:

  • 算法的数据存放位置的选择:
    • 放在 Strategy 的内部,这时候就需要客户端将数据一一传入,或者通过 Context 传入
    • 放在外部然后依赖注入,我们不可能直接将将客户端的对象传入,那么我们引入 Context, 由 Context 统一管理,将数据和算法拆开,有点类似于 MVC
  • 此外加了一层中间件,可以为客户端提供更简洁的接口,隐藏算法部分的细节,客户端不与策略类接口直接耦合,方便策略类日后更改接口
  • 因为中间件的介入,可以在中间件上直接结合简单工厂模式,从而可以使客户端完全与策略类解耦合,并且使用起来更方便

应用场景

  • 许多相关的类仅仅是行为有异。“策略” 提供了一种用多个行为中的一个行为来配置一个类的方法
  • 需要使用一个算法的不同变体
  • 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自 Stategy 类中以代替这些条件语句

举个例子

使用前面提到的根据用户类型决定折扣方式的例子,首先先实现折扣部分的代码

// 普通用户 不打折
class Discount{
public:
    virtual ~Discount(){}
    virtual double PriceAfterDiscount(double price) {
        return price;
    }
};

// 新用户 减10元
class NewCustomersDiscount :public Discount {
public:
    virtual double PriceAfterDiscount(double price) {
        return price>10?price-10:0;
    }
};

// 会员用户 打9折
class MemberDiscount :public Discount {
public:
    virtual double PriceAfterDiscount(double price) {
        return price*0.9;
    }
};

然后我们实现中间件 Context, 这里为了封装一下,我将类改成 Consume, 它负责记录用户购买的商品信息,并且加入了对 Discount 使用简单工厂的形式直接创建

#include "discount.h"
#include <iostream>

#define SAFE_DELETE(p) if(p){ delete p; p=nullptr;}

class Consume{

public:
    Consume(std::string user="") : m_dicount(nullptr), m_realPrice(0) {
        getDiscount(user);
    }
    ~Consume() { SAFE_DELETE(m_dicount); }

    void updateUser(std::string user) {
        SAFE_DELETE(m_dicount);
        getDiscount(user);
    }

    void AddProduct(std::string name, int price) {
        m_realPrice += price;
        std::cout <<"添加了新的商品 " <<name << " 价格为 " << price << " 目前总价为 "<<m_realPrice << std::endl;
    }

    double getTotalPrice(){
        return m_dicount->PriceAfterDiscount(m_realPrice);
    }

private:
    void getDiscount(std::string user){
        if (user == "NewCustomers")
            m_dicount = new NewCustomersDiscount;
        else if (user == "Member")
            m_dicount = new MemberDiscount;
        else
            m_dicount = new Discount;
    }

private:
    Discount *m_dicount;
    double m_realPrice;
};

最后轮到我们客户端调用了

#include "consume.h"

int main()
{
    Consume member;
    member.AddProduct("cola", 10);
    member.AddProduct("hamburger", 20);
    member.AddProduct("frenchfries", 15);

    double price = member.getTotalPrice();
    std::cout << "共需支付 : "<<price << std::endl;

    member.updateUser("Member");
    price = member.getTotalPrice();
    std::cout << "共需支付 : "<<price << std::endl;
    return 0;
}

运行结果

添加了新的商品 cola 价格为 10 目前总价为 10
添加了新的商品 hamburger 价格为 20 目前总价为 30
添加了新的商品 frenchfries 价格为 15 目前总价为 45
共需支付 : 45
共需支付 : 40.5

总结

策略模式的核心还是封装变化,并且这份变化可以做到相互替换,这一点还是很值得细细品味的~