设计模式--行为型模式

设计模式--行为型模式

行为型模式,共十一种:

策略模式(Calculator子类)、模板方法模式(大象装冰箱)、观察者模式(观察者列表)、迭代子模式、责任链模式、命令模式(一层接一层)、备忘录模式(备忘录类包含被备忘录对象)、状态模式(IF)、访问者模式(访问外观模式)、中介者模式(join)、解释器模式(解释公式)。

1. 策略模式(Strategy Pattern)

策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式就是由这三个部分组成的。

策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类。策略的创建由工厂类来完成,封装策略创建的细节。策略模式包含一组策略可选,客户端代码选择使用哪个策略,有两种确定方法:编译时静态确定和运行时动态确定。其中,“运行时动态确定”才是策略模式最典型的应用场景。

在实际的项目开发中,策略模式也比较常用。最常见的应用场景是,利用它来避免冗长的if-else或switch分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。实际上,策略模式主要的作用还是解耦策略的定义、创建和使用,控制代码的复杂度,让每个部分都不至于过于复杂、代码量过多。除此之外,对于复杂代码来说,策略模式还能让其满足开闭原则,添加新策略的时候,最小化、集中化代码改动,减少引入bug的风险。

参考:
https://zhuanlan.zhihu.com/p/64584526

2. 模板模式

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

模板模式有两大作用:复用和扩展。其中复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

除此之外,我们还讲到回调。它跟模板模式具有相同的作用:代码复用和扩展。在一些框架、类库、组件等的设计中经常会用到,比如JdbcTemplate就是用了回调。

相对于普通的函数调用,回调是一种双向调用关系。A类事先注册某个函数F到B类,A类在调用B类的P函数的时候,B类反过来调用A类注册给它的F函数。这里的F函数就是“回调函数”。A调用B,B反过来又调用A,这种调用机制就叫作“回调”。

回调可以细分为同步回调和异步回调。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。回调跟模板模式的区别,更多的是在代码实现上,而非应用场景上。回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活。

参考:

  1. https://zhuanlan.zhihu.com/p/64584630 代码

3. 观察者模式(Observer Pattern)

观察者模式将观察者和被观察者代码解耦。观察者模式的应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。又称为发布-订阅模式

不同的应用场景和需求下,这个模式也有截然不同的实现方式:有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。同步阻塞是最经典的实现方式,主要是为了代码解耦;异步非阻塞除了能实现代码解耦之外,还能提高代码的执行效率;进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互。

框架的作用有隐藏实现细节,降低开发难度,实现代码复用,解耦业务与非业务代码,让程序员聚焦业务开发。针对异步非阻塞观察者模式,我们也可以将它抽象成EventBus框架来达到这样的效果。EventBus翻译为“事件总线”,它提供了实现观察者模式的骨架代码。我们可以基于此框架非常容易地在自己的业务场景中实现观察者模式,不需要从零开始开发。

技术要点总结

  1. 被观察者,Subject 或者叫Observable 需持有observer的集合
  2. 观察者在自己接收通知的方法中获取被观察者的状态以及通知

观察者模式的应用非常广泛,例如在各种消息中间件中,如kafka、rabitmq等,例如RxJava 等等。其实Java从1.0就对观察者模式进行了支持,提供了两个类型java.util.Observable和java.util.Observer ,但其在Java9时候被标记为废弃状态,

通过对比,我们发现,观察者模式和回调函数模式及其相似,差别仅在于在观察者模式中,被观察者维护这一个观察者列表,而在回调模式中,“被观察者”只是保存了一个“观察者”。 这就是形式上的终极区别。 也就是说,回调函数是一种特殊的观察者模式,是一种一对一的观察者模式。

参考:

  1. https://zhuanlan.zhihu.com/p/528354177 代码

4. 迭代器模式(Iterator Pattern)

迭代器模式也叫游标模式,它用来遍历集合对象。这里说的“集合对象”,我们也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如,数组、链表、树、图、跳表。迭代器模式主要作用是解耦容器代码和遍历代码。大部分编程语言都提供了现成的迭代器可以使用,我们不需要从零开始开发。

遍历集合一般有三种方式:for循环、foreach循环、迭代器遍历。后两种本质上属于一种,都可以看作迭代器遍历。相对于for循环遍历,利用迭代器来遍历有3个优势:

迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。
在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。针对这个问题,有两种比较干脆利索的解决方案,来避免出现这种不可预期的运行结果。一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。第一种解决方案比较难实现,因为很难确定迭代器使用结束的时间点。第二种解决方案更加合理,Java语言就是采用的这种解决方案。增删元素之后,我们选择fail-fast解决方式,让遍历操作直接抛出运行时异常。

举例:
Java中的迭代器接口iterator

参考:

  1. https://zhuanlan.zhihu.com/p/382360388

5. 责任链模式(Chain of responsibility)

在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过A处理器处理,然后再把请求传递给B处理器,B处理器处理完后再传递给C处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。

在GoF的定义中,一旦某个处理器能处理这个请求,就不会继续将请求传递给后续的处理器了。当然,在实际的开发中,也存在对这个模式的变体,那就是请求不会中途终止传递,而是会被所有的处理器都处理一遍。

职责链模式常用在框架开发中,用来实现过滤器、拦截器功能,让框架的使用者在不需要修改框架源码的情况下,添加新的过滤、拦截功能。这也体现了之前讲到的对扩展开放、对修改关闭的设计原则。

责任链模式要点:

  1. Handler 接口持有它自己的类型,通过set方法或者构造函数将责任链上的下一个处理器赋值进去
  2. 客户端负责将各个处理器连成链,而且必然知道链上的第一个处理器,通过调用它的handle方法触发处理流程
  3. 注意千万不能将链搞成一个环(将最后一个处理的下一个handler设置为第一个),那样就无法结束了。

