23种模式 - 总结

创建型设计模式

创建型设计模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。包括:单例模式、工厂模式、建造者模式、原型模式。

单例模式

单例设计模式(Singleton Design Pattern):一个类只允许创建一个对象(或实例),那这个类就是一个单例类,这种设计模式就叫做单例模式

单例模式实现
  • 饿汉式
    饿汉式的实现方式,在类加载期间就已经将instance静态实例初始化好了,所以instance实例的创建是线程安全的,不过这样的实现方式不支持延迟加载实例
  • 懒汉式
    懒汉式相对于饿汉式的优势是支持延迟加载,这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈
  • 双重检测
    双重检测实现方式即支持延迟记载,又支持高并发。只要instance被创建之后,再调用getInstance函数都不会进入到加锁逻辑中。所以这种实现方式解决了懒汉式并发度低的问题
  • 静态内部类
    利用Java的静态内部类来实现单例。这种实现方式即支持延迟加载,也支持高并发,实现起来比双重检测简单
  • 枚举
    基于枚举类型的单例实现。这种实现方式通过Java枚举类型本身的特性,保证了实例创建的线程安全和实例的唯一性。
单例模式缺点
  • 单例对 OOP 特性的支持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不支持有参数的构造函数。

工厂模式

工厂模式(Factory Design Pattern)是最常用的实例化对象模式,是用工厂方法代替 new 操作的一种模式。在创建对象时,不会对客户端暴露对象的创建逻辑,而是通过使用共同的接口来创建对象。

  • 简单工厂(Simple Factory)
    简单工厂模式中创建实例的方法通常为静态方法,因此简单工厂模式又叫做静态工厂方法模式
  • 工厂方法(Factory Method)
    工厂方法模式是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则
  • 抽象工厂(Abstract Factory)
    在简单工厂和工厂方法中,类只有一种分类方式。但如果类有两种分类方式,比如既可以按照配置文件格式来分类,也可以按照解析的对象来分类,那就会对应8个 parser 类。如果未来还需要增加针对业务配置的解析器,过多的类会导致系统难以维护。此时抽象工厂就诞生了,它可以让一个工厂负责创建多个不同类型的对象,可以有效减少工厂类的个数。
什么时候该用工厂方法模式,而非简单工厂模式
  • 当对象的创建逻辑比较复杂,而不是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,推荐使用工厂方法模式
  • 如果对象不可复用,那工厂类每次都要返回不同的对象,使用简单工厂模式就只能选择包含 if-esle 分支逻辑的实现方式,为了避免这种分支逻辑,推荐使用工厂方法模式
判断要不要使用工厂模式
  • 封装变化
    创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明
  • 代码复用
    创建代码抽离到独立的工厂类之后可以复用
  • 隔离复杂性
    封装复杂的创建逻辑,调用者无需了解如何创建对象
  • 控制复杂度
    将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁

建造者模式

建造者模式(Builder Design Pattern、构建者模式、生成器模式)是设计模式的一种,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

为什么需要建造者模式

如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,可以通过构造函数配合 set 方法来解决。
如果存在以下几种情况,就需要考虑使用构造者模式

  • 必填属性过多
    把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填属性也有很多,就会导致构造函数又会出现参数列表过长。如果把必填属性通过 set 方法设置,那校验这些必填属性是否已填写的逻辑就无处安放
  • 类属性之间有一定的依赖关系或约束条件
    如果类属性之间有依赖关系或约束条件,继续使用构造函数配合 set 方法的思路一样会导致这些依赖关系或约束条件的校验逻辑无处安放
  • 创建出不可变对象
    对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,就不能在类中暴露 set 方法。构造函数配合 set 方法来设置属性值的方式就不合适了
与工厂模式有何区别

工厂模式是用来创建不同但是相关类型的对象(继承同一父类或接口的一组子类),由给定的参数决定创建哪种类型的对象。
构造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象

原型模式

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大,这种情况下,可以利用对已有对象(原型)进行复制(或拷贝)的方式来创建新对象,已达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern)

何为“对象的创建成本比较大”

创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。

如果对象中的数据需要经过复杂的计算才能得到,或者需要从RPC、网络、数据库、文件系统等非常慢的 IO 中读取,这种情况下,就可以利用原型模型,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

