1 Star 0 Fork 0

被水淹没 / Design-Patterns

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

设计模式 Design-Patterns

[TOC]

文章文件夹里有详细文章,

可以点击标题浏览更多内容(附带示例以及实例代码)。

同时欢迎关注我的公众号:迈向架构师,有更多文章分享哦~

设计原则

↑ 点击查看详细说明 ↑

设计原则

单一职责原则(Single Responsibility Principle)

一个类或者模块只负责单一职责

开闭原则(Open Closed Principle)

软件实体(模块、类、方法等)应该对扩展开放、对修改关闭

里式替换原则(Liskov Substitution Principle)

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

另一种说法叫做:按协议设计

接口隔离原则(Interface Segregation Principle)

客户端不应该被强迫依赖它不需要的接口。

依赖反转原则(Dependency Inversion Principle)

高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。

KISS 原则(Keep It Simple and Stupid)

尽量保持简单

YAGNI 原则(You Ain’t Gonna Need It)

不要做过度设计

DRY原则(Don’t Repeat Yourself)

不要重复自己

迪米特法则(Law of Demeter)

又叫最少知识原则(The Least Knowledge Principle)

每个模块只应该了解那些与它关系密切的模块的有限知识。

创建型

设计模式-创建型


单例模式(Singleton Design Pattern)

↑ 点击查看详细说明与示例 ↑

介绍

单例模式:一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

适用场景

  • 保证一个类只有一个实例
  • 为该实例提供一个全局访问节点

优缺点

优点:

  • 可以保证一个类只有一个实例
  • 获得指向该实例的全局访问节点

缺点:

  • 对 OOP 特性支持不友好
  • 隐藏了类之间的依赖关系
  • 扩展性、可测试性不友好
  • 不支持有参数的构造函数

实现方式

  1. 将默认构造函数设为私有, 防止其他对象使用单例类的 new 运算符。
  2. 新建一个静态构建方法作为构造函数。 该函数调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。

工厂模式(Factory)

工厂模式:将创建对象移交给工厂来处理。

大部分工厂类都是以“Factory”这个单词结尾的,但也不是必须的,比如 Java 中的 DateFormat、Calender。

除此之外,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),

但有的也命名为 getInstance()、createInstance()、newInstance(),

有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等。

这里划分为3个部分:简单工厂、工厂方法、抽象工厂。


简单工厂(Simple Factory)

↑ 点击查看详细说明与示例 ↑

介绍

简单工厂模式描述了一个类,它拥有一个包含大量条件语句的构建方法,可根据方法的参数来选择对何种产品进行初始化并将其返回。

适用场景

当每个对象的创建逻辑都比较简单的时候,将多个对象的创建逻辑放到一个工厂类中。

优缺点

优点:

  • 代码简单
  • 避免耦合

与其他模式的关系

  • 大多数情况下,简单工厂是引入工厂方法或抽象工厂模式时的一个中间步骤。

实现方式

  1. 新建一个工厂类。
  2. 新建方法,通过入参判断返回生成的对象。

工厂方法(Factory Method)

↑ 点击查看详细说明与示例 ↑

介绍

工厂方法是一种创建型设计模式,其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。

Head First 定义:定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类

适用场景

当每个对象的 创建逻辑 都比较 复杂 的时候,

为了避免设计一个过于庞大的简单工厂类时,将创建逻辑拆分得更细,

让每个对象的创建逻辑独立到各自的工厂类中。

  • 当你在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。
  • 如果你希望用户能扩展你软件库或框架的内部组件,可使用工厂方法。
  • 如果你希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法。

优缺点

优点:

  • 避免耦合
  • 单一职责
  • 开闭原则

缺点:

  • 代码变得复杂