实例:
网关框架ZUUL,通过多个handler,完成鉴权、修改变量等功能

参考:

  1. https://zhuanlan.zhihu.com/p/367811878

6. 命令模式(Command Pattern)

命令模式用到最核心的实现手段,就是将函数封装成对象。我们知道,在大部分编程语言中,函数是没法作为参数传递给其他函数的,也没法赋值给变量。借助命令模式,我们将函数封装成对象,这样就可以实现把函数像对象一样使用。

命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等,这才是命令模式能发挥独一无二作用的地方。

从定义上我们就可以看出其可以解决的问题。

当需要将各种执行的动作抽象出来,使用时通过不同的参数来决定执行哪个对象
当某个或者某些操作需要支持撤销的场景
当要对操作过程记录日志,以便后期通过日志将操作过程重新做一遍时
当某个操作需要支持事务操作的时候
以上是命令模式可以胜任的场景,需要你在实践中不断摸索和体会。

命令模式有4个参与角色

  1. Command是一个接口,定义一个命令
  2. ConcreteCommand 具体的执行命令,他们需要实现Command接口
  3. Receiver真正执行命令的角色,那些具体的命令引用它,让它完成命令的执行
  4. Invoker 负责按照客户端的指令设置并执行命令,像命令的撤销,日志的记录等功能都要在此类中完成

参考:

  1. https://zhuanlan.zhihu.com/p/367522817 有故事

7. 备忘录模式(Memento Pattern)

备忘录模式也叫快照模式,具体来说,就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。这个模式的定义表达了两部分内容:一部分是,存储副本以便后期恢复;另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。

备忘录模式的应用场景也比较明确和有限,主要用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计。

对于大对象的备份来说,备份占用的存储空间会比较大,备份和恢复的耗时会比较长。针对这个问题,不同的业务场景有不同的处理方式。比如,只备份必要的恢复信息,结合最新的数据来恢复;再比如,全量备份和增量备份相结合,低频全量备份,高频增量备份,两者结合来做恢复。

备忘录模式有3个角色

Originator
我们知道备忘录模式就是要完成保存状态,然后恢复状态的功能。那么保存和恢复谁的状态呢?对了,就是这个角色的状态。

Memento
这个就比较简单了,就是一个存储状态的类,里面没有业务逻辑,一般是一个POJO。

CareTaker
负责保存和恢复Originator的状态,状态是保存在这类里面的。

https://zhuanlan.zhihu.com/p/376860623

8. 状态模式(State Pattern)

状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。状态机又叫有限状态机,它由3个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

针对状态机,我们总结了三种实现方式。

第一种实现方式叫分支逻辑法。利用if-else或者switch-case分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。

第二种实现方式叫查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。

第三种实现方式就是利用状态模式。对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。

技术要点总结

  1. 必须要有一个Context类,这个类持有State接口,负责保持并切换当前的状态。
  2. 状态模式没有定义在哪里进行状态转换,本例是在Context类进行的,也有人在具体的State类中转换
  3. 当使用Context类切换状态时,状态类之间互相不认识,他们直接的依赖关系应该由客户端负责。 例如,只有在接单状态的操作完成后才应该切换到出库状态,那么出库状态就对接单状态有了依赖,这个依赖顺序应该由客户端负责,而不是在状态内判断。

当使用具体的State类切换时,状态直接就可能互相认识,一个状态执行完就自动切换到了另一个状态。

https://zhuanlan.zhihu.com/p/369732910

9. 访问者模式(Visitor Pattern)

访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。

对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的。除此之外,我们还讲到Double Disptach。如果某种语言支持Double Dispatch,那就不需要访问者模式了。

访问者模式要点总结

  1. 准确识别出Visitor实用的场景,如果一个对象结构不稳定决不可使用,不然在增删元素时改动将非常巨大。
  2. 对象结构中的元素要可以迭代访问
  3. Visitor里一般存在与元素个数相同的visit方法。
  4. 元素通过accept方法通过this将自己传递给了Visitor。

参考:

  1. 阿里的Druid连接池,有AdsVisitor,用于分离不同的SQL语法
  2. https://zhuanlan.zhihu.com/p/380161731

https://zhuanlan.zhihu.com/p/380161731

10. 中介者模式(Mediator Pattern)

中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟n个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。

观察者模式和中介模式都是为了实现参与者之间的解耦,简化交互关系。两者的不同在于应用场景上。在观察者模式的应用场景中,参与者之间的交互比较有条理,一般都是单向的,一个参与者只有一个身份,要么是观察者,要么是被观察者。而在中介模式的应用场景中,参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。

应用实例: 1、中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。 2、机场调度系统。 3、MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。

https://www.runoob.com/design-pattern/mediator-pattern.html

11. 解释器模式

解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。

要想了解“语言”要表达的信息,我们就必须定义相应的语法规则。这样,书写者就可以根据语法规则来书写“句子”(专业点的叫法应该是“表达式”),阅读者根据语法规则来阅读“句子”,这样才能做到信息的正确传递。而我们要讲的解释器模式,其实就是用来实现根据语法规则解读“句子”的解释器。

解释器模式的代码实现比较灵活,没有固定的模板。我们前面说过,应用设计模式主要是应对代码的复杂性,解释器模式也不例外。它的代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。

https://www.cnblogs.com/adamjwh/p/10938852.html 代码

===
参考
https://zhuanlan.zhihu.com/p/347111780 举例a+b-c表达式计算

posted @ 2022-11-21 11:54  starmoon1900  阅读(51)  评论(0编辑  收藏  举报