如果看过一些关于如何干掉 if...else
的文章,大部分都会介绍到 策略模式
,而今天主要就是带大家更全面的了解什么是 策略模式
意图
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户端而变化
动机
先上个例子,我去小吃店买了一个炸鸡,一瓶可乐,店家会根据我们是新用户,还是会员用户,还是一般用户等用户的身份来决定是否需要打折或者打折的力度,很显然这里就出现了一个 if...else
判断用户的身份,来决定是否打折,例如新用户第一次购买立减10块,一般用户价格不变,老用户打9折,可以看一下伪代码:
原价 = 20;
if 身份 == 新用户:
最终 = 原价 - 10;
if 身份 == 老用户:
最终 = 原价 * 0.9;
else:
最终 = 原价;
此时如果折扣有了新的变化,那么这一部分的代码就需要,或者说根据用户身份我们送上一些不同的小礼品,那么客户端这边改动量就相当大了
最理想的是什么呢?将不同的用户对应的打折策略的封装成一个类,客户端这边不直接接触到具体的打折行为,客户最后只需要知道一个结算的接口即可,例如:
原价 = 20;
if 身份 == 新用户:
策略 = 新用户策略;
if 身份 == 老用户:
策略 = 老用户策略;
else:
策略 = 默认策略;
策略.总价(原价);
最终 = 策略.结算结果();
礼品 = 策略.赠送();
这时候不止是结算结果,赠送礼品,我们甚至可以将针对用户的对话啊等等都封装到对应的策略中,等于说策略中封装了原本判断之后需要做的所有行为,比如折扣方式,礼品等等,客户端在使用的时候简洁了很多,之后就算出现新的策略,实现一种新的策略然后在加入到策略选择中即可,整体的逻辑清晰了很多
但是都说能干掉 if...else
, 这样写还是没有完全干掉啊,对,这就是本模式一个潜在的缺点,就是一个客户要选择一个合适的策略,而这选择的过程其实就是一个判断的过程,并且他还需要知道这些策略到底有哪些不同,不过这部分是可以使用之前提到一种创建型模式来弥补的–简单工厂模式,传入当前用户,由它去判断选择何种策略,例如:
原价 = 20;
策略 = 简单工厂模式.获取策略(当前用户);
策略.总价(原价);
最终 = 策略.结算结果();
礼品 = 策略.赠送();
先看一下,具体的 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
总结
策略模式的核心还是封装变化,并且这份变化可以做到相互替换,这一点还是很值得细细品味的~