原型模型的实现方式:深拷贝和浅拷贝
private HashMap currentKeywords=new HashMap<>();
// 原型模式就这么简单,拷贝已有对象的数据,更新少量差值 
HashMap newKeywords = (HashMap) currentKeywords.clone();

浅拷贝和深拷贝的区别在于,浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord对象),而深拷贝得到的是一份完完全全独立的对象

如何深拷贝

递归拷贝对象、对象的引用对象以及引用对象的引用对象...
先将对象序列化,然后再反序列化成新的对象

针对HashMap这种新老替换的数据结构,可以先采用浅拷贝的方式创建 newKeywords。对于需要更新的 SearchWord 对象,再使用深拷贝的方式创建一份新的对象,替换 newKeywords 中的老对象。这种方式即利用了浅拷贝节省时间、空间的优点,又能保证 currentKeywords 中的数据都是老版本的。

总结

单例模式:用来创建全局唯一的对象。
工厂模式:用来创建不同但是相关类型的对象(继承同一父类或接口的一组子类),由给定的参数来决定创建哪种类型的对象
建造者模式:用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象
原型模式:针对创建成本比较大的对象,利用对已有对象复制的方式进行创建,以达到节省创建时间的目的

——

结构型设计模式

结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。

代理模式

代理模式(Proxy Design Pattern)在不改变原始类(或被代理类)代码的情况下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

原理与实现

一般情况下,让代理类和原始类实现相同的接口,如果原始类并没有定义接口,并且原始类代码并不在维护范围内,这种情况下,可以通过让代理类继承原始类的方法来实现代理模式

动态代理

静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。对于静态代码存在的问题,可以通过动态代理来解决。不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。

代理模式的应用场景
  • 业务系统的非功能性需求开发
    在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。将这些附加功能与业务功能解耦,放到代理类中统一处理,让开发人员只需要关注业务方面的开发。
  • 代理模式在RPC、缓存中的应用
    实际上,RPC框架也可以看作一种代理模式,通过远程代理,将网络通信、数据编解码等细节隐藏起来。

桥接模式

桥接模式(Bridge Design Pattern)有两种理解

  • 将抽象和实现解耦,让他们可以独立变化。
    定义中的“抽象”指的并非“抽象类”或“接口”,而是被抽象处来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起
  • 一个类存在两个(或多个)独立变化的维度,通过组合的方式,让这两个(或多个)维度可以独立进行扩展
    类似“组合优于继承”设计原则,通过组合关系来替代继承关系,避免继承层次的指数级爆炸

装饰器模式

