本章学习主要参照Datawhale开源学习及《大话设计模式》
本项目结合《大话设计模式》这本书,总结了各种设计模式的基本概念、知识点和适用场景。先通过书中的案例,介绍了23种设计模式及其相关的代码示例,项目中有多种语言代码示例,本文主要采用Python语言进行说明。
第3章 设计模式
3.1 简单工厂模式
模式定义
简单工厂模式(Simple Factory Pattern)是用一个单独的类来实现具体的实例化过程,避免客户端对具体实例化过程的显式指定
结构组成
简单工厂模式由三类主要角色组成:
- 抽象类:定义所有支持算法的公共接口,在这个例子中具体运算抽象类;
- 具体算法类:具体的算法,在这个例子中具体为加减乘除运算类;
- 简单工厂类:维护对运算类的应用。
问题描述
要求用面向对象语言实现一个计算器控制台程序, 输入两个数和运算符号,得到结果。要实现这一程序并不困难,但若未来该程序要增添新的运算符号,而仍想使用这个程序的话,程序应当要设计得耦合性低些,尽量减少增加功能时需要的改动。同时,应该考虑把计算部分独立出来,这样如果未来该windows程序迁移到web版,计算部分也不需要修改。
解决方案
使用简单工厂模式来解决问题。
- 首先定义一个抽象的运算类Operation,定义抽象类的同时定义其所有子类的公共接口,并定义一个方法getResult(numberA,numberB)用于得到结果;
- 分别创造具体的加减乘除运算类,都为抽象运算类的子类,则具体运算类之中若有一者需要修改,不会影响其它的运算类。覆写具体运算类中getResult(numberA,numberB)方法实现运算操作;
创建简单工厂类OperationFactory,根据输入参数,使用条件判断调用具体的运算类,实现业务逻辑和界面逻辑的分离。
代码实现
# 首先定义一个抽象运算类
class Operation(object):
__numberA = 0
__numberB = 0
def get_numberA(self):
return self.__numberA
def set_numberA(self,numberA):
self.__numberA = numberA
def get_numberB(self):
return self.__numberB
def set_numberB(self,numberB):
self.__numberB = numberB
def get_result():
result = 0
return result
numberA = property(get_numberA,set_numberA)
numberB = property(get_numberB,set_numberB)
# 定义加减乘除运算类
class OperationAdd(Operation):
def get_result(self):
result = self.numberA + self.numberB
return result
class OperationSub(Operation):
def get_result(self):
result = self.numberA - self.numberB
return result
class OperationMul(Operation):
def get_result(self):
result = self.numberA * self.numberB
return result
class OperationDiv(Operation):
def get_result(self):
if self.numberB == 0:
raise Exception("除数不能为0")
result = self.numberA / self.numberB
return result
# 简单运算工厂类维护创造运算实例的过程
class OperationFactory(object):
def createOperate(self,operate):
if operate == "+":
self.oper = OperationAdd()
elif operate == "-":
self.oper = OperationSub()
elif operate == "*":
self.oper = OperationMul()
elif operate == "/":
self.oper = OperationDiv()
return self.oper
if __name__ == '__main__':
oper = OperationFactory().createOperate("+")
oper.numberA = 1
oper.numberB = 2
result = oper.get_result()
适用场景
将类当做产品,则使用者可以在不清楚类生产的具体过程的情况下,使用不同的产品。
3.2 策略模式
模式定义
策略模式(Strategy Pattern)是指定义一个算法家族,使得家族内的不同算法都遵从算法家族的接口及方法规范,从而可以实现算法间互相替换,且不会影响到使用算法的客户。
结构组成
策略模式由三类主要角色组成:
- 策略类:定义所有支持算法的公共接口,在这个例子中具体为收费抽象类;
- 具体策略类:具体的算法,在这个例子中具体为各类收费类和折扣优惠收费类;
- 上下文类:维护对策略对象的应用。
问题描述
要求实现一个商场收银软件程序,营业员可以通过输入客户所有购买商品的单价和数量,程序自动计算出总金额。同时,商场有时会有打折活动(如商品打7折),或满减促销活动(如商品满300-100),程序应能考虑这些活动的情形,在尽量减少重复代码的前提下,实现正确的金额计算。
解决方案
使用策略模式来解决问题。
- 首先创建抽象的算法类CashSupur,作为所有促销活动算法的抽象类,同时定义所有支持算法的公共接口,定义方法acceptCash()用于得到结果;
- 创建具体的促销算法类CashNormal,CashRebate等,继承于抽象算法类CashSupur,覆写acceptCash()实现具体的促销算法;
- 创建上下文类CashContext,维护对算法对象的引用,使用时根据用户输入,传入一个具体的促销算法类来配置。
代码实现
import tkinter
import tkinter.ttk
# 首先定义一个收费抽象类。
class CashSuper(object):
def __init__(self):
pass
def accept_cash(self,money):
pass
# 定义具体的促销算法类,包括正常收费类CashNormal,打折收费类CashRebate,返利收费类CashReturn
# 正常收费
class CashNormal(CashSuper):
def accept_cash(self,money):
return money
# 打折收费
class CashRebate(CashSuper):
__moneyRebate = 1
def cash_rebate(self,moneyRebateStr):
self.__moneyRebate = float(moneyRebateStr)
def accept_cash(self,money):
return money*self.__moneyRebate
# 返利收费
class CashReturn(CashSuper):
__moneyCondition = 0
__moneyReturn = 0
def cash_return(self,moneyConditionStr,moneyReturnStr):
self.__moneyCondition = float(moneyConditionStr)
self.__moneyReturn = float(moneyReturnStr)
def accept_cash(self,money):
result = money
if (money >= self.__moneyCondition):
result = money - money // self.__moneyCondition * self.__moneyReturn
return result
# 定义上下文类CashContext,维护对算法对象的引用。
class CashContext(object):
def __init__(self,typ):
self.cs = CashSuper()
if typ == "正常收费":
self.cs = CashNormal()
elif typ == "满300返100":
self.cs = CashReturn()
self.cs.cash_return("300","100")
elif typ == "打8折":
self.cs = CashRebate()
self.cs.cash_rebate("0.8")
def get_result(self,money):
return self.cs.accept_cash(money)
# 客户端如下
class CashWindow(object):
def __init__(self):
self.total = 0
root = tkinter.Tk()
self.label1 = tkinter.Label(root,text="单价:")
self.txtPrice = tkinter.Entry(root,width = 24,)
self.label2 = tkinter.Label(root,text="数量:")
self.txtNum = tkinter.Entry(root,width = 24,)
self.ibxList = tkinter.Text(root,width = 45, height = 10)
self.label4 = tkinter.Label(root,text="总计:")
self.iblResult = tkinter.StringVar()
self.iblResult.set("%.2f"%self.total)
self.result = tkinter.Label(root,textvariable=self.iblResult, height = 2, font = ('TimeNewsRoman',24))
self.button = tkinter.Button(root,text="确定",width = 10,command = self.btnOk_click)
self.buttonReset = tkinter.Button(root,text="重置",width = 10,command = self.btnReset_click)
self.label3 = tkinter.Label(root,text="计算方式:")
self.comboVar = tkinter.StringVar()
self.combobox = tkinter.ttk.Combobox(root, textvariable = self.comboVar,width = 22,)
self.combobox['value'] = ("正常收费","打8折","满300返100")
self.combobox.current(0)
self.layout()
root.mainloop()
def refresh(self):
self.txtPrice.delete('0','end')
self.txtNum.delete('0','end')
def layout(self):
self.label1.grid(row = 0, column = 0, padx = (10,0), pady = 10)
self.txtPrice.grid(row = 0, column = 1, pady = 10,padx = (0,5),)
self.label2.grid(row = 1, column = 0, padx = (10,0))
self.txtNum.grid(row = 1, column = 1,padx = (0,5),)
self.label3.grid(row = 2, column = 0, padx = (10,0))
self.combobox.grid(row = 2, column = 1,padx = (0,5),pady = 10)
self.ibxList.grid(row = 4, column = 0,columnspan = 4,padx = (5,5),pady = 10)
self.label4.grid(row = 5, column = 0, padx = (10,0))
self.result.grid(row = 5, column = 1,columnspan = 3, rowspan = 2)
self.button.grid(row = 0, column = 2, columnspan = 2,pady = 10, padx = (0,10))
self.buttonReset.grid(row = 1, column = 2, columnspan = 2, padx = (0,10))
def btnReset_click(self):
self.total = 0
self.ibxList.delete('1.0','end')
self.iblResult.set("%.2f"%self.total)
self.refresh()
# 主要部分
def btnOk_click(self):
csuper = CashContext(self.comboVar.get())
totalPrice = csuper.get_result(float(self.txtPrice.get()) * float(self.txtNum.get()))
self.total = self.total + totalPrice
self.ibxList.insert('end',"单价:"+self.txtPrice.get()+" 数量:"
+self.txtNum.get()+" "+self.comboVar.get()+" 合计:%.2f"%(totalPrice)+"\n")
self.iblResult.set("%.2f"%self.total)
self.refresh()
if __name__ == '__main__':
CashWindow()
适用场景
一个系统中有多个算法和类很相似,区分这些类和算法的只是其内部行为。
实际应用
在导航应用中,有不同的路径规划算法,如针对步行的、骑行的、搭乘公共交通工具的、以及开车的等。主要导航类的主要工作是在地图上渲染出规划好的路径,并不会在意是由和算法生成的路径。此时可以将路径规划算法使用策略模式进行封装,方便与主要导航类的交互。
优点缺点
优点
可以以相同的方式调用所有算法,减少了各种算法类与使用算法类之间的耦合。策略模式的Strategy类层次为Context类定义了一系列的可供重复使用的算法或行为,继承有助于析取这些算法中的公共功能。简化了单元测试。每个算法都有自己的类,可以通过自己的接口单独测试;
符合“开放封闭原则”,无需对上下文进行修改就可以引入新的策略。
缺点
不适合算法极少发生改变的场景,会使得程序整体过于复杂;
要求客户端必须知晓策略间的不同,因为需要从中选择;
3.3 装饰模式
模式定义
装饰模式(Decorator Pattern)是指创建一个装饰类,来包装原有的类,从而实现动态地向一个现有的对象添加一些额外的职责,同时不改变其原有的结构。装饰模式比生成子类更为灵活。
结构组成
装饰模式由四类主要角色组成:
- 实体接口:对象接口的定义,可以为对象动态添加职责,在这个例子中职责具体为形象展示的过程,由于Person类在此程序中只有形象展示这一个职责,Person类既是访问接口,也是实体类;
- 实体类:在这个例子中具体为Person类;
- 装饰抽象类:继承实体接口,动态扩展其职责,在这个例子中具体为服饰抽象类;
- 具体装饰类:装饰的具体实现,在这个例子中具体为各类服饰类,如大T恤,大垮裤等。
问题描述
使用程序写一个可以给人搭配不同服饰的系统。为实现这一程序,可先定义Person类,将服饰装扮都写为Person类的接口方法。但如若要在此基础上增加“超人”的装扮,就需要修改Person类,会违背开放-封闭原则,因此应考虑将服饰单独为类。同时,由于穿衣过程不能在众目睽睽之下完成,应当考虑在类内部组装完毕,再显示。而类内部的服饰组装过程不是固定的,因为通过服饰组合出一个有个性的人完全可以有无数种方案,如上衣大T恤,既可以搭配球鞋,也可以搭配皮鞋。同时服饰的装扮的先后顺序也有一定讲究,毕竟先穿内裤后穿外裤和先穿外裤再穿内裤是截然不同的效果。也就是说,我们需要将所需功能按正确的顺序串联起来进行控制,此时可以考虑使用装饰模式。
解决方案
使用装饰模式来解决问题。
- 创建抽象的接口类Component,定义给对象动态添加职责的公共接口(在此例中,由于具体的接口只有一个,所以该步也可省略);
- 创建具体的接口Person(Concrete Component),继承于抽象接口类Component,同时:
定义方法Show()用于显示装扮结果; - 创建抽象的装饰类Finery(Decorator),继承于接口类Person(一般来说继承于抽象接口类Component,由于此例只有一个接口,故继承于具体接口类),同时:
定义方法Decorate(component)用于进行装扮过程;
覆写Show()具体装扮结果的显示; - 创建系列具体的服饰类(Concrete Decorator),如Tshirts,BigTrouser等,继承于抽象装饰类Finery,实现具体的装饰对象,同时:
覆写Show()具体装扮结果的显示。
代码实现
# 定义Person类,相当于ConcreteComponent
class Person(object):
def __init__(self,name = ""):
self.name = name
def show(self):
print("装扮的"+self.name)
# 服饰类(Decorator)
class Finery(Person):
component = None
def decorate(self,component:Person):
self.component = component
def show(self):
if self.component != None:
self.component.show()
# 具体服饰类(Concrete Decorator)
class TShirts(Finery):
def show(self):
print("大T恤 ",end = "")
super().show()
class BigTrouser(Finery):
def show(self):
print("垮裤 ",end = "")
super().show()
class Sneakers(Finery):
def show(self):
print("破球鞋 ",end = "")
super().show()
class Suit(Finery):
def show(self):
print("西装 ",end = "")
super().show()
class Tie(Finery):
def show(self):
print("领带 ",end = "")
super().show()
class LeatherShoes(Finery):
def show(self):
print("皮鞋 ",end = "")
super().show()
if __name__ == '__main__':
xc = Person("小菜")
print("第一种装扮:")
dtx = TShirts()
kk = BigTrouser()
pqx = Sneakers()
pqx.decorate(xc)
kk.decorate(pqx)
dtx.decorate(kk)
dtx.show()
print("第二种装扮:")
xz = Suit()
ld = Tie()
px = LeatherShoes()
px.decorate(xc)
ld.decorate(px)
xz.decorate(ld)
xz.show()
print("第三种装扮:")
pqx2 = Sneakers()
px2 = LeatherShoes()
kk2 = BigTrouser()
ld2 = Tie()
pqx2.decorate(xc)
px2.decorate(pqx2)
kk2.decorate(px2)
ld2.decorate(kk2)
ld2.show()
适用场景
可以在不生成很多子类的情况下扩展类,适用于扩展类需求较多,而又不想引起子类膨胀的场景。
实际应用
通知信息有多种渠道,如通过短信、微信、QQ、邮件等。不同的信息会采用不同的多种渠道组合进行通知,此时若对每一个组合都建立子类,会造成子类数量爆炸,可以考虑装饰器模式。
优点缺点
优点
把类中的装饰功能从类中搬移去除,很好地简化了原有的类;
有效地把类的核心职责和装饰功能区分开了,可以去除相关类中重复的装饰逻辑;
装饰类和被装饰类可以独立发展,不会相互耦合;
无需创建新子类即可实现对类功能的动态扩展;
支持运行时添加或删除对象的功能;
满足“单一职责原则”,可将实现许多不同行为的类拆分为多个较小的类。
缺点
在封装器栈中删除特定封装器比较困难;
较难实现行为不受到先后顺序影响的装饰;
各装饰层的代码相对冗余。
3.4 代理模式
模式定义
代理模式(Proxy Pattern)是指实现一个类代表另一个类的功能,为其他对象提供一种代理以控制对这个对象的访问。
结构组成
代理模式由三个主要角色组成:
- 访问接口:在这个例子中具体为送礼物的行为;
- 实体类:在这个例子中具体为追求者;
- 替代实体的代理类:在这个例子中具体为代理。
问题描述
隔壁班的卓贾易想追求娇娇,但是他自己不好意思,就委托和娇娇同班的戴励帮助他。卓贾易给娇娇先后买了芭比娃娃、花、巧克力,饼委托戴励送给娇娇,却没想给娇娇和戴励创造了相处的机会。娇娇和戴励通过接触互生情愫,最后在一起了。卓贾易自然是很生气,一番追求却为他人做了嫁衣。但细细想来,虽说一直是卓贾易给娇娇买的礼物,但娇娇自始至终都是从戴励手里拿到的礼物,她并未接触过送礼之人——卓贾易。
现在我们想用程序来描述这一个故事,关键之处在于准确描述卓贾易、戴励及娇娇三者之间的行动关系。如果我们只描述了追求者卓贾易,和被追求者娇娇,则与实际情况不符,因为娇娇并不认识卓贾易;而若我们只描述了代理戴励,和被追求者娇娇,亦与实际情况不符,因为礼物是卓贾易买的,戴励并没有礼物直接送给娇娇。为了准确描述他们三人的关系,我们可以考虑使用代理模式。
解决方案
使用代理模式来解决这一问题。
- 创建送礼物的抽象类IGiveGift,定义追求者和代理的共用接口:
送玩具方法GiveDolls();
送花方法GiveFlowers();
送巧克力方法GiveChocolate()。 - 创建追求者Pursuit,定义需要代理的真正实体,继承于抽象类IGiveGift:
覆写类初始化方法,记录被追求者姓名;
覆写送玩具、送花、送巧克力方法,具体送礼操作。 - 创建代理Proxy,保存追求者实体的一个引用,使得代理可以访问实体,继承于抽象类IGiveGift,实现对实体的替代:
覆写类初始化方法,初始化的同时初始化一个追求者Pursuit对象;
覆写送玩具、送花、送巧克力方法,在每一个送礼方法下调用追求者Pursuit的具体送礼操作
代码实现
# 首先定义一个被追求者类,是为了使故事可以完整描述,无需实现特别的功能。
class SchoolGirl(object):
__name = None
def get_name(self):
return self.__name
def set_name(self,name):
self.__name = name
name = property(get_name,set_name)
# 定义送礼物的抽象类IGiveGift。
class IGiveGift(object):
def give_dolls(self):
pass
def give_flowers(self):
pass
def give_chocolate(self):
pass
# 追求者类增加了实现送礼物的接口的改动
class Pursuit(IGiveGift):
__mm = None
def __init__(self,mm:SchoolGirl):
self.__mm = mm
def give_dolls(self):
print(self.__mm.name + " 送你洋娃娃")
def give_flowers(self):
print(self.__mm.name + " 送你鲜花")
def give_chocolate(self):
print(self.__mm.name + " 送你巧克力")
# 代理类,是唯一即认识追求者,又认识被追求者的类。初始化时与追求者及被追求者建立关联,实现送礼物方法时调用追求者的同名方法。
class Proxy(IGiveGift):
def __init__(self,mm:SchoolGirl):
self.gg = Pursuit(mm)
def give_dolls(self):
self.gg.give_dolls()
def give_flowers(self):
self.gg.give_flowers()
def give_chocolate(self):
self.gg.give_chocolate()
if __name__ == '__main__':
jiaojiao = SchoolGirl()
jiaojiao.name = "李娇娇"
daili = Proxy(jiaojiao)
daili.give_dolls()
daili.give_flowers()
daili.give_chocolate()
适用场景
不方便直接访问对象时,为不宜直接访问的对象提供一个访问层。
使用代理模式的方式分为以下几种:
- 本地执行远程服务(远程代理):适用于服务对象位于远程服务器上的情形,可以为一个对象在不同的地址空间提供局部代表。
- 延迟初始化(虚拟代理):如果你有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。
- 访问控制(保护代理/安全代理):如果只希望特定客户端使用服务对象,对象可以是操作系统中的重要部分,而客户端则是各种已启动程序,可使用代理模式。
- 记录日志请求(日志记录代理):适用于需要保存对于服务对象的请求历史记录时。
- 缓存请求结果(缓存代理):适用于需要缓存客户请求结果并对缓存生命周期进行管理时,特别是返回结果体积非常大时。
- 智能指引:调用真实对象时,代理处理另外一些事,可在没有客户端使用某个重量级对象时,立刻销毁该对象。
实际应用
信用卡是银行账户的代理,而银行账户则是一大捆现金的代理。它们都可以实现相同的支付功能。
windows里的快捷方式。
客户端对数据库的查询有时要消耗大量系统资源,常在有需要时才创建(延迟初始化),这会带来代码的大量重复。可以创建代理,让代理伪装成数据库对象,在客户端或实际数据库对象不知情的情况下处理延迟初始化和缓存查询结果的工作。
优点缺点
优点:
引入代理后,职责清晰;
引入代理后,可扩展多种用途,如:
远程代理可以隐藏一个对象存在于不同地址空间的事实。
虚拟代理可以存放实例化时间很长的真实对象。
符合“开放封闭原则”,无需对服务器或客户端进行修改就创建新的代理。
缺点:
代码可能变得复杂,因为需要新建许多类;
服务响应可能会延迟。