来源:《大话设计模式》
1. 商场收银软件
def btnOk_Click(total, price, numbers): totalPrice = price * numbers total += totalPrice print("单价: %s, 数量: %s, 合计: %s" % (price, numbers, totalPrice)) return total
A: 我现在要求商场对商品搞活动,所有的商品打八折.
B: 那不就是在totalPrices后面乘以0.8吗?A: 难道商场活动结束,不打折了,你还要再改一次代码,然后再用改后的程序去把所有机器全部安装一次吗?再说还有打五折的情况?B: 通过下拉框解决.2. 增加打折
def btnOk_Click(total, price, numbers, alg): # 选择策略 if alg == 0: totalPrice = price * numbers elif alg == 1: totalPrice = price * numbers * 0.8 elif alg == 2: totalPrice = price * numbers * 0.7 elif alg == 3: totalPrice = price * numbers * 0.5 total += totalPrice print("单价: %s, 数量: %s, 合计: %s" % (price, numbers, totalPrice)) return total
A: 灵活性好多了,不过重复代码很多,像Convert.ToDouble()就写了8遍,而且4个分支要执行的语句除了打折多少以外几乎没有差别,应该考虑重构一下.重点是,有新的需求,满300返100的活动,怎么办?
B: 根据需求,子类有几个写几个.A: 我要满300送80,难道再去加子类?你不想想这当中有哪些是相同的,哪些是不同的.3. 简单工厂实现
B: 打折基本相同,只要个初始化参数就可以了.满几送几,需要两个参数.
A: 面向对象编程,并不是类越多也好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类.打一折和打九折只是形式不同,抽象分析出来,所有的算法都是一样的,所有打折算法应该是个类.代码结构图:
现金收取父类
class CashSuper(abc.ABC): @abc.abstractclassmethod def acceptCash(self, money): """收取现金,参数为原价,返回为当前价""" raise NotImplementedError
正常收费子类
class CashNormal(CashSuper): """正常收款""" def acceptCash(self, money): return money
打折收费类
class CashRebate(CashSuper): """打折类""" def __init__(self, rebate=1.0): self._rebate = rebate def acceptCash(self, money): return money * self._rebate
返利收费子类
class CashReturn(CashSuper): """满几减几 ex: 满300减100,则condition=300, free=100 """ def __init__(self, condition, free): self._condition = condition self._free = free def acceptCash(self, money): if money >= self._condition: return money - (money // self._condition) * self._free else: return money
现金收费工厂类:
class CashFactory(object): """现金收取工厂""" @staticmethod def createCashAccept(alg): cs = None if alg == "正常收费": cs = CashNormal() elif alg == "满300减50": cs = CashReturn(300, 50) elif alg == "8折": cs = CashRebate(0.8) return cs or CashNormal()
客服端:
def btnOk_Click(total, price, numbers, alg): # 选择策略 cashsuper = CashFactory.createCashAccept(alg) totalPrice = cashsuper.acceptCash(price * numbers) total += totalPrice print("单价: %s, 数量: %s, 合计: %s" % (price, numbers, totalPrice)) return total
A: 这是如果要增加满100积分10点?
B: 收费对象生成工厂里增加满100积分10点的分支条件,再到界面稍加修改.A: 简单工厂模式虽然也能解决这个问题,但是这个模式只是解决对象的创建问题,而且由于工厂本身包括了所有的收费方式,商场是可能经常性地改打折额度和返利额度,每次维护或扩展收费方式都要改动这个工厂,以至代码重新编译部署.面对算法的时常变动,应该有更好的方法.4. 策略模式
它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客服.
商场收银时如何促销,用打折还是返利,其实就是一些算法,用工厂来生成算法对象,这没有错,但是算法本身只是一种策略,最重要的是这些算法是随时都有可能互相替换的,这就是变化点,而封装变化点是我们面向对象的一种重要的思维方式.
策略模式的结构图和基本代码:
抽象算法类
class Strategy(abc.ABC): @abc.abstractclassmethod def algorithmInterface(self): """算法方法""" raise NotImplementedError
具体算法A
class ConcreteStrategyA(Strategy): def algorithmInterface(self): print("算法A实现方法")
具体算法B
class ConcreteStrategyB(Strategy): def algorithmInterface(self): print("算法B实现方法")
具体算法C
class ConcreteStrategyC(Strategy): def algorithmInterface(self): print("算法C实现方法")
上下文(用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用)
class Context(object): """上下文 维护一个对Strategy对象的引用 """ def __init__(self, strategy): self._strategy = strategy def contextInterface(self): self._strategy.algorithmInterface()
客服端:
if __name__ == "__main__": context = Context(ConcreteStrategyA()) context.contextInterface() # 算法A实现方法 context = Context(ConcreteStrategyB()) context.contextInterface() # 算法B实现方法 context = Context(ConcreteStrategyC()) context.contextInterface() # 算法C实现方法
5. 策略模式实现
代码结构图:
CashContext:
class CashContext(object): def __init__(self, csuper: CashSuper): self._cs = csuper def getResult(self, money): return self._cs.acceptCash(money)
客服端
def btnOk_Click(total, price, numbers, alg): if alg == "满300减50": cc = CashContext(CashReturn(300, 50)) elif alg == "8折": cc = CashContext(CashRebate(0.8)) else: cc = CashContext(CashNormal()) totalPrice = cc.getResult(price * numbers) total += totalPrice print("单价: %s, 数量: %s, 合计: %s" % (price, numbers, totalPrice)) return total
B: 在客服端去判断用哪一个算法?
A: 有没有好办法,把这个判断的过程从客服端移走呢?A: 难道简单工厂就一定是一个单独的类吗?难道不可以与策略模式的Context结合?(与Context关联度比较高,应将代码转移到Context类中.实例化算法,将其加入到Context中,其后的计算都是调用Context.)6. 策略与简单工厂模式
改造后的CashContext:
class CashContext(object): def __init__(self, alg): self._strategy = self._getStrategy(alg) def _getStrategy(self, alg): if alg == "满300减50": cs = CashReturn(300, 50) elif alg == "8折": cs = CashRebate(0.8) else: cs = CashNormal() return cs def getResult(self, money): return self._strategy.acceptCash(money)
客服端:
def client9(total, price, numbers, alg): cc = CashContext(alg) totalPrice = cc.getResult(price * numbers) total += totalPrice print("单价: %s, 数量: %s, 合计: %s" % (price, numbers, totalPrice)) return total
不同:
简单工厂模式写法:csuper = CashFactory.createCashAccept(alg);...=csuper.GetResult(...)
策略模式和简单工厂结合:
csuper = new CashContext(alg);...=csuper.GetResult(...);
B: 你的意思是,简单工厂模式我需要让客服端认识两个类,CashSuper和CashFactory,而策略模式与简单工厂结合的用法,客服端就只需要认识一个类CashContext就可以了.耦合更加降低.
A: 这使得具体的收费算法彻底地与客服端分离.7. 策略模式解析
- 策略模式是一种定义一系列算法的方法(打几折),从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它们可以以相同的方式调用相同所有的算法,减少了各种算法类与使用算法类之间的耦合.
- 策略模式的Strategy层次为Context定义了一系列的可供重用的算法或行为.继承有助于析取出这些算法中的公共功能.
- 公共的功能就是获取计算费用的结果的GetResult,这使得算法间有了抽象的父类CashSuper.
- 简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试.互补影响.
- 客服端中的switch分支,这也是正常的.因为,当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为.将这些行为封装在一个个独立的Strtegy类中,可以在使用这些行为的类中消除条件语句.在客服端代码中消除条件语句,避免大量判断(虽然转移到CashContext中),这也是非常重要的进展.
- 策略模式就是用来封装算法的,但是在实践中.我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这个变化的可能性
- 在CashContext里还是用到了switch,也就是说,如果我们需要增加一种算法,你必须要更改CashContext中的switch代码.
- 面对同样的需求,当然是改动越小越好.