装饰器模式(Decorator Design Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。

基于继承的设计方案

如果 InputStream 只有一个子类 FileInputStream 的话,那在 FileInputStream 基础上再设计一个子类 BufferedFileInputStream 也可以接受,毕竟继承结构还算简单。但实际上继承 InputStream 的子类很多,需要给每一个 InputStream 的子类再继续派生支持缓存读取的子类。除了支持缓存读取之外,如果还需要对功能进行其他方面的增强,比如支持按照基本数据类型读取等等,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。

基于装饰器模式的设计方案

针对继承结构过于复杂的问题,可以通过将继承关系改为组合关系来解决。相对于简单的组合关系,装饰器有两个比较特殊的地方:

  • 装饰器类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰器类
    FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。
  • 装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点
    代理模式和装饰器模式的区别在于:代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

适配器模式

适配器模式(Adapter Design Pattern)用来做适配的,它将不兼容的接口转化为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

应用场景

适配器模式是一种事后的补救策略,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。

  • 封装有缺陷的接口设计
    依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入后会影响到自身代码的可测试性。为了隔离设计上的缺陷,可以采用适配器模式对外部系统提供的接口进行二次封装,抽象出更好的接口设计
  • 统一多个类的接口设计
    某个功能的实现依赖多个外部系统(或类),通过适配器模式,将它们的接口适配为统一的接口定义,然后就可以使用多态的特性来复用代码逻辑
  • 替换依赖的外部系统
    把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动
  • 兼容老版本接口
    做版本升级的时候,对于一些要废弃的接口,通常不直接将其删除,而是暂时保留,并标注为 Deprecated,同时将内部实现逻辑委托为新的接口实现
  • 适配不同格式的数据
    除了用于接口的适配,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取不同格式的征信数据,统一为相同格式方便存储和使用。再比如,Java 中的 Arrays.asList 也可以看作一种数据适配器。

代理、桥接、装饰器、适配器的区别

代理、桥接、装饰器、适配器这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似,笼统来说,都可以称之为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

  • 代理模式
    代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同
  • 桥接模式
    桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变
  • 装饰器模式
    装饰器模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用
  • 适配器模式
    适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理、装饰器模式提供的都是跟原始类相同的接口

门面模式

门面模式(Facade Design Pattern)为子系统提供一组统一的接口,定义一组高层接口让子系统更易用

应用场景
  • 解决易用性问题
    门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。
  • 解决性能问题
    通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 APP 客户端的响应速度。
    如果门面接口不多,可以将它跟非门面接口放在一起,也不需要特殊标记;如果门面接口很多,可以在已有的接口上,再重新抽象出一层,专门防止门面接口,从类、包的命名上跟原来的接口层做区分;如果门面接口特别多,并且很多都是跨多个子系统的,可以将门面接口放到一个新的子系统中。
  • 解决分布式事务问题
    要支持两个接口调用在一个事务中执行,是比较难实现的,这涉及分布式事务问题。可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行这两个接口的业务逻辑。

类、模块、系统之间的“通信”,一般都是通过接口调用来完成的。接口设计的好坏直接影响到类、模块、系统是否好用。接口粒度设计得太大,太小都不好。太大会导致接口不可复用,太小会导致接口不易用。在实际开发中,接口的复用性和易用性需要“微妙”的权衡。通常处理原则是:尽量保持接口的可复用性,但针对特殊情况,允许提供冗余的门面接口,来提供更易用的接口。

组合模式

组合模式(Composite Design Pattern)将一组对象组织(Compose)成树形结构,以表示一种“部分-整体”的层次结构。组合让客户端(使用者)可以统一单个对象和组合对象的处理逻辑。

组合模式的设计思想,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成数这种数据结构,业务需求可以通过树上的递归算法来实现。

组合模式将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且利用它树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,业务场景必须能够表示成树形结构。所以组合模式的应用场景比较局限。

享元模式

享元模式(Flyweight Design Pattern)顾名思义是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
一个系统中存在大量重复对象的时候,就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。实际上不仅仅相同对象可以设计成享元,对于相似对象,也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引用这些享元。

实现

享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或 List 来缓存已经创建好的享元对象,以达到复用的目的。

享元模式 VS 单例、缓存、对象池

区别两种设计模式,不能光看代码实现,而是要看设计意图:享元模式是为了实现对象复用,节省内存;单例模式是为了保证对象全局唯一;缓存是为了提高访问效率;池化技术中的“复用”理解为“重复使用”,主要是为了节省时间

——

行为型设计模式

行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。

观察者模式

观察者模式(Observer Design Pattern)在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

被依赖的对象叫做被观察者(Observable),依赖的对象叫做**观察者(Observer)。实际开发中,这两种对象称呼比较灵活(Subject-Observer、Publisher-Subscriber、Producer-Consumer、Emitter-Listener、Dispatcher-Listener)

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

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

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

模板模式

模板方法模式(Template Method Design Pattern)在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。
模板模式主要是用来解决复用和扩展两个问题。复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

回调

A类调用B类,B类反过来又调用A类,这种调用机制就叫做回调。

  • 从应用场景上来看
    同步回调跟模板模式几乎一致,都是在一个大的算法骨架中,自有替换其中的某个步骤,起到代码复用和扩展的目的;而异步回调跟模板模式有较大的差别,更像是观察者模式。
  • 从代码实现上来看
    回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

回调相对于模板模式会更加灵活:

  • 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力
  • 回调可以使用匿名类来创建回调对象,可以不用事先定义类;模板模式针对不同的实现都要定义不同的类
  • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,只需要往用到的模板方法中注入回调对象即可。

策略模式

策略模式(Strategy Design Pattern)定义一组算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端
策略模式解耦的是策略的定义、创建、使用这三部分:

策略的定义

策略类的定义包含一个策略接口和一组实现这个接口的策略类。因为所有的策略都实现相同的接口,所以客户端代码基于接口而非实现编程,可以灵活地替换不同的策略。

策略的创建

因为策略模式会包含一组策略,在使用它们的时候,一般会通过类型来判断创建哪个策略来使用。

  • 如果策略是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,不需要每次调用的时候都创建一个新的策略对象。针对这种情况,可以事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回。
  • 如果策略类是有状态的,根据业务场景的需要,希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,可以按如下方式来实现策略工厂类
策略的使用

策略模式包含一组可选策略,在程序运行期间,根据配置、用户输入、计算结果等不确定的因素,动态决定使用哪种策略

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

职责链模式

责任链模式(Chain Of Responsibility Design Pattern)将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个对象能够处理它为止。

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

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

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

迭代器模式

迭代器模式(Iterator Design Pattern)也叫游标模式(Cursor Design Pattern)。它用来遍历集合对象。这里的集合对象也可以叫做“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、数、图、跳表。迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。

一个完整的迭代器模式,一般会涉及容器和容器迭代器两部分。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。容器中需要定义 iterator() 方法用来创建迭代器。迭代器接口中需要定义 hasNext、currentItem、next三个最基本的方法。容器对象通过依赖注入传递到迭代器类中。

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

  • 迭代器模式封装集合内部的复杂数据结构,开发中不需要了解如何遍历,直接使用容器提供的迭代器即可
  • 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一
  • 迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。

在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。针对这个问题,有两种比较干脆利索的解决方案,来避免出现这种不可预期的运行结果。一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。第一种解决方案比较难实现,因为很难确定迭代器使用结束的时间点。第二种解决方案更加合理,Java 语言就是采用的这种解决方案。增删元素之后,我们选择 fail-fast 解决方式,让遍历操作直接抛出运行时异常。

状态模式

状态模式(State Pattern)是状态机的一种实现方法。它通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,以此来避免状态机类中的分支逻辑判断,应对状态机类代码的复杂性。状态机常用在游戏、工作流引擎等系统开发中。

状态机又叫有限状态机,它由 3 个部分组成:状态(state)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过动作不是必须的,也可能只转移状态,不执行任何动作。

实现方法有三种:

  • 分支逻辑法
    利用 if-else 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,实现方式最简单、最直接、是首选
  • 查表法
    对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。例如游戏
  • 状态模式
    对于状态不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,首选这种方式。例如电商下单、外卖下单等

访问者模式

访问者模式(Visitor Design Pattern)允许一个或多个操作应用到一个组对象上,解耦操作和对象本身

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

正是因为代码实现难理解,所以,在项目中应用这种模式,会导致代码的可读性比较差。如果你的同事不了解这种设计模式,可能就会读不懂、维护不了你写的代码。所以,除非不得已,不要使用这种模式。

备忘录模式

备忘录模式(Memento Design Pattern)在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复对象为先前的状态。也叫快照模式(Snapshot)。

这个模式的定义表达了两部分内容:一是,存储副本以便后期恢复;二是,要在不违背封装原则的前提下,进行对象的备份和恢复。

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

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

命令模式

命令模式(Command Design Pattern)将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能

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

把函数封装成对象之后,对象就可以存储下来,方便控制执行。所以命令模式的主要作用和应用场景,是用来控制命令的执行,比如异步、延迟、排队执行命令,撤销重做命令、存储命令、给命令记录日志等,这才是命令模式能发挥独一无二作用的地方

命令模式 VS 策略模式

在策略模式中,不同的策略具有相同的目的、不同的实现、互相之间可以替换。
在命令模式中,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相之间不可替换

解释器模式

解释器模式(Interpreter Design Pattern)为某个语言定义它的语法表示,并定义一个解释器用来处理这个语法。

这里的“语言”不仅仅指平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。

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

解释器模式的代码实现比较灵活,没有固定的模板。解释器模式代码实现的核心思想,就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。一般的做法是,将语法规则拆分一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。

中介模式

中介模式(Mediator Design Pattern)定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系从多对多转化为一对多。从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。

中介模式 VS 观察者模式

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

总结

每个设计模式都应该由两部分组成:一是应用场景,即这个模式可以解决哪类问题;二是解决方案,即这个模式的设计思路和具体的代码实现。不过代码实现并不是模式必须包含的。如果单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉。实际上,设计模式之间的主要区别还是在于设计意图,也就是应用场景。

设计的初衷是提高代码质量

应用设计模式只是方法,最终的目的是提高代码的质量。具体点就是提高代码的可读性、可扩展性、可维护性等。所有的设计都是围绕着这个初心来做的。

所以在做代码设计的时候,一定要先问下,为什么要这样设计,为什么要应用这种设计模式,这样做是否能真正的提高代码质量,能提高代码质量的哪些方面。如果很难讲清楚,或给出的理由都比较牵强,没有压制性的优势,那基本上就可以断定这是一种过度设计,是为设计而设计。

设计原则和思想是心法,设计模式是招数。掌握心法,以不变应万变。所以设计原则和思想比设计模式更加普适、重要。掌握了设计原则和思想,才能更清楚地了解为什么要用某种设计模式,就能更恰到好处地应用设计模式。

设计的过程是先有问题后又方案

先要去分析代码存在的痛点,比如可读性不好、可扩展性不好等,然后再针对性地利用设计模式去改善,而不是看到某个场景之后,觉得跟之前在某本书中看到的某个设计模式的应用场景很相似,就套用上去,也不考虑合不合适。

如果只是掌握了理论知识,即便把23中设计模式的原理和实现倍的滚瓜烂熟,不具备具体问题具体分析的能力,在面对真实项目的千变万化的代码的时候,很容易就回滥用设计模式,过度设计。

设计的应用场景是复杂代码

设计模式要干的事情就是解耦,也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。而解耦的主要目的是应对代码的复杂性。设计模式就是为了解决复杂代码问题而产生的。

对于复杂代码,比如项目代码量多、开发周期长、参与开发的人员多,前期要多花点时间在设计上,越是复杂代码,花在设计上的时间就要越多。不仅如此,每次提交的代码,都要保证代码质量,都要经过足够的思考和精心的设计,这样才能避免烂代码效应。

相反,如果参与的只是一个简单的项目,代码量不多,开发人员也不够,那简单的问题用简单的解决方案就好,不要引入过于复杂的设计模式,将简单的问题复杂化

持续重构能有效避免过度设计

为了避免错误的需求预判导致的过度设计,推荐持续重构的开发方法。持续重构不仅仅是保证代码质量的重要手段,也是避免过度设计的有效方法。在真正有痛点的时候,再去考虑用设计模式来解决,而不是一开始就为不一定实现的未来需求而应用设计模式。

当对要不要应用某种设计模式感到模棱两可的时候,可以思考一下,如果暂时不用这种设计模式,随着代码的演进,当某一天不得不去使用它的时候,重构的代码是否很大。如果不是,那能不用就不用,怎么简单就怎么来。

避免设计不足的3个必要条件

  • 要有一定理论知识的储备
    要熟练掌握各种设计原则、思想、编程规范、设计模式。理论知识是解决问题的工具,是前人智慧的结晶。
  • 要有一定的刻意训练
    理论知识学过后很容易忘记,遇到问题也想不到对应的知识点,这就是缺乏理论结合实践的刻意训练。
  • 一定要有代码质量意识,设计意识
    写代码之前,要多想想未来会有哪些扩展的需求,哪部分是会变的,哪部分是不变的,这样写会不会导致之后添加新的功能比较困难,代码的可读性好不好等代码质量问题。

不要脱离具体的场景去谈设计

设计是一个非常主观的事情,可以称之为一门“艺术”。那相应地,好坏就很难评判了。如果真要评判,需要放到具体的场景中,脱离具体的场景去谈设计是否合理,都是空谈。

比如,一个手游项目能否被市场接受,往往非常不确定,所以尽快上市占领市场是一款手游致胜的关键,此时对于手游项目来说,往往前期不会花太多的时间在代码设计、代码质量上;对于MMORPG大型端游,一般投资上亿资金,几百号人开发好几个年,推到重来的成本很大。此时代码质量就非常关键,前期就要多花点时间在设计上,否则代码质量太差,bug太多,后期无法维护。

再比如,如果开发的是偏底层的、框架类的、通用的代码,那代码质量就比较重要,因为一旦出问题或代码改动,影响面就比较大;如果开发的是业务系统或不需要长期维护的项目,那稍微放低点代码质量的要求也是没问题的。

你可能感兴趣的:(23种模式 - 总结)