设计模式笔记
设计模式有不少书籍、笔记和文档,抽象的、形式化的逻辑关系就不再赘述。本文主要记录学习过程中的感性理解,记录具象的案例、自己通俗理解的原理和适用情况。
设计模式笔记
0 链接
CYC2018的笔记,讲的言简意赅,配合Java代码的示例,可读性好,容易快速看懂,还列举了这些设计模式在JDK中的应用。
图说设计模式文档列举的设计模式少一些,但每个设计模式写的都更为详细,配合UML关系图、时序图、C++代码实例以及优缺点评述,可以更深地理解。
菜鸟教程网站的设计模式,配备图示、示例代码,主要是介绍部分对核心思想、优缺点列举的很清晰。
1 创建型模式(Creational Pattern)
顾名思义,创建型模式就是创建实例。
1.1 单例(Singleton)
理解:
- 确保一个类只有一个实例。
案例:
- 驱动程序提供的服务,如:打印机驱动程序提供的打印服务。
原理:
- 通过类的静态方法和静态变量,调用getSingleton方法时返回唯一的实例(若未初始化,则先初始化创建)。
适用:
- 面向全局提供单例的、不希望重复创建实例的情况。
1.2 简单工厂(Simple Factory)
理解:
- 一个工厂根据订单要求来实例化相应的产品。
案例:
- 客户给食品工厂下达订单,订单要求饼干,工厂就调用饼干实例化方法生产饼干;订单要求棒冰,工厂就调用棒冰实例化方法生产棒冰。
原理:
- 工厂类的
creareProduct
方法根据输入的参数,实例化对应的产品作为结果返回。 - 用户调用时,调用具体工厂,用抽象产品来接收返回的创建出的产品。
适用:
- 产品类型少,创建对象少。对扩展性要求不高。因为一旦增加产品,就需要修改工厂实现。
1.3 工厂方法(Factory Method)
理解:
- 一个具体工厂生产其对应的具体产品,用户调用哪个具体工厂,就生产其对应的产品。
- 一个工厂生产一类产品。
案例:
- 饼干工厂生产饼干,棒冰工厂生产棒冰。客户调用哪个具体工厂,就生产得到哪个具体产品。
原理:
- 具体工厂的
createProduct
方法生成其对应的具体产品。 - 用户调用时,用抽象工厂(基类、接口)来调用具体工厂,用抽象产品(基类、接口)来接收具体产品。
适用:
- 比简单工厂可扩展性更好,客户端中处理的都是抽象工厂、抽象产品,可以轻松扩展新的具体工厂和具体产品。符合开放闭合原则。
1.4 抽象工厂(Abstract Factory)
理解:
- 不同的工厂可以生产同类但不同种的产品。
- 一个工厂生产多类产品。
案例:
- 苹果工厂可以生产手机和电脑,小米工厂也可以生产手机和电脑,他们生产的产品属于同类但不同种。客户调用哪个具体工厂,就获取该工厂生产的各类产品。
原理:
- 具体工厂的
createProductA
生产其对应的A类具体商品,createProductB
生产其对应的B类具体商品。 - 每类产品用一个抽象产品表示,每个工厂都可以生产若干个抽象产品的具体产品。
适用:
- 相比于工厂模式一个具体工厂生产一类产品,抽象工厂可以适用于更复杂的情况——一个具体工厂可以生产多类产品。
1.5 生成器(Builder)
理解:
- 生成器可以接受持续的增删修改,最终的结果用来生成一个整体的东西。
案例:
StringBuilder
可以连续接受String的增删,然后生成一个最终的String。- 电商购物车可以接受商品的增删修改,结算的时候再生成一个最终的订单。
原理:
- 生成器内部是一个容器,对容器进行增删修改,最后生成时用容器中的数据来创建一个生成结果。
适用:
- 实例的创建过程比较复杂,不是一下子就能创建的,需要连续增删修改,直到到最终生成的情况。
1.6 原型(Prototype)
理解:
- 相当于克隆给定原型对象,以一个实例为原型,再克隆一个出来。
案例:
- 我有一份纸质文件作为原型,我用复印机复印了一份出来,即创建了原型的复制。
原理:
- 原型类要提供克隆方法供外部调用。
适用:
- 给定原型,克隆出新对象的情况。
2 行为型模式(Behavioral Pattern)
行为型模式是对在不同对象之间划分责任和算法的抽象化。就像是划分每个人的工作内容和职责。
2.1 责任链(Chain of Responsibility)
理解:
- 发送者发出请求,接收方是一个责任链,链上的对象依次收到请求,能处理就处理请求完成任务,不能处理就往后面继续传递,直到最后有接收者处理掉。
案例:
- 用户发来HTTP GET请求,CDN里有缓存就直接响应,没有就发给源站,源站的缓存里有结果就拿来响应,没有就继续查询数据库,数据库有结果就返回,没有就再转交给Not Found的处理机制去处理,完成响应。
原理:
- 把
Handler
组织成链表的形式,前面的无法处理,就交给后续的处理。
2.2 命令(Command)
理解:
- 把命令(命令对应一系列操作)封装在一个命令对象里。
- Client把Command设置到Invoker里用来调用,而Command里负责调用Receiver的方法,即执行命令。
案例:
- 遥控器就是一个Invoker,里面装了很多代表Command的按钮,Client只管操作遥控器上的按钮,按钮所对应的命令程序负责发送信号,遥控目标。
原理:
- 对命令进行封装,将命令执行的流程封装在命令对象中。
- 引入抽象命令接口,外部只需要调用命令实例执行命令方法,具体命令才负责具体完成命令所需的具体执行步骤。
适用:
- Client不直接和Receiver接触。
- Command既然已经封装为实例了,那就可以以实例的形式放到容器里,例如:队列,排队等待执行。
2.3 解释器(Interpreter)
理解:
- 为语言创建解释器,通常由语言的语法和语法分析来定义。
案例:
- 句法分析,句法解析树。
原理:
- 解析上下文,分解交给下一级,形成句法分析树。
适用:
- 语法、句法等分析。
2.4 迭代器(Iterator)
理解:
- 顺序访问,迭代容器内的对象。
案例:
- 各类数据结构的迭代器。
原理:
- 迭代器与容器相关联,记录着容器中的迭代位置,提供hasNext()和next()方法来检测后续有没有,以及获取后续元素。
适用:
- 迭代容器。
2.5 中介者(Mediator)
理解:
- 做一件事情需要牵涉到很多对象,找一个中介,让中介去和杂七杂八的对象打交道,我只要和中介对接就可以了。
案例:
- 线程池里有很多线程,我不需要去管理每一个具体线程,我只需要把任务交给线程池就行。
- 房产中介负责给汇总卖方们的信息,提供给买方一个结论。
原理:
- 中介需要调用各方面的对象(同事),完成复杂的沟通和控制,完成调用者的目标。
适用:
- 调用方从与各种对象的复杂控制关系中脱离出来,交给中介去处理。
2.6 备忘录(Memento)
理解:
- 把对象的内部状态记录下来,必要的适合可以恢复到当时的状态。
案例:
- 序列化,如:把一个字典序列化为JSON格式的文本,必要的适合可以从JSON来恢复出字典。
原理:
- 对象要实现
createMemento
方法(backup)来输出内部状态,实现setMemento
方法(recover)来根据记录内部状态的备忘录恢复内部状态。
适用:
- 个人理解就是实现备份和恢复的功能。
2.7 观察者(Observer)
理解:
- 观察者可以注册到被观察对象上,被观察对象状态变化的时候,要通知观察者。
案例:
- 烟雾报警器状态变化的时候,要发送信息、信号来通知房屋管理方,手机APP。
- 服务器管理平台发现服务器温度过热了,服务器管理平台通过邮件、消息推送告警信息给管理员。
原理:
- 被观察对象里能够添加、关联观察者,被观察对象的update方法除了修改内部状态,也要执行观察者的notify方法。
2.8 状态(State)
理解:
- 主体的状态会自动变化,根据主体的状态,做相应的操作。
案例:
- 在自动贩卖机有货的时候按购买按钮执行的是支付-出货操作,在没货的时候按购买按钮,执行的是缺货提醒操作。按下同一个按钮,在状态不同的时候执行的操作就不同了。
原理:
- 调用的是状态实例的方法,这样状态改变了,调用的方法自然就改变了。
- 状态的方法要检测上下文,及时修改主体的状态。主体关联状态,每一个状态实例也关联主体。
- 售卖机有货的时候,购买时,调用售卖机的
HasProductState.sell()
方法,该方法如果发现卖完就售空了,要把售卖机的状态修改为NoProductState
缺货状态,下次再购买时,调用的就是NoProductState.sell()
了。
2.9 策略(Strategy)
理解:
- 把算法实例化为策略对象,我给主体手动设置哪个策略,主体就执行哪个策略。
案例:
- 创建一个Robot,他调用自己的
hello()
方法来打招呼,该hello
方法调用strategy.hello()
,我把他的策略设置为EnglishStrategy
,他就调用EnglishStrategy.hello()
输出Hello
,我把他的策略设置为ChineseStrategy
,他就调用ChineseStrategy.hello()
输出你好
。
原理:
- 算法由策略对象实现,主体调用自己的策略对象实现的算法。
2.10 模板方法(Template Method)
理解:
- 抽象类定义的功能方法是一个调用模板,模板里具体被调的函数有的实现了,有的是抽象方法,还没实现,需要子类实现。
案例:
- 冲剂饮料作为抽象类,冲泡饮料包含:倒水、加冲剂、搅拌三步。倒水和搅拌没区别,加冲剂则为抽象方法。咖啡作为子类,实现的加冲剂方法就是加咖啡粉。橙汁作为子类,实现的加冲剂方法就是加橙汁粉。基类定义的模板由子类来实现。
原理:
- 抽象类可以定义具体方法和抽象方法,组合起来可以形成一个模板方法。模板方法中用到的抽象方法交给子类去实现。外部调用抽象类的模板方法就可以了。
2.11 访问者(Visitor)
理解:
- 访问者访问对象结构(包含一些对象),对象结构要接待访问者,要安排对象结构中的每个对象接待访问者,让访问者访问这些对象。访问者访问完对象结构中的各个对象,就可以根据访问过程中记录的信息,汇总出结果。
案例:
- 访问者遍历一批客户对象,在访问的过程中汇总这些客户的订单数据(数量、金额)。
- 审计员访问(visit)工厂,工厂总经理负责接待(accept),要安排每一个办公室、车间、仓库的负责人接待(accept)审计员,让审计员访问(visit),审计员完成访问后,就可以得出审计结果,完成审计报告。
原理:
- 访问者实现访问方法
visit(element)
; - 被访问的对象结构和对象实现接待方法
accept(visitor)
,接待方法中负责调用访问者的访问方法visitor.visit(myself)
让访问者能访问自己;
适用:
- 数据结构预留面向访问者的接待方法,可以让外部动态地实现访问方法,增加对数据结构的遍历操作功能。
2.12 空对象(Null)
理解:
- 空对象什么也不做,只是用来代替
Null
。 - 这样一来,调用方就不用检查结果是否为null了,只管调用空对象的对应方法,反正什么也不会做就结束了。
案例:
- JDK中的
Collections.emptySet()
、Collections.emptyMap()
、Collections.emptyList()
都是返回可读但不可写的空对象。
原理:
- 实对象实现有功能的
handle()
方法,空对象实现没功能的handle()
方法。调用实对象时执行功能操作,调用空对象时执行空白的、没有操作的方法。
适用:
- 检查判断Null代价高时,可以用空对象。
3 结构型模式(Structural Pattern)
结构型模式的作用在于,把类或对象以某种结构模式来组合起来,形成一个更大的结构,就像搭积木。
3.1 适配器(Adapter)
理解:
- 适配器把具体的特殊调用转换为统一的调用方法,让用户可以通过适配器的一个方法,调用各种特殊的具体方法。
案例:
- 三脚插座只能用三脚插头,但是通过两脚转三脚适配器,就可以用两脚插头。
原理:
用户只需要调用适配器提供的统一的方法,适配器方法的实现则需要去调用具体各种的特殊方法。
Client只调用一个标准方法(如:
add()
方法),但被调用的数据结构各有各的特殊方法,有的数据结构是append()
方法,有的是push()
方法。通过定义适配器,Client统一都调用适配器adapter.add()
即可,而适配器在adapter.add()
中调用data.push()
或data.append()
这些特殊的add方法。
3.2 桥接(Bridge)
理解:
- 将抽象与实现分离开来,使它们可以独立变化。
- 外部使用抽象接口,内部实现抽象接口。抽象接口就是桥接的桥梁。
- 顶层应用只关心抽象的接口功能,至于每个具体类与对象是怎么实现这些接口功能,是底层实现的关心的事情。顶层变动了底层不需要跟着改,底层变动了顶层也依旧可以工作,只要接口不改就行。
案例:
- 定义图形接口
Drawing
,配有绘制函数draw()
;圆形Circle
、方形Square
都扩展了该接口,分别完成了draw
绘制函数的具体实现。外部只需要通过调用Drawing
接口引用图形实例,调用接口的draw
函数实现实例的绘制,至于每个图形具体是如何实现绘制功能的,外部不需要关心。这种通过接口来引用实例、调用方法的做法就是桥接模式。
原理:
- 通过定义接口、抽象类来规定桥接的”桥“;具体类要负责扩展接口、实现抽象类,外部不关心其具体实现;外部通过接口、抽象类引用具体实例,调用接口、抽象类所定义的方法来调用具体实例的功能。
3.3 组合(Composite)
理解:
- 把对象组合成树形结构。
案例:
- 二叉树,节点实例又包含了对左子树和右子树的引用。
原理:
- 组件(Component)类是组合类(Composite)和叶子类(Leaf)的父类,可以把组合类看成是树的中间节点。
- 组合对象拥有一个或者多个组件对象,因此组合对象的操作可以委托给组件对象去处理,而组件对象可以是另一个组合对象或者叶子对象。
3.4 装饰(Decorator)
理解:
- 为对象添加功能;
- 装饰模式把原来的对象又包了一层(装饰了一层),装饰对象里包含了被装饰对象,不再直接调用原始的被装饰对象。这样一来,调用装饰对象的方法时候,就可以在调用被装饰对象方法的基础之上,再额外执行装饰对象增添的功能。
案例:
- Java中的Stream
I/O,通过
BufferedInputStream
、BufferedOutputStream
、ZipOutputStream
等装饰类,在调用InputStream
和OutputStream
的基础之上,额外实现了缓存、压缩等功能。
原理:
- 装饰类里把被装饰类实例作为成员,外部调用装饰类实现的方法,装饰类再调用被装饰类的方法,并实现一些其他的功能。
3.5 外观(Facade)
理解:
- 系统内部太复杂了,外部难以访问操作,那就对外提供一批统一的简易接口,让外部更容易使用系统的功能。
案例:
- 手机、电脑的有各种一键模式:飞行模式可以一键关闭所有的无线电通信功能,不需要再一个个单独关闭WiFi、蓝牙、移动网络等等;勿扰模式可以一键关闭震动、铃声、消息弹窗等功能,不需要单独屏蔽;
- 一些”安全“软件的一键体检、一键优化也是对外提供的统一接口,一键实现了很多文件扫描、设置修改功能。
原理:
- 定义一个外观类,实现对外提供的方法,例如:一键优化、勿扰模式、飞行模式等方法,这些方法完成具体的操作。
适用:
- 通过外观(Facade)模式,为复杂子系统提供简单接口。
3.6 享元(Flyweight)
理解:
- Flyweight在英语里的意思是”小东西,无足轻重的东西“;
- 享元模式就是通过共享的形式来复用大量内部状态相同的小东西(细粒度对象),相同内容的小对象,没必要重复创建,共享同一个就可以了。
案例:
- Java中的字符串常量池(String Pool),创建字符串对象时,如果字符串常量池中已经有相同内容的字符串常量了,那就不再实例化新对象了,直接共享已有的。
- Java中,包装类
Integer
、Long
、Double
等的valueOf()
函数也是共享相同的常量对象,已有的就不再新建,共享已有常量即可。 - 操作系统中,文件复制的Copy-on-Write(CoW)机制,也是共享(不过可能未必算小对象)。
原理:
- 享元对象包含内部状态(Intrinsic state)和外部状态(Extrinsic state)。内部状态存储在享元内部,不因环境改变;外部状态由Client另外保存,调用的时候再传入享元。享元对象之间,内部状态相同,即可共享;外部状态则可以互相不同。
- 建立一个对象池(享元池,Flyweight Pool),如果缓存池里有相同内部状态的享元对象,就无需实例化新对象,直接共享已有的。
3.7 代理(Proxy)
理解:
- 代理模式控制对原始对象的访问。
- 在用户和目标对象之间,加一层代理。让用户不直接接触目标对象,所需操作要经由代理来代办。
案例:
- 现实生活中,律师就是一种代理,将当事人与其他的相关方隔开,所有的对当事人的询问、诉求、谴责都经由律师处理,由律师来把关,判断合不合适,保护当事人利益。秘书、管家都由代理的功能,访客不直接见主人,需要先请求秘书、管家进行安排,秘书、管家作为中间的代理,负责把关、甄别,再转交给主人处理。
- 软件开发中,代理有以下四类:
- 远程代理(Remote Proxy):控制对远程对象(不同地址空间)的访问,它负责将请求及其参数进行编码,并向不同地址空间中的对象发送已经编码的请求。
- 虚拟代理(Virtual Proxy):根据需要创建开销很大的对象,它可以缓存实体的附加信息,以便延迟对它的访问,例如:在网站加载一个很大图片时,不能马上完成,可以用虚拟代理缓存图片的大小信息,然后生成一张临时图片代替原始图片。
- 保护代理(Protection Proxy):按权限控制对象的访问,它负责检查调用者是否具有实现一个请求所必须的访问权限。
- 智能代理(Smart Reference):取代了简单的指针,它在访问对象时执行一些附加操作:记录对象的引用次数;当第一次引用一个对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。
原理:
- 代理实现和原始对象相同的接口,代理实例中关联了原始对象。使用时访问代理实例,调用代理实例实现的接口方法。代理实例实现的接口方法可以包含:编解码、权限检查、临时缓存、操作记录等功能。
适用:
- 个人感觉代理模式和装饰模式挺像的,都是把原始对象作为代理类型实例、装饰类型实例的成员,让外部不要直接调用原始对象,而是由代理实例、装饰实例去代办,往往用来增加额外功能。我感觉区别可能主要在于,代理模式侧重于控制访问权限(偏安全管控),装饰模式侧重于增加额外操作以改进功能(把粗糙的装饰得更完善)。