设计模式——行为型
- 一、观察者模式(Observer Pattern)
- 二、模板模式(Template Method Design Pattern)
- 三、策略模式(Strategy Design Pattern)
- 四、职责链模式(Chain Of Responsibility Design Pattern)
- 五、迭代器模式
- 六、状态模式(State Partern)
- 七、访问者模式(Visitor Pattern)
- 八、备忘录模式(Memento Design Pattern)
- 九、命令模式(Command Design Pattern)
- 十、解释器模式(Interpreter Design Pattern)
- 十一、中介者模式(Mediator Design Pattern)
一、观察者模式(Observer Pattern)
1、观察者模式的原理与实现
观察者模式也被称为发布订阅模式(Publish-Subscribe Design Pattern)
定义:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
一般情况下,被依赖的对象叫做被观察者(Observable),依赖的对象叫做观察者(Observe)。
设计模式要干的就是解耦,创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。具体到观察者模式,它是将观察者和被观察者代码解耦,借助设计模式,利用更好的代码结构,将一大坨代码拆分成职责更单一的小类。让其满足开闭原则,高内聚、低耦合等特性,以此来控制和应对代码的复杂性,提高代码的可扩展性。
观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的解耦,再或者一些产品的设计思路,基于同的应用场景和需求,具有不同的实现方式:
-
同步阻塞的实现方式:
观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码,主要是为了代码解耦。 -
异步非阻塞的实现方式:
除了能实现代码解耦,还能提高代码的执行效率
EventBus框架功能需求(事件总线):提供了实现观察者模式的骨架代码,不仅支持异步非阻塞模式,同时也支持同步阻塞模式。 -
有进程内的实现方式:
不管是同步阻塞实现方式还是异步非阻塞实现方式,都是进程内的实现方式。 -
跨进程的实现方式:
注册成功后需要发送用户信息给大数据征信系统(独立的系统),跟它之间的交互是跨不同进程的。
基于消息队列(Message Queue)如ActiveMQ来实现:
- 弊端:需要引入一个新的系统(消息队列),增加了维护成本。
- 好处:基于消息队列的实现方式,被观察者和观察者解耦更加彻底、两部分的耦合更小,被观察者完全不感知观察者,同理他这也完全不感知被观察者。被观察者只管发送消息到消息队列,观察者只管从消息队列中读取消息来执行相应的逻辑。
框架的作用有:隐藏实现细节,降低开发难度。做到代码复用结构,业务与非业务的代码,让程序员聚焦业务开发。
实际上业务开发也会涉及很多非业务功能的开发。在平时业务开发中要善于抽象这些非业务的可复用的功能,并积极的把他们实现成通用的框架。
二、模板模式(Template Method Design Pattern)
1、模板模式原理
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的算法可以理解为广义上的业务逻辑,并不特指数据结构和算法中的“算法”,算法骨架即为模板。包含算法骨架的方法就是“模板法”。
在模板模式经典的实现中,模板方法定义为final,可以避免被子类重写,需要子类重写的方法定义为abstract,可以强迫子类去实现,但不是必须的,模板模式的实现比较灵活。
模板模式的作用:复用和扩展复用指的是所有的子类可以复用父类中提供的模板方法的代码。
扩展:指框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架功能。
2、回调(Callback):
能起到跟模板模式相同的作用,相对于普通的函数调用来说,回调是一种双向调用关系。
a类事先注册某个函数f到b类,a类在调用b类的p函数的时候,b类反过来调用a类注册给他的f函数。这里的f函数就是“回调函数”,a调用b,b反过来又调用a,这种调用机制就叫做“回调”。
回调还可以用于更高层次的架构设计上,如通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的url)给三方支付的系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。
回调可以分为同步回调和异步回调(或者延迟回调)。
- 同步回调指在函数返回之前执行回调函数。
- 异步回调指在函数返回之后执行回调函数。
应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。
回调跟模板模式区别,在代码实现上回调基于组合关系来实现,模板模式基于继承关系来实现。
3、模板模式与回调
从应用场景上来看,同步回调跟模板模式几乎一致,它们都是在一个大的算法骨架中自由替换其中的某个步骤,起到代码复用和扩展的目的,而异步回调跟模板模式有较大的差别,更像是观察者模式。
从代码实现上来看,回调和模板模式完全不同,回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系。模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
组合优于继承、回调相对于模板模式更加灵活:
- JAVA只支持单继承,基于模板模式编写的子类已经继承一个父类,不再具有继承能力。
- 回调可以使用匿名类来创建回调对象,可以不用事先定义类。而模板模式针对不同的实现都要定义不同的子类。
- 某个类定义了多个模板方法,每个方法都有对应的抽象方法,那即使只用到其中一种模板方法子类也必须实现所有的抽象方法。而回调更加灵活,只需要往用到模板方法中注入回调对象即可。
三、策略模式(Strategy Design Pattern)
1、策略模式原理
定义一族算法类,将每个算法分别封装起来,让它们可以相互替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端带值使用算法的代码)。
工厂模式是解耦对象的创建和使用;观察者模式是解耦观察者和被观察者;策略模式解耦的是策略的定义、创建、使用这三部分。
-
策略的定义:
策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类,因为所有的策略类都实现相同的接口,所以客户端代码基于接口而非实现编程,可以灵活地替换换不同的策略。 -
策略的创建:
由工厂类来完成,封装策略创建的细节。 -
策略的使用:
策略模式包含一组可选策略,客户端代码如何选择使用哪个策略有两种确定的方法:编译时静态确定和运行时动态确定。“运行时动态”指的是:事先并不知道会使用哪个策略,而是在程序运行期间,根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。
策略模式可用来移除if-else分支判断,实际上的这得益于策略工厂类本质上是借助“查表法”根据type查表替换,根据type分支判断。
策略模式主要作用是:解耦策略的定义、创建、使用,控制代码的复杂度,让每个部分都不至于过复杂,代码量过多,此外对于复杂代码来说,还能让其满足开闭原则,添加新策略时最小化、集中化代码改动,减少引入bug的风险。
实际上设计原则和思想比设计模式更加普适和重要。
四、职责链模式(Chain Of Responsibility Design Pattern)
1、职责链模式原理与实现
将请求的发送和接收解耦,让多个接受对象都有机会处理这个请求,将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。
在职责链模式中,多个处理器(即接收对象)依次处理同一个请求,一个请求先经过a处理器处理,然后再请求传递给b处理,b处理器处理完成后再传递给c处理器,以此类推,形成一个链条,链条上的每个处理器各自承担各自的处理责任,所以叫做职责链模式。
在以上GOF给出的定义中,一旦某个处理器能够处理这个请求,就不会继续将请求传递给后续的处理器了。但在实际开发中也存在对这个模式的变体:即请求不会中途终止传递,而是会被所有的处理器都处理一遍。
职责链模式有两种常用的实现:一种是使用职责链来存储处理器,另一种是使用数组来存储处理器。后面一种实现方式更加简单。
2、职责链模式应用场景
- 用户生成的内容(论坛发表帖子,可能会包含一些敏感词汇)
有两种处理方式:①直接禁止发布③给敏感词汇打马赛克之后再发布。
第一种处理方式符合GOF给出的职责链模式定义。
第二种是职责链模式的变体。 - 最常用来开发框架的过滤器和拦截器(Servlet Filter、Spring Interceptor).
3、职责链模式如何应对代码的复杂性?如何满足开闭原则?
将大块代码逻辑拆分成函数,将大类拆分成小类,如把各个敏感词过滤函数拆分出来设计成独立的类,让其不会过多复杂在。不修改框架源码的情况下,基于职责链模式提供的扩展点来扩展新的功能,在框架范围内实现了开辟原则。
五、迭代器模式
六、状态模式(State Partern)
1、状态机
状态模式一般用来实现状态机,状态机又叫有限状态机(Finite State Machine),常用在游戏、工作流引擎等系统开发中。不过状态模式是状态机的一种实现方式,状态机实现方式还有分支逻辑法和查表法。
状态机(FSM)有三个组成部分:状态(state)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行,不过动作不是必须的,也可能只转移状态,不执行任何动作。
2、状态机实现方式
- 分支逻辑法:利用if-else或switch-case分支逻辑,参照状态转移图,将每一个状态转移原模原样的直译成代码,这种方式最简单,最直接,是首选方式。
- 查表法:对于状态很多,状态转移比较复杂的状态机来说,查表法比较适合通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
- 状态模式:对于状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,首选这种方式。
3、状态模式的优缺点
- 状态模式的优点:避免过多的分支逻辑,避免了程序的复杂性,符合开闭原则和单一职责原则。
- 状态模式的缺点:类膨胀
状态模式是状态机的一种实现方法,通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,以此来避免状态机类中的分支判断逻辑,应对状态机代码的复杂性。
七、访问者模式(Visitor Pattern)
1、访问者模式的原理与实现
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。保持类职责单一,满足开闭原则以及应对代码的复杂性。
对于访问者模式难点在代码实现上:因函数重载在大部分面向对象编程语言中是静态绑定的,即调用类的哪个重载函数,是在编译时间由参数的声明类型决定的,而非运行时根据参数的实际类型决定。
一般来说,访问者模式针对的是一组类型不同的对象,但尽管类型不同,它们都继承相同的父类或者实现相同的接口。在不同的应用场景下,需要对这组对象进行一系列不相关的业务操作,但为了避免不断添加功能导致类不断膨胀、职责越不单一,以避免频繁的增删功能,使用访问者模式,将对象与操作解耦,抽离出独立细分的访问者类。
八、备忘录模式(Memento Design Pattern)
1、备忘录模式定义
备忘录模式也叫快照模式,即在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
这个模式的定义主要表达了两部分内容:一部分是存储副本,以便后续恢复;另一部分是要在不违背封装原则的前提下进行对象的备份和恢复。
2、备忘录模式的应用场景
备忘录模式的应用场景主要是:防丢失、撤销、恢复。跟平常说的“备份”很相似,两者的区别在于备忘录模式更侧重于代码的设计和实现;备份更侧重于架构设计或产品设计。
对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长,针对这个问题,不同的业务场景有不同的处理方式,如:只备份必要的恢复信息,结合新的数据来恢复,或者全量备份或增量备份相结合,低频全量备份,高频增量备份,两者结合做恢复。
九、命令模式(Command Design Pattern)
1、命令模式的原理与实现
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求 参数化 其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行,记录日志,撤销等(附加控制)功能。
落实到编码实现命令模式用到的最核心的手段就是将函数封装成对象,我们知道c语言支持函数指针,可以把函数当做变量传递,但是,大部分编程语言中函数没法作为参数传递给其他函数,也没法赋值给变量。借助命令模式,我们可以将函数封装成对象,实现把函数像对象一样使用,当函数封装成对象之后,对象就可以存储下来,方便控制执行。即命令模式的主要作用和应用场景是用来控制命令的执行,比如:异步延迟、排队执行命令,撤销重做命令、存储命令、给命令记录日志等。
十、解释器模式(Interpreter Design Pattern)
1、解释器模式定义
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
实际上,这里的语言不仅仅指我们平时说的中英日法等各种语言,从广义上讲,只要是能承载信息的载体都可以称之为语言,要想了解语言要表达的信息,就必须定义相应的语法规则,这样书写者就可以根据语法规则来写句子,阅读者根据语法规则来阅读句子。这样才能做到信息的正确传递,而解释器模式就是用来实现根据语法规则解读句子的解释器。
2、解释器模式实现
解释器模式的代码实现比较灵活,没有固定的模板,其代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类,一般的做法是将语法规则拆分到一小的独立单元,然后再对每个单元进行解析,最终合并为整个语法规则的解析。
十一、中介者模式(Mediator Design Pattern)
1、中介模式原理
中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互,将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
实际上,中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系),原来一个对象要跟n个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可维护性和可读性。
2、中介模式应用场景
-
经典例子:航空管制
为了让飞机飞行时互不干扰,每架飞机需要知道其他飞机每时每刻的位置,这就需要时刻跟其他飞机通信,飞机通信形成的通信网络就无比复杂,这时引入“塔台”这样一个中介,让每架飞机只跟塔台来通信,发送自己的位置给塔台,由塔台来负责每架飞机的航线调度,这样就大大简化了通信网络。