设计模式
一、结构型模式
1)模板方法模式
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤
模板简单来说抽取一部分逻辑,其他具体实现可以直接用。在开发中经常会遇到设计一个系统时,我们会抽取一个抽象类,而有许多个不同的子实现类,但是子实现类中有部分算法逻辑是固定的,那我们只需要在父类中定义好这些逻辑,然后在不同的实现类中扩展额外的算法逻辑即可。
因此我们可以得出一个小结论:模板方法模式是基于继承的
我们顺势根据 UML图 总结下 模板方法模式 中存在的几种角色:
- 抽象类(AbstractClass)角色: 负责给出一个算法的轮廓和骨架,它由一个模板方法和若干个基本方法构成。
- 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法
- 基本方法:实现算法各个步骤的方法,是模板方法的组成部分
- 具体子类(ConcreteClass)角色: 实现抽象类中所定义的抽象方法
接下来我们用个简单的例子来了解一下 模板方法模式 的使用方法:
以上我们以炒菜为例,其中炒菜的步骤是固定的,倒油、下菜、下调料、翻炒
,然后其中基本代码由子类来具体实现。
我们可以发现,模板方法模式是通过父类建立框架,子类再重写父类的部分方法之后,产生不同的效果,通过修改子类可以影响到父类行为的结果,那么这种模式又有啥优缺点呢!如下:
优点:
- 提高代码复用性
- 通过父类调用子类的操作,子类具体的实现扩展了不同的行为,实现了反向控制
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象
- 父类中的抽象方法由子类实现,子类执行的结果会影响到父类的结果,虽实现了反向代理,但也提高了代码阅读的难度
2)策略模式
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
生活中策略模式随处可见,出行方式的选择:
乃至开发工具的选择:
该模式的 UML 图也较为简单:
由图可知存在的角色如下:
- 抽象策略(Strategy)类: 这是一个抽象角色,通常由一个接口或抽象类实现,此角色给出所有的具体策略类所需的接口
- 具体策略(ConcreteStrategy)类: 实现了抽象策略定义的接口,提供具体的算法实现或行为
- 上下文(Context)类: 持有一个策略类的引用,最终给客户端调用
举个生活中的例子如下:
某大型商场开展周年庆活动,根据商家的不同等级(金牌会员,银牌会员,铜牌会员)做出不同的活动策略,活动 UML 图如下:
码示如下:
通过以下代码我们简单的实现了策略模式,其中的优缺点我们也有必要知道:
优点:
- 策略类之间可以自由切换。由于策略类都实现同一个接口,所以使它们之间可以自由切换
- 易于扩展。增加一个新的策略只需要添加一个具体的策略类即可,符合 开闭原则
- 避免使用多重条件选择语句(if else)
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
- 策略模式将造成很多策略类,可以通过使用享元模式在一定程度上减少对象的数量
3)命令模式
讲一个请求封装成为一个对象,使发出请求的责任和执行请求的责任分隔开,这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理
看到这张图我们就会想到我们平时去餐店吃饭的场景:我们需要把我们要点的餐写在订单上,然后服务员拿着订单转去柜台,通知厨师需要出的菜,这个时候厨师根据订单上 的内容将食物备好,最后顾客满意的吃上食物。
这个过程便是利用了命令模式,将订单作为一个请求封装成对象,将服务员和厨师之间的责任隔开,两者只需要通过订单进行沟通,方便将命令模式进行存储、传递、调用、增加与管理。
我们顺势得出命令模式的 UML 图 和 角色:
- 抽象命令(Command)角色: 定义命令的接口,声明执行的方法
- 具体命令(Concrete Command)角色: 具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行操作
- 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要他能够实现命令要求实现的相应功能
- 调用者/ 请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求执行相应操作的地方,也就是说相当于使用命令对象的入口
有了以上基本的了解,我们就这上面点餐的例子,总结一下点餐的 UML 图
码示如下:
看完了示例代码,我们再来盘一盘它的优缺点:
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦
- 增加或删除命令比较方便,采用命令模式增加与删除命令不会影响其他类,它满足 开闭原则,对扩展比较灵活
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类
- 系统结构更加复杂
4)责任链模式
责任链模式又称为职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
现实中我们最经常经历的便是请假,不管是学校请假或是公司请假,我们都得提交请假申请。但是根据请假天数的不同,处理的责任人也不同。在公司请假,可以审批的责任人可能有部门负责人,区域负责人甚至总负责人。但是每个负责人可以批准的天数不同,如果没有用到责任链模式的设计,我们请假就必须自己去找相应的负责人,而我们如果找到部门负责人还比较方便,如果请的天数较多,我们还得去找区域负责人以及总负责人,这是一件十分麻烦的事情。那我们就来了解一下责任链模式到底是怎样运行的。
责任链模式 UML 图 与角色关系如下:
- 抽象处理者(Handler)角色: 定义一个处理请求的接口,包含处理的抽象方法和一个后继连接
- 具体处理者(ConcreteHandler)角色: 实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理请求,否则将该请求转给它的后继者
- 客户端(Client)角色: 创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程
我们根据上面那个例子,总结一下。图示如下:
码示如下:
老规矩盘下 责任链模式 的优缺点:
优点:
非常显著的优点就是将请求和处理分开。请求者不用知道是谁处理的,处理者不用知道请求的全貌。两者解耦,提高系统灵活性
缺点:
- 性能问题。每个请求都是从链头遍历到链尾,在链比较长的时候,性能是一个非常大的问题
- 调试不方便,当链比较长,环节比较多的时候,由于采取了类似递归的方式,调试的时候逻辑比较复杂。
5)状态模式
对有状态的对象,把复杂的 “判断逻辑” 提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为
状态是我们生活中用到比较多的词汇之一。万物皆有状态,不同时间不同地点不同状态。我们可以从精神饱满的状态到昏昏欲睡的状态。
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类
。状态模式 主要解决的问题就是当控制一个对象状态转换的条件表达式过于复杂时,把状态的判断逻辑转移到标识不同状态的一系列类当中。
状态模式 中存在的角色有:
- 抽象状态(State)角色: 定义一个接口,用以封装环境对象中的特定状态所对应的行为
- 具体状态(ConcreteState)角色: 实现抽象状态所对应的行为
- 环境(Context)角色: 也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理
我们如果在网上进行购物的话就会产生订单,不同状态下会有不同的行为,订单状态可分为 加购状态、付款状态、收货状态、评价状态。在整个订单流转的过程中,由不同的事件行为可导致它状态的变更:
一般写法我们是这样的:
public class OrderServiceImpl implements OrderService { private OrderMapper orderMapper; public void process(Long orderId) { Order order = orderDao.findById(orderId); if (order.getStatus() == OrderStatus.WAIT_PAY) { // 待支付,则进行支付 } else if (order.getStatus() == OrderStatus.PAYING) { // 支付中,则进行相应提示 } else if (order.getStatus() == OrderStauts.PART_PAID) { // 部分支付,则进行相应处理 } else { // 其他状态进行对应处理 } } }
如果遇到状态比较多的情况,用 if else
处理起来比较乱,那么我们可以用状态模式来改进试下,UML 图如下:
码示如下:
抽象状态角色:
具体状态角色 - 加购状态:
具体状态角色 - 付款状态:
具体状态角色 - 收货状态:
具体状态角色 - 评价状态:
上下文角色:
以上便是简单的实现了 状态模式,那我们盘一下它的优缺点
优点:
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块
缺点:
- 状态模式的使用必然会增加系统类和对象的个数
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱
- 状态模式对 开闭原则 的支持并不大好
6)观察者模式
观察者模式又称为 发布-订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,是他们能够自动更新自己。
观察者模式相当于一个第三者,比如我们在学校自习的时候,有些人聊天,有些人睡觉,这个时候为了不让老师抓到,我们都会找一个 "放风" 的同学,如果老师来的话就会及时发出通知。
观察者模式 的 UML 图是这样的:
我们可以清晰的看出其中存在的几种角色:
- 抽象主题(Subject)角色: 抽象主题提供增加和删除观察者对象的接口
- 具体主题(ConcreteSubject)角色: 该角色将所有观察者对象保存在一个集合中,当具体主题的内部状态发生改变时,给所有注册过的观察者发送通知
- 抽象观察者(Observer)角色: 抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知更新自己
- 具体观察者(Concrete)角色: 实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态
接下来我们根据以上学生的例子用 观察者模式 实现一下:
这样子就简单地实现了 观察者模式。但是观察者模式也存在不足之处,我们想说说它的好:
优点:
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系
- 被观察者发送通知,所有注册的观察者都会受到消息(广播模式)
缺点:
- 如果观察者非常多的话,所有的观察者收到的被观察者发送的通知会有一定的耗时
- 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃
7)中介者模式
中介模式又称为 调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互
一般来说,同事对象之间的关系是比较复杂的,多个同事对象之间相互关联的时候,他们之间的关系就会呈现为复杂的网状结构,这是一种过渡耦合的架构,,既不利于类的复用,也不稳定,加入其中一个对象发生变化,其他对象也会相应收到影响。
因此这个时候就引入了中介者模式,那这个时候同事对象之间的关系就变成了星型结构,任何一个类的变动,只会影响到类本身以及中介者,这样就减少了系统的耦合。
中介者模式的 UML 图 和 角色 如下:
- 抽象中介者(Mediator)角色: 中介者的接口,提供同事对象注册与转发同事对象信息的抽象方法
- 具体中介者(ConcreteMediator)角色: 实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系
- 抽象同事者(Colleague)角色: 定义同事者接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能
- 具体同事者(ConcreteColleague)角色: 是抽象同事类的实现者,当需要与其他同事交互时,由中介者对象负责后续的交互
中介者模式最经典的案例还是 房产中介
。现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。代码实现如下:
通过 中介者模式,房东与租户可以不用直接交互,较少了类与类之间的复杂关系,那么这种模式又有什么优缺点呢?如下:
优点:
- 松散耦合。把多个同事对象之间的交互都封装在了中介者对象里面,各个同事对象可以独立地变化和复用
- 集中控制。多个同事对象之间的交互被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了。
缺点:
- 如果同事对象过多时,中介者对象会变得过于庞大,而变得复杂,难以维护
8)迭代器模式
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示
这里说的 迭代器模式 与我们平时在集合中用到的 迭代器 是一回事,我们平时是这样这样使用的:
List<String> list = new ArrayList<>(); Iterator<String> iterator = list.iterator(); //list.iterator()方法返回Iterator接口的子实现类对象 while (iterator.hasNext()) { System.out.println(iterator.next()); }
我们平时用的溜溜的,但是具体内部实现你是否有所了解,我们老样子来看下迭代器模式中的 UML 图 和 角色:
- 抽象聚合(Aggregate)角色: 定义添加、删除聚合元素以及创建迭代器对象的接口
- 具体聚合(ConcreteAggregate)角色: 实现抽象聚合类
- 抽象迭代器(Iterator)角色: 定义访问和遍历聚合对象的接口
- 具体迭代器(ConcreteIterator)角色: 实现抽象迭代器接口中所定义的方法, 完成对聚合对象的遍历,记录遍历的当前位置
然后我们用代码来了解一下迭代器的内部实现:
通过以上方式我们简单的实现了 迭代器 的实现,以下说下它的优缺点
优点:
- 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据的遍历等方法,这样可以简化聚合类的设计
- 在迭代器模式中,由于引入抽象层,增加新的聚合类和迭代器类都很方便,无需修改原有代码,满足 开闭原则 的要求
缺点:
增加了类的个数,这在一定程度上增加了系统的复杂性
9)访问者模式
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作
简单来说就是想要为一个对象增强新的行为,且不封装具体的实现,那么就可以用访问者模式。 UML 图如下:
其中存在的角色有:
- 抽象访问者(Visitor)角色: 定义了每一个元素(
Element
)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲是与元素类的个数(Element
)的实现类个数是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变
- **具体访问者(ConcreteVisitor)角色:**给出对每一个元素访问时所产生的具体行为
- 抽象元素(Element)角色: 定义了一个接受访问者的方法(
accept
),意义在于每一个元素都可以被访问者访问
- 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法
- 对象结构(ObjectStructure)角色: 定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(
Element
),并且可以迭代这些元素,供访问者访问
我们假设一个场景,动物园的动物需要饲养员喂养,部分动物(老虎,狮子...)游客也可以喂养,然后我们给这些角色分下类:
- 抽象访问者角色:喂养者
- 具体访问者角色: 饲养员、游客
- 抽象元素角色: 动物
- 具体元素角色: 老虎,狮子
- 对象结构角色: 动物园
代码实现如下:
抽象访问者
具体访问者
抽象元素 & 具体元素
对象结构
测试类
输出
饲养员喂养老虎 老虎受到了饲养员喂养 饲养员喂养狮子 狮子受到了饲养员的喂养 ========= 游客喂养老虎 老虎受到了游客喂养 游客喂养狮子 狮子受到了游客的喂养
通过 访问者模式,我们可以很清楚的实现这个需求,这个模式的优缺点如下:
优点:
- 扩展性好。在不修改对象结构中元素的情况下,为对象结构中的元素添加新的功能
- 复用性好。通过访问者来定义整个对象结构通用的功能,从而提高复用程度
- 分离无关行为。通过访问者来分离无关的行为。把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一
缺点:
- 对象结构变化很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加响应的具体操作,违背了 开闭原则
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类
10)备忘录模式
备忘录模式又称为 快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态
其实这个模式最经典的案例便是 ctrl+z
的功能,在我们编辑文本的时候,撤销操作便是。备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
UML结构图如下:
- 发起人(Originator)角色: 记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息
- 备忘录(Memento)角色: 负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人
- 管理者(Caretaker)角色: 对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问和修改
有玩过游戏的同学,应该都知道一个称为 存档 的功能,存档的目的在于在游戏进行到一个不理想的程度时,可以恢复到之前巅峰的状态。让我们就用代码来简单模拟一下这个游戏的场景。首先游戏角色肯定会有 生命值
的属性,我们简单整理一下 UML 图
然后我们用代码来表示一下:
通过 备忘录模式 我们就可以实现无线重试的机会,让我们来理下这种模式的优缺点:
优点:
- 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
- 实现了信息的封装,使得用户不需要关心状态的保存细节
缺点:
- 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存
11)解释器模式
给定一个语言,定义它的文发表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子
解释器这个名词小伙伴们应该不会很陌生,在编译原理中,一个算术表达式可通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析树。
计算器我们肯定都有用过,如果让你实现一个简单的加法运算,我们的第一个想法应该是这样写的:
public static int plus(Integer ... args) { int sum = 0; for (Integer i : args) { sum += i; } return sum; }
但是这种适合于形式比较单一,有限的场景,如果形式变化比较多,那么这种就不符合要求。
那么现在我们就需要一种翻译识别机器,能够解析由数字、- 、+
构成的运算序列。我们可以把数字和运算符都看作节点, 然后对逐个节点进行读取解析运算,这种就是 解释器模式 的思维。
然后我们来看下解释器模式的 UML 图:
- 抽象表达式(AbstractExpression)角色: 定义解释器的接口,约定解释器的解释操作,主要包含解释方法
interpret()
- 终结符表达式(TerminalExpression)角色: 实现抽象表达式,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结符表达式与之相对应
- 非终结符表达式(NonTerminalExpression)角色: 实现抽象表达式,用来实现文法中与非终结符相关的操作,文法中每条规则都对应与一个非终结符表达式
- 环境(Context)角色: 通常包含多个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值
那我们接下来就用 解释器模式 来简单实现一下 加减法
的逻辑:
UML 图
:
代码如下:
表达式角色:
上下文环境:
测试类:
通过 解释器模式 我们可以很清晰的看出代码的逻辑,实现了以上功能。总结下这种模式的优缺点:
优点:
- 易于改变和扩展文法。在解释器模式中使用类来表示语言的文法规则, 因此可以通过继承等机制来改变或扩展文法,每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言
- 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂
- 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无需修改,符合 开闭原则
缺点:
- 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护
- 执行效率低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程比较麻烦
END