与其他模式的关系

  • 在许多设计工作的初期都会使用工厂方法模式(较为简单,而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、原型模式或生成器模式(更灵活但更加复杂)。

  • 你可以同时使用工厂方法和迭代器模式来让子类集合返回不同类型的迭代器,并使得迭代器与集合相匹配。

  • 工厂方法是模板方法模式的一种特殊形式。同时,工厂方法可以作为一个大型模板方法中的一个步骤。

实现方式

工厂方法模式建议使用特殊的工厂方法代替对于对象构造函数的直接调用。

对象仍将通过 new 运算符创建,只是该运算符改在工厂的方法中调用罢了。

  1. 让所有产品都遵循同一接口。该接口必须声明对所有产品都有意义的方法。
  2. 在创建类中添加一个空的工厂方法。该方法的返回类型必须遵循通用的产品接口。
  3. 在创建者代码中找到对于产品构造函数的所有引用。将它们依次替换为对于工厂方法的调用,同时将创建产品的代码移入工厂方法。你可能需要在工厂方法中添加临时参数来控制返回的产品类型。
  4. 现在,为工厂方法中的每种产品编写一个创建者子类,然后在子类中重写工厂方法,并将基本方法中的相关创建代码移动到工厂方法中。
  5. 如果应用中的产品类型太多,那么为每个产品创建子类并无太大必要,这时你也可以在子类中复用基类中的控制参数。
  6. 如果代码经过上述移动后,基础工厂方法中已经没有任何代码,你可以将其转变为抽象类。如果基础工厂方法中还有其他语句,你可以将其设置为该方法的默认行为。

抽象工厂(Abstract Factory)

↑ 点击查看详细说明与示例 ↑

介绍

抽象工厂是一种创建型设计模式,它能创建一系列相关或相互依赖的对象,而无需指定其具体类。

Head First 定义:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

适用场景

  • 如果代码需要与多个不同系列的相关产品交互,但是由于无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望代码基于产品的具体类进行构建,在这种情况下,你可以使用抽象工厂。
  • 如果你有一个基于一组抽象方法的类,且其主要功能因此变得不明确,那么在这种情况下可以考虑使用抽象工厂模式。
  • 如果你的程序中并不涉及产品系列的话,那就不需要抽象工厂。

优缺点

优点:

  • 可以确保同一工厂生成的产品相互匹配。
  • 可以避免客户端和具体产品代码的耦合。
  • 单一职责原则。你可以将产品生成代码抽取到同一位置,使得代码易于维护。
  • 开闭原则。向应用程序中引入新产品变体时,你无需修改客户端代码。

缺点:

  • 引入众多的接口和类,代码可能会比之前更加复杂。

与其他模式的关系

  • 抽象工厂模式通常基于一组工厂方法,但你也可以使用原型模式来生成这些类的方法。

  • 在许多设计工作的初期都会使用工厂方法模式(较为简单,而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、原型模式或生成器模式(更灵活但更加复杂)。

  • 你可以同时使用工厂方法和迭代器模式来让子类集合返回不同类型的迭代器,并使得迭代器与集合相匹配。

实现方式

  1. 以不同的产品类型与产品变体为维度绘制矩阵。
  2. 为所有产品声明抽象产品接口。然后让所有具体产品类实现这些接口。
  3. 声明抽象工厂接口,并且在接口中为所有抽象产品提供一组构建方法。
  4. 为每种产品变体实现一个具体工厂类。
  5. 在应用程序中开发初始化代码。该代码根据应用程序配置或当前环境,对特定具体工厂类进行初始化。然后将该工厂对象传递给所有需要创建产品的类。
  6. 找出代码中所有对产品构造函数的直接调用,将其替换为对工厂对象中相应构建方法的调用。

工厂模式与 DI 容器

DI 容器:依赖注入容器(Dependency Injection Container)。

一个工厂类只负责某个类对象或者某一组相关类对象的创建,而 DI 容器负责的是整个应用中所有类对象的创建。

DI 容器底层最基本的设计思路就是基于工厂模式的。

DI 容器相当于一个大的工厂类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。

当应用程序需要使用某个类对象的时候,直接从容器中获取即可。

正是因为它持有一堆对象,所以这个框架才被称为“容器”。

DI 容器的核心功能

  • 配置解析
  • 对象创建
  • 对象生命周期管理

建造者模式(Builder)

↑ 点击查看详细说明与示例 ↑

介绍

建造者模式(又叫生成器模式、构建者模式):

建造者模式是一种创建型设计模式,使你能够分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。

适用场景

  • 避免重叠构造函数(telescopic constructor)
  • 希望创建不同形式的产品或分步骤构造产品
  • 类属性有依赖或约束关系时(如单个set无法满足多个值的校验)
  • 创建不可变对象(构建前赋值)
  • ...

优缺点

优点:

  • 可以分步创建 或延缓创建 或递归创建
  • 生成不同形式的产品时可以复用代码
  • 单一职责

缺点:

  • 需要新增类,复杂度增加

与其他模式的关系

  • 工厂模式是用来创建不同但是相关的对象,建造者模式是用来创建一种类型的复杂对象

实现方式

  • 创建一个Builder类(原对象内部类或者独立的类都可以)
  • 实现其构造步骤(每个构造器的set)
  • 实现build方法(包括校验逻辑与 创建逻辑
  • 实现原对象的构造函数(参数为Builder,即创建方法)

原型模式(Prototype)

↑ 点击查看详细说明与示例 ↑

介绍

原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同), 在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。

适用场景

  • 对象创建成本较大(比如需要计算or排序等),且同一个类的不同对象直接差别不大(大部分字段相同)时。
  • 需要复制对象,同时也希望代码能独立于对象所属的具体类时。
  • 子类直接的区别仅在于其初始化的方式时,可以用该模式减少子类的数量(别人创建这些子类的目的可能是为了创建特定类型的对象)。
  • ...

优缺点

优点:

  • 克隆对象,代码不耦合。
  • 可以克隆预生成对象,避免反复初始化。
  • 更方便的生成复杂对象。
  • 可以用继承以外的方式来处理复杂对象的不同配置。

缺点:

  • 克隆包含循环引用的复杂对象可能会非常麻烦。

与其他模式的关系

  • 在许多设计工作的初期都会使用工厂方法模式(较为简单,而且可以更方便地通过子类进行定制),随后演化为使用抽象工厂模式原型模式生成器模式(更灵活但更加复杂)。
  • 抽象工厂模式通常基于一组工厂方法,但也可以使用原型模式来生成这些类的方法。
  • 原型并不基于继承,但原型需要对被复制对象进行复杂的初始化。工厂方法基于继承,但是它不需要初始化步骤。
  • 抽象工厂生成器原型都可以用单例模式来实现。

实现方式

  1. 原型类必须添加一个以该类对象为参数的构造函数
  2. 声明并实现 clone 方法, clone 方法一般使用 new 关键字调用第一步的构造函数。
  3. 还可以新建一个工厂类来当注册表用。

两种具体实现方式:

  • 深拷贝(完全独立的新对象)
  • 浅拷贝(有数据被修改的风险)

结构型

设计模式-结构型

代理模式(Proxy)

↑ 点击查看详细说明与示例 ↑

介绍

代理模式是一种结构型设计模式,让你能够提供对象的替代品或其占位符。

代理控制着对于原对象的访问,并允许在将请求提交给对象前后进行一些处理。

适用场景

  • 非功能性需求开发(增强代理,比如:监控、统计、鉴权、限流、事务、幂等、日志、缓存等)
  • 本地执行远程服务(远程代理,如 RPC 框架)
  • 访问控制(保护代理)
  • 延迟初始化、智能引用(虚拟代理:如果是重量级对象,可以实现延迟初始化、监控无使用则销毁等)
  • ...

优缺点

优点:

  • 对客户端透明
  • 可以进行生命周期的管理
  • 即使对象还没准备好,代理类也可以工作
  • 开闭原则,可以不对服务和客户端修改的情况下创建新代理

缺点:

  • 代码变复杂
  • 服务响应可能延迟

与其他模式的关系

后续讲到对应的模式时再写

实现方式

  1. 代理类和原始类需要实现相同的接口,如果是无法修改的第三方类可以采用继承的方式。
  2. 创建代理类,其中必须包含一个存储指向服务的引用的成员变量。
  3. 根据需求实现代理方法

以上为静态代理

还有动态代理的实现方式:

  • jdk 动态代理(通过反射实例化代理对象)
  • cglib 动态代理(借助 asm 字节码技术:直接生成新的 .class 字节码文件)
  • Aspectj 动态代理(通过织入的方式修改目标类:编译时织入/编译后织入/加载时织入)
  • instrumentation 动态代理(修改目标类的字节码:类装载的时候动态拦截去修改)
  • ...

桥接模式(Bridge)

↑ 点击查看详细说明与示例 ↑

介绍

将抽象和实现解耦,让它们可以独立变化。

在 GoF 的《设计模式》中,桥接模式是这样定义的:“将抽象和实现解耦,让它们可以独立变化。”

独立的概念可能是:抽象/平台,域/基础设施,前端/后端或接口/实现。

抽象部分(也被称为接口)是一些实体的高阶控制层,该层自身不完成任何具体的工作,它需要将工作委派给实现部分层(也被称为平台)。

这里的抽象和实现是广义上的,而非特指抽象类、实现类。

例子:JDBC 驱动

  • JDBC API 对应 抽象
  • 数据库的 Driver 对应 实现

多维度通过组合使可以独立扩展

很多书籍资料中还有另外一种理解方式: “一个类存在两个(或多个)独立变化的维度,通过组合的方式,让这些维度可以独立进行扩展。”

例子:slf4j

slf4j 其中有三个核心概念,logger,appender 和 encoder。

分别指这个日志记录器负责哪个类的日志,日志打印到哪里以及日志打印的格式。

三个纬度上可以有不同的实现,使用者可以在每一纬度上定义多个实现。

适用场景

  • 运行时切换不同实现方法
  • 从几个独立维度上扩展一个类
  • 拆分或重组一个具有多重功能的庞杂类
  • ...

优缺点

优点:

  • 可以创建与平台无关的类和程序。
  • 客户端代码仅与高层抽象部分进行互动,不会接触到实现平台。
  • 开闭原则:可以独立新增抽象或实现部分。
  • 单一职责:抽象专注于高层逻辑处理,实现专注于实现细节。

缺点:

  • 对高内聚的类使用该模式可能会让代码更加复杂。

与其他模式的关系

  • 可以将抽象工厂模式和桥接搭配使用。
  • 可以结合使用生成器模式和桥接模式:主管类负责抽象工作,各种不同的生成器负责实现工作。

实现方式

  1. 在抽象基类(高阶控制层)中定义客户端的业务需求。
  2. 在抽象类中添加指向实现类型的引用成员变量。
  3. 在通用实现接口(实现平台层)中声明抽象部分所需的业务。
  4. 创建实现类。
  5. 如果高层逻辑有多个变体,则可通过扩展抽象基类为每个变体创建一个精确抽象。
  6. 客户端代码必须将实现对象传递给抽象部分的构造函数才能使其能够相互关联。此后,客户端只需与抽象对象进行交互,无需和实现对象打交道。

装饰器模式(Decorator)

↑ 点击查看详细说明与示例 ↑

介绍

装饰模式是一种结构型设计模式,允许通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能

适用场景

  • 无需修改代码的情况下给原始类添加增强功能。
  • 类继承结构复杂、组合爆炸的情况下。
  • 可以对原始类嵌套使用多个装饰器。

优缺点

优点:

  • 无需创建新子类即可扩展对象的行为。
  • 可以在运行时添加或删除对象的功能。
  • 可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。可以将实现了许多不同行为的一个大类拆分为多个较小的类。

缺点:

  • 在封装器栈中删除特定封装器比较困难。
  • 实现行为不受装饰栈顺序影响的装饰比较困难。
  • 各层的初始化配置代码看上去可能会很糟糕。

与其他模式的关系

  • 装饰模式代理模式有着相似的结构,但是其意图却非常不同。这两个模式的构建都基于组合原则,也就是说一个对象应该将部分工作委派给另一个对象。两者之间的不同之处在于代理通常自行管理其服务对象的生命周期,而** 装饰的生成则总是由客户端进行控制**。

实现方式

  1. 创建一个组件接口并在其中声明通用方法。
  2. 创建一个具体组件类,并定义其基础行为。
  3. 创建装饰基类,使用一个成员变量存储指向被封装对象的引用。该成员变量必须被声明为组件接口类型,从而能在运行时连接具体组件和装饰。装饰基类必须将所有工作委派给被封装的对象。
  4. 装饰器实现类实现组件接口。

适配器模式(Adapter)

↑ 点击查看详细说明与示例 ↑

介绍

适配器模式是一种结构型设计模式,它能使接口不兼容的对象能够相互合作。

适用场景

  • 封装有缺陷的接口设计。
  • 统一多个类的接口设计。
  • 替换依赖的外部系统。
  • 兼容隔离老版本接口。
  • 适配不同格式的数据。
  • ...

优缺点

优点:

  • 单一职责:将转换代码从业务逻辑中剥离。
  • 开闭原则:不修改原有代码的情况下添加适配器。

缺点:

  • 代码整体复杂度增加

与其他模式的关系

  • 桥接模式通常用于将接口与实现的分离,各自独立。另一方面,适配器模式通常在已有程序中使用,让相互不兼容的类能很好地合作。
  • 适配器可以对已有对象的接口进行修改,装饰则能在不改变对象接口的前提下强化对象功能并且支持递归组合。
  • 适配器能为被封装对象提供不同的接口,代理能为对象提供相同的接口,装饰则能为对象提供加强的接口。
  • 外观模式为现有对象定义了一个新接口,适配器则会试图运用已有的接口。适配器通常只封装一个对象,外观通常会作用于整个对象子系统上。

实现方式

有两种实现方式,一种是类适配器(通过继承),一种是对象适配器(通过组合)

  1. 创建遵循客户端接口的适配器类。
  2. 在适配器类中添加一个成员变量用于保存对于服务对象的引用(对象适配器)或者直接继承目标类(类适配器)。
  3. 依次实现适配器类客户端接口的所有方法。适配器会将实际工作委派给服务对象,自身只负责接口或数据格式的转换。
  4. 客户端必须通过客户端接口使用适配器。这样可以在不影响客户端代码的情况下修改或扩展适配器。

如果接口很多,并且适配器与目标接口定义大部分相同,推荐使用类适配器,复用多,代码量少。

如果接口很多,并且适配器与目标接口定义大部分不同,推荐使用对象适配器,组合结构更加灵活。


门面模式(Facade)

↑ 点击查看详细说明与示例 ↑

介绍

门面模式(外观模式)是一种结构型设计模式,能为程序库、框架或其他复杂类提供一个简单的接口。

适用场景

  • 提供一组更简单易用、更高层的接口,隐藏系统的复杂性。
    • 可以解决易用性问题
    • 可以解决多次调用的性能问题
    • 可以解决简单的分布式事务问题
  • ...

优缺点

优点:

  • 代码独立于子系统
  • 接口隔离原则
  • 最少知识原则(迪米特法则)

缺点:

  • 外观可能成为与程序中所有类都耦合的上帝对象

与其他模式的关系

  • 适配器是做接口转换,解决的是原接口和目标接口不匹配的问题。门面模式做接口整合,解决的是多接口调用带来的问题。
  • 只需对客户端隐藏创建过程的话,可以用抽象工厂模式来代替门面模式
  • 一般只要一个门面,可以转换为单例
  • 门面代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。代理与其服务对象遵循同一接口使得自己和服务对象可以互换。

实现方式

  1. 在一个新的外观类中声明并实现该接口。(如果客户端代码没有对子系统进行初始化,也没有对其后续生命周期进行管理,那么外观必须完成此类工作)
  2. 客户端代码仅通过外观来与子系统进行交互。(此后客户端代码将不会受到任何由子系统代码修改而造成的影响)
  3. 如果外观变得过于臃肿,可以考虑将其部分行为抽取为一个新的专用外观类。

组合模式(Composite)

↑ 点击查看详细说明与示例 ↑

介绍

组合模式是一种结构型设计模式,组合模式将一组对象组织(Compose)成树形结构,以表示一种”部分-整体”的层次结构。

组合模式让客户端可以统一单个对象和组合对象的处理逻辑。

适用场景

  • 实现树状对象结构
  • 以相同方式处理简单或复杂的元素
  • ...

优缺点

优点:

  • 可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则:无须修改现有代码即可添加新元素。

缺点:

  • 当差异较大时比较难划分接口。

与其他模式的关系

  • 组合模式通常和责任链模式结合使用。
  • 可以在创建复杂组合树时使用生成器模式,可使其构造步骤以递归的方式运行。
  • 可以使用迭代器模式来遍历组合树。
  • 可以使用访问者模式对整个组合树执行操作。
  • 可以使用享元模式实现组合树的共享叶节点以节省内存。
  • 可以使用原型模式来复制大量使用组合装饰的对象。

实现方式

  1. 声明组件接口及其一系列方法。
  2. 创建一个叶节点类表示简单元素。
  3. 创建一个容器类表示复杂元素。
  4. 在容器中定义添加和删除子元素的方法。

享元模式(Flyweight)

↑ 点击查看详细说明与示例 ↑

介绍

享元模式是一种结构型设计模式。

享元模式顾名思义就是被共享的单元,意图是复用对象节省内存

适用场景

  • 存在大量重复对象(重复状态)且没有足够的内存容量时使用享元模式。
  • ...

优缺点

优点:

  • 节省大量内存。

缺点:

  • 代码复杂度提升。
  • 如果对象有不同的情景数据(外在状态),调用者需要耗费时间来重新计算。

与其他模式的关系

  • 可以使用享元模式实现组合模式树的共享叶节点以节省内存。
  • 如果能将对象的所有共享状态简化为一个享元对象,那么享元就和单例类似了。但这两个模式有两个根本性的不同。
    • 单例只会有一个单例实体。
    • 享元可以有多个实体,各实体的内在状态也可以不同。
    • 单例对象可以是可变的。
    • 享元对象是不可变的。

实现方式

  1. 将享元对象划分为2部分:
    • 内在状态(不变的部分)
    • 外在状态(改变的部分)
  2. 保留类中表示内在状态的成员变量,并将其设置为不可修改
  3. 客户端必须存储和计算外在状态(情景)的数值,因为只有这样才能调用享元对象的方法。
    • 为了使用方便,外在状态和引用享元的成员变量可以移动到单独的情景类中。
  4. 可以创建工厂类来管理享元缓存池,如果使用了工厂,那么客户端就只能通过工厂来获取享元对象。

行为型

设计模式-行为型

观察者模式(Observer)

↑ 点击查看详细说明与示例 ↑

介绍

观察者模式是一种行为型设计模式。

可以用来定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其他对象。

适用场景

  • 当应用中的一些对象必须观察其他对象时,可使用该模式。
  • 当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用该模式。
  • ...

优缺点

优点:

  • 开闭原则:无须修改发布者代码即可引入新的订阅类,反之亦然。
  • 可以在运行时建立对象之间的联系。

缺点:

  • 通知顺序随机。

与其他模式的关系

后面讲到再写。

实现方式

  1. 声明订阅者接口,该接口至少应声明一个 update 方法。
  2. 声明发布者接口,并定义添加和删除订阅对象接口。
  3. 创建具体发布者类,每次发布者发生了重要事件时都必须通知所有的订阅者。
  4. 创建具体订阅者类,实现通知更新的方法。

观察者模式有不同的代码实现方式:

同步阻塞/异步非阻塞的实现方式;

进程内/跨进程的实现方式。

  • 基于消息队列(跨进程):
    • 需要引入一个新的系统(消息队列),增加了维护成本,但被观察者和观察者解耦更加彻底。

模板方法模式(Template Method)

↑ 点击查看详细说明与示例 ↑

介绍

模板方法模式是一种行为设计模式,它在一个超类中定义一个算法骨架,并将某些步骤推迟到子类中实现。

模板方法可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

适用场景

  • 希望客户端扩展某个特定算法步骤
  • 多个类的算法相似时,可将实现步骤提取到超类中。
  • ...

优缺点

优点:

  • 允许重写一个大型算法中的特定部分。
  • 重复代码被提取到一个超类中。

缺点:

  • 受骨架框架限制。
  • 违反里氏替换原则。
  • 步骤越多维护越难。

与其他模式的关系

  • 工厂方法模式是模板方法模式的一种特殊形式。
  • 同时工厂方法也可以作为一个大型模板方法中的一个步骤。

实现方式

  1. 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。
    • 可用final修饰模板方法防止子类重写。
  2. 将步骤设为抽象方法(或者通过报错的方式强制子类实现),或者提供默认实现。
  3. 为每个算法变体新建一个具体子类,子类必须实现抽象步骤,也可以重写默认实现。

回调(Callback)也能起到跟模板模式相同的作用。

同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

  • 回调:注册函数到某个类中。
    • 同步回调:函数返回之前执行注册的函数。
    • 异步回调(延迟回调):函数返回之后执行注册的函数。

策略模式(Strategy)

↑ 点击查看详细说明与示例 ↑

介绍

策略模式是一种行为设计模式。

策略模式定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。使算法的变化独立于使用它们的客户端。

适用场景

  • 想使用对象中各种不同的算法变体,并希望能在运行时切换算法
  • 将类的业务逻辑与其算法实现隔离开。
  • 有许多仅在执行某些行为时略有不同的相似类。
  • 当类中使用了复杂条件运算符以在同一算法的不同变体中切换(冗长的分支判断)。
  • ...

优缺点

优点:

  • 可以在运行时切换对象内的算法。
  • 可以将算法的实现和使用算法的代码隔离开来。
  • 可以使用组合来代替继承。
  • 开闭原则:无须修改上下文即可引入新策略。

缺点:

  • 复杂度增加。
  • 客户端必须知晓策略间的不同。

与其他模式的关系

  • 装饰模式可让你更改对象的外表,策略模式则让你能够改变其本质。
  • 模板方法模式与策略模式:
    • 模板方法基于继承机制:它允许通过扩展子类中的部分内容来改变部分算法。
    • 策略基于组合机制:可以通过对相应行为提供不同的策略来改变对象的部分行为。
    • 模板方法在类层次上运作,因此它是静态的。
    • 策略在对象层次上运作,因此允许在运行时切换行为。
  • 观察者模式是解耦观察者和被观察者。策略模式跟两者类似,它解耦的是策略的定义、创建、使用这三部分。

实现方式

  1. 声明该算法所有变体的通用策略接口
  2. 将算法逐一抽取到各自的类中,它们都必须实现策略接口
  3. 在上下文类中添加一个成员变量用于保存对于策略对象的引用。然后提供设置器以修改该成员变量。
    • 上下文仅可通过策略接口同策略对象进行交互。
    • 如有需要还可定义一个接口来让策略访问其数据。

Java 8 开始支持 lambda 方法, 它可作为一种替代策略模式的简单方式。


职责链模式(Chain of Responsibility)

↑ 点击查看详细说明与示例 ↑

介绍

责任链模式是一种行为设计模式。

责任链将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。

这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

适用场景

  • 需要按顺序执行多个处理者。
  • 需要处理者及其顺序必须在运行时进行改变。
  • 需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知。
  • ...

优缺点

优点:

  • 可以控制请求处理的顺序。
  • 单一职责原则:可以将发起类和操作类解耦。
  • 开闭原则:不改变现有代码的情况下增加处理者。

缺点:

  • 部分请求可能未被处理。

与其他模式的关系

  • 责任链模式可以和组合模式结合使用。
  • 责任链模式装饰模式的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。
    • 责任链的管理者可以相互独立地执行一切操作,可以随时停止传递请求。
    • 装饰可以在遵循基本接口的情况下扩展对象的行为。但无法中断请求的传递。

实现方式

  1. 可以根据处理者接口创建抽象处理者基类(模板方法)。
    • 需要有一个成员变量来存储指向链的下个处理者的引用。
  2. 创建具体处理者子类并实现其处理方法。
    • 是否自行处理这个请求。
    • 是否将该请求沿着链进行传递。
  3. 客户端可以自行组装链或者从其他对象处获得预先组装好的链。

可以使用链表来存储处理器,也可以使用数组来存储处理器。

如果链上的某个处理器能够处理这个请求,可以选择停止传递或者继续传递处理。


状态模式(State)

↑ 点击查看详细说明与示例 ↑

介绍

状态模式是一种行为设计模式

状态模式能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。

有限状态机的概念紧密相关。

状态机由 3 个部分组成:状态事件(转移条件)、动作。事件触发状态的转移及动作的执行。

适用场景

  • 对象需要根据当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更。
  • 某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句。
  • 当相似状态和基于条件的状态机转换中存在许多重复代码。
  • ...

优缺点

优点:

  • 开闭原则。
  • 单一职责原则。
  • 通过消除臃肿的状态机条件语句简化上下文代码。

缺点:

  • 复杂度增加:如果状态机只有很少的几个状态,使用状态模式会很复杂。

与其他模式的关系

  • 状态可被视为策略的扩展
    • 策略模式中的策略则几乎完全不知道其他策略的存在。
    • 状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换。

实现方式

  1. 声明状态接口
  2. 为每个实际状态创建一个继承状态接口的类。
  3. 在上下文类中添加一个状态接口类型的引用成员变量,以及一个用于修改该成员变量值的公有设置器
  4. 为切换上下文状态,你需要创建某个状态类实例并将其传递给上下文
    • 可以在上下文、各种状态或客户端中完成这项工作。无论在何处完成这项工作,该类都将依赖于其所实例化的具体类。
    • 如果状态类中不包含成员变量,则可以使用单例模式来配合使用。

其他实现方法:

分支逻辑法: 直接利用 if 逻辑或者 switch 分支逻辑,直接写状态转移的代码。适合简单直接的状态机。

查表法: 通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。适合状态较多、转移复杂的状态机。


迭代器模式(Iterator)

↑ 点击查看详细说明与示例 ↑

介绍

迭代器模式是一种行为设计模式,

也叫作游标模式(Cursor Design Pattern)

能在不暴露集合底层表现形式(数组、链表、树、图、跳表等)的情况下遍历集合中所有的元素。

适用场景

  • 减少程序中重复的遍历代码。
  • 对客户端隐藏数据结构复杂性。
  • 遍历不同甚至无法预知的数据结构。
  • ...

优缺点

优点:

  • 开闭原则。
  • 单一职责原则。
  • 可以并行遍历同一集合。
  • 可以暂停遍历并在需要时继续。
  • 针对某一数据结构类型可以实现多种不同的迭代方式。

缺点:

  • 可能比直接遍历效率低。
  • 增加复杂度,简单集合遍历用迭代器小题大做。

与其他模式的关系

  • 可以使用迭代器模式来遍历组合模式树。
  • 可以同时使用工厂方法模式迭代器来让子类集合返回不同类型的迭代器,并使得迭代器与集合相匹配。
  • 可以同时使用访问者模式迭代器来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。

实现方式

  1. 声明迭代器接口
    • 至少提供获取下一个元素的方法
  2. 声明集合接口并描述一个获取迭代器的方法
  3. 实现具体迭代器类
    • 必须与单个集合实体组合
  4. 在集合类中实现集合接口,提供创建迭代器的快捷方式。

访问者模式(Visitor)

↑ 点击查看详细说明与示例 ↑

介绍

访问者模式是一种行为设计模式。

它允许一个或者多个操作应用到一组对象上,解耦操作和对象本身

访问者主要目的在于:在单分派的语言中实现双分派的功能 (Java是单分派的)。

适用场景

  • 将对象与操作解耦,将这些业务操作抽离出来。
  • 针对复杂对象结构中的所有元素执行某些操作。
  • 访问者模式可以用来梳理辅助行为的业务逻辑。
  • 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可使用该模式。
  • ...

优缺点

优点:

  • 开闭原则:需要添加新的操作时只需增加新的访问者类。
  • 单一职责原则:可将同一行为的不同版本移到同一个类中。
  • 访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构(例如对象树),并在结构中的每个对象上应用访问者时,这些信息可能会有所帮助。

缺点:

  • 代码复杂度暴增。
  • 每次在元素层次结构中添加或移除一个类时,你都要更新所有访问者
  • 在访问者同某个元素进行交互时,它们可能没有访问元素私有成员变量和方法的必要权限

与其他模式的关系

  • 可以使用访问者对整个组合模式树执行操作。
  • 可以同时使用访问者迭代器模式来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。

实现方式

  1. 在访问者接口中声明一组 “访问” 方法,分别对应每个具体元素类。
  2. 声明元素接口
    • 该方法必须接受访问者对象作为参数。
  3. 在所有具体元素类中实现接收方法
    • 这些方法必须将调用重定向到当前元素对应的访问者对象中的访问者方法上。
  4. 为每个无法在元素层次结构中实现的行为创建一个具体访问者类并实现所有的访问者方法
  5. 客户端必须创建访问者对象并通过 “接收” 方法将其传递给元素。
  • 元素类只能通过访问者接口与访问者进行交互。
  • 访问者必须知晓所有的具体元素类,因为这些类在访问者方法中都被作为参数类型引用。

备忘录模式(Memento)

↑ 点击查看详细说明与示例 ↑

介绍

备忘录模式是一种行为设计模式,

它允许在不暴露对象实现细节(封装原则)的情况下保存和恢复对象之前的状态。

适用场景

  • 需要创建对象状态快照来恢复其之前的状态时。
  • 直接访问对象的成员变量、获取器或设置器会导致封装被破坏时。
  • ...

优缺点

优点:

  • 你可以在不破坏对象封装情况的前提下创建对象状态快照。
  • 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。

缺点:

  • 频繁的创建快照会消耗大量内存。
  • 大部分动态编程语言无法确保备忘录中的状态不被修改。
  • 负责人必须完整跟踪原发器的生命周期才能销毁弃用的备忘录。

与其他模式的关系

  • 有时候原型模式可以作为备忘录的一个简化版本。
  • 可以同时使用备忘录迭代器模式来获取当前迭代器的状态,并且在需要的时候进行回滚。

实现方式

  1. 创建备忘录类,将备忘录类设为不可变。
  2. 在原发器中添加创建备忘录与恢复自身状态的方法。
  3. 创建负责人类,负责人需要知道何时向原发器请求新的备忘录、如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。

可以灵活的各种实现,只要不破坏原有封装即可,如:

  • 基于嵌套类的实现
  • 基于中间接口的实现
  • 封装更加严格的实现

也可以针对备忘录进行优化,如利用**“增量备份”**的方式来节省内存消耗。

实现时要注意深度拷贝的问题。


命令模式(Command)

↑ 点击查看详细说明与示例 ↑

介绍

命令模式是一种行为设计模式,

它可以将请求转换为一个包含与请求相关的所有信息的独立对象

转换让你能根据不同的请求将方法参数化,并且能够支持排队、延迟执行、记录日志、撤销等附加控制功能

适用场景

  • 实现参数化对象。
  • 实现将操作序列化功能。
    • 比如放入队列中或者远程执行操作。
  • 实现操作回滚功能。
    • 如果部分状态私有,可配合备忘录模式进行实现。
    • 备份会占据大量内存,如果可能,可使用反向操作来进行优化。
  • ...

优缺点

优点:

  • 开闭原则。
  • 单一职责原则:可以解耦触发和执行操作的类。
  • 可以实现操作的延迟执行。
  • 可以实现撤销和恢复功能。
  • 可以将一组简单命令组合成一个复杂命令。

缺点:

  • 代码复杂度增加:相当于在发送者和接受者中间多加了一层。

与其他模式的关系

  • 原型模式可用于保存命令的历史记录。
  • 可以同时使用命令备忘录模式来实现 “撤销”。
  • 可以将访问者模式视为命令模式的加强版本,其对象可对不同类的多种对象执行操作。
  • 责任链的管理者可使用命令模式实现。
    • 可以对由请求代表的同一个上下文对象执行许多不同的操作。
    • 也可以是请求自身就是一个命令对象。
  • 命令策略模式看上去很像但它们的意图有非常大的不同。
    • 在命令模式中,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相之间不可替换。
    • 在策略模式中,不同的策略具有相同的目的、不同的实现、互相之间可以替换。
  • 责任链模式、命令模式、中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式
    • 责任链按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理。
    • 命令在发送者和请求者之间建立单向连接。
    • 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
    • 观察者允许接收者动态地订阅或取消接收请求。

实现方式

  1. 声明仅有一个执行方法的命令接口
  2. 抽取请求并使之成为实现命令接口的具体命令类。
    • 每个类都必须有一组成员变量来保存请求参数实际接收者对象的引用。
    • 所有变量的数值都必须通过命令构造函数进行初始化。
  3. 找到担任发送者职责的类。
    • 在这些类中添加保存命令的成员变量。发送者只能通过命令接口与其命令进行交互。
    • 发送者自身通常并不创建命令对象,而是通过客户端代码获取。

解释器模式(Interpreter)

↑ 点击查看详细说明与示例 ↑

介绍

解释器模式是一种行为设计模式。

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

适用场景

  • 构建一个“语言”解释器。
  • 实现编译器、规则引擎、正则表达式等功能。
  • 将一些重复出现的问题用简单的语法来进行表达。
  • 将一个需要解释执行的语言中的句子表示为一个抽象语法树。
  • ...

优缺点

优点:

  • 开闭原则。
  • 单一职责原则。

缺点:

  • 可以利用的场景比较少。
  • 如果文法复杂的话可能较难维护。

与其他模式的关系

  • ...

实现方式

解释器模式代码实现的核心思想就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。

解释器模式的代码实现比较灵活没有固定的模板,在我的示例中的做法大体可以拆分为:

  1. 定义表达式接口。
  2. 实现表达式具体类。
  3. 实现解释器上下文类,作为“启动器”,负责调用表达式进行。
  4. 客户端通过解释器上下文进行调用。

中介模式(Mediator)

↑ 点击查看详细说明与示例 ↑

介绍

中介者模式是一种行为设计模式,能减少对象之间混乱无序的依赖关系。

该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作(中转与协调)。

适用场景

  • 对象之间紧密耦合导致难以对其进行修改时。
  • 当组件因过于依赖其他组件而无法在不同应用中复用时。
  • 为了在不同情景下复用一些基本行为,导致被迫创建大量组件子类时。
  • ...

优缺点

优点:

  • 开闭原则。
  • 单一职责原则。
  • 减轻组件耦合。
  • 方便复用组件。

缺点:

  • 中介者可能会演化成为上帝对象

与其他模式的关系

  • 外观模式中介者的职责类似:它们都尝试在大量紧密耦合的类中组织起合作。
    • 外观为子系统中的所有对象定义了一个简单接口,但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。
    • 中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象,无法直接相互交流。
  • 责任链模式、命令模式、中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式
    • 命令在发送者和请求者之间建立单向连接。
    • 观察者允许接收者动态地订阅或取消接收请求。
    • 责任链按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理。
    • 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
  • 中介者观察者之间的区别
    • 中介者的主要目标是消除一系列系统组件之间的相互依赖。这些组件将依赖于同一个中介者对象。
    • 观察者的目标是在对象之间建立动态的单向连接,使得部分对象可作为其他对象的附属发挥作用。
    • 中介者的应用场景中,参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。
    • 观察者的应用场景中,参与者之间的交互比较有条理,一般都是单向的。
    • 更进一步、也可以让中介者负责对象的创建与销毁,这样的话中介者可能会与工厂/外观类似。

实现方式

  1. 声明中介者接口并描述中介者和各种组件之间所需的交流接口。
  2. 实现具体中介者类。该类可从自行保存其下所有组件的引用中受益。
  3. 组件必须保存对于中介者对象的引用。
  4. 修改组件代码使其可调用中介者的通知方法。

总结篇

创建型设计模式主要解决对象创建问题,

结构型设计模式主要解决类或对象的组合或组装问题,

行为型设计模式主要解决的就是类或对象之间的交互问题。


实际上,设计模式要干的事情就是解耦,

创建型模式是将创建和使用代码解耦 ,

结构型模式是将不同功能代码解耦,

行为型模式是将不同行为代码解耦。

如何避免过度设计

  • 设计的初衷是提高代码质量。
  • 设计的过程是先有问题后有方案。
  • 设计的应用场景是复杂代码。
  • 持续重构能有效避免过度设计。

如何避免设计不足

  • 理论知识储备。
  • 进行刻意训练。
  • 要有代码质量意识、设计意识。
  • 不要脱离具体的场景去谈设计。

最后

设计原则和思想比设计模式更加普适和重要。

掌握了代码的设计原则和思想,我们能更清楚的了解为什么要用某种设计模式,就能更恰到好处地应用设计模式。

同时遵循 KISS 原则,怎么简单怎么来,就是最好的设计。

空文件

简介

设计模式的文章与示例及实例(从 github 备份过来,避免重大打击 /滑稽 ) 展开 收起
Java
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/NIbbbbbbbb/Design-Patterns.git
git@gitee.com:NIbbbbbbbb/Design-Patterns.git
NIbbbbbbbb
Design-Patterns
Design-Patterns
master

搜索帮助

53164aa7 5694891 3bd8fe86 5694891