Skip to content

tbfungeek/Design-Patterns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

Design Pattern 设计模式

六大设计原则

1. 开闭原则:对扩展开放,对修改关闭

也就是说在增加新特性的时候最好考虑通过扩展已有系统的方式来实现需求的变化,而不是通过直接通过修改系统已有源码的方式。也就是说一旦程序开发完成,程序中一个类的实现只应该因为错误而修改,因为一个已有的系统一般都是事先经过千万次测试的稳定系统,如果直接对这些稳定系统进行修改,很有可能直接影响到原有系统的稳定性。所以如果要添加新的或者改变的特性应该通过新建不同的类的实现,新建的类可以通过继承的方式来重用已有的代码。

2. 里氏替换原则:所有父类能够出现的地方,子类都应该可以出现

用在实际就是在类中调用其他类的时候尽可能使用父类或者接口。里氏替换原则的核心思想就是通过抽象建立规范,具体的实现在运行时替换掉抽象,从而实现功能扩展。

3. 依赖倒置原则:模块之间的依赖关系需要通过接口或者抽象类来产生的

每个类都应该有接口或者抽象类,或者两者都存在,变量的类型尽量是接口或者是抽象类一般使用接口定义公开的属性和方法,抽象类负责准确地实现业务逻辑。

4.接口隔离原则: 类之间的依赖关系应该建立在最小的接口上

类之间的依赖关系应该建立在最小的接口上,在项目中需要按照职责将臃肿的接口拆封成更小的和更具体的接口,这样客户只需要知道他们感兴趣的方法,把不需要的接口剔除掉,最好做到一个接口只服务于一个子模块或者业务逻辑。

5.迪米特法则:一个类应该对自己需要耦合或者调用的类知道得最少

6.单一职责原则:接口或者类,尽量要做到单一职责 类的设计尽量要做到只有一个原因引起变化,而对于方法则需要满足一个方法尽可能只做一件事情。

创建型模式

  • 工厂方法模式(Factory Method)工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码:

工厂方法从顶层看是通过特殊的工厂方法创建对象来替代直接通过调用new运算符创建,底层实质还是通过new运算符创建,只是将这部分放在底层罢了。 这些工厂生产出来的产品必须遵循共同的基类或者接口。也即是说工厂方法并不是能够返回任意对象,而是一类具有同一特征的产品。

比如下面的例子直接使用new来创建,那么每个节点的实例是固定的,如果需要改变节点返回的对象必须修改全部节点,这样对于上层应用来说需要改动的点就比较多。

使用工厂模式后大体的结构如下所示,这个和上面的区别是每Transport结点不再是单一的运输工具,而是可以随意更换的实体结点。如果需要更换节点不需要改变顶层代码只需要改变Transport结点内部生产传输工具的规则即可,这样上层很容易做到对这些结点的更换。

总结

工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。

  • 结构图:

在使用工厂方法进行重构的时候,首先需要将各个节点对象特征进行抽象,将其抽象成一个约束接口。这个接口作为工厂生产方法的返回出口。

  • 抽象工厂模式(Abstract Factory)创建一系列相关的对象, 而无需指定其具体类:

抽象工厂其实从名字上并不能传达出它的实际功能,光看名字很难了解它的实际用途,确实它是一个工厂,但是这个工厂的作用是用于生产一套套方案的,而不是一个个物品。这种问题一般是一个二维的矩阵,一个是物体,一个是特性。见下面例子:

1. 一个萝卜一个坑,大萝卜大坑,小萝卜小坑。那么萝卜和坑是物体,大和小是特性。

2. Window系统下的UI控件,和Linux系统下的UI控件. 这里各个控件是物体,Window风格和Linux风格是特性。

3. 现代Modern风格的家具,维多利亚Victorian风格的家具,装饰风艺术Art­Deco风格的家具,这里家具是物体,风格是特征。

在抽象工厂中,每个抽象工厂生产的是物体,而不同抽象工厂生产的是不同特征的物体。

  • 创建者模式(Builder): 能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象

创建者模式主要用在两种场景:

  1. 通过将整体构建过程和具体部件的构建过程分离,达到使用同一个构建过程创建出不同表示的目的。
  2. 创建目标对象步骤十分灵活繁琐,并且有些情况需要设置某些参数,有些则不需要。

创建者模式的主要变化集中在Director中。而Builder只负责生成产品对象并按照Director的指导构建产品,最后通过Builder执行交付产品工作。Director负责根据不同的类型调用Builder执行不同的产品构建过程。

也就是说应用层只负责将建造者交给指导者,指导者知道怎么创建一个产品,但是具体的产品生产过程是通过建造者来完成的。创建步骤在Director中,但是每个环节怎么实现是放在Builder中。要创建不同的对象只需要,创建不同的Builder交给Director,由Director指导Builder一步一步完成产品生成任务,最后按照Builder -> Director -> 应用层 这样层次结构逐级交付。

  • 原型模式(Prototype):能够复制已有对象, 而又无需使代码依赖它们所属的类

原型模式一般用在需要快速创建大量相似的对象的情景下。它与用new 创建对象的区别在于:原型模式是一种内存二进制拷贝,所以比使用new产生对象的方式来得快速高效。所以在需要重复地创建相似对象时可以考虑使用原型模式。比如需要在一个循环内创建对象,并且对象创建过程比较复杂或者循环次数很多的情况,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多。

原型模式的另一个优点在于它可以将组成当前对象的内部依赖以及内部私有成员的都封装起来。原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下,这样的接口中仅包含一个克隆方法。比如我们有一个数组,放的是各种形状,但是我们不用具体知道每个形状的类型,只知道它们都是形状,我们要复制出一批相同的数组,这时候只要在每个对象上调用copy方法,即可完成任务,具体不同形状复制的细节上层不用在意,上层也不用引入不同形状的依赖,这些都封装在各个形状的内部。通过这种方式我们在复制的过程不用在意具体的形状的区别,只有在使用的时候才需要关注它到底是哪个形状。

为了完善原型模式可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索。搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。 找到合适的原型后,注册表应对原型进行克隆,并将复制生成的对象返回给客户端。最后将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。

  • 单例模式(Singleton):让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点

对于单例模式,下面图比较形象:

它的特点如下:

  1. 全局只有一个实例

    如果创建某个对象将会耗费过多的资源,或者需要某个对象作为某种设备或者其他对象的管理者的时候,比如对项目配置文件或者数据库进行读写的情况。这种情况下单例模式比较适用。

  2. 整个应用生命周期都存在

  3. 提供一个全局共享访问节点 在应用的不同位置都可以访问单例对象

创建单例一般有如下两个步骤:

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

结构型模式

  • 适配器模式(Adapter): 使接口不兼容的对象能够相互合作

适配器主要用于连接两个交互接口存在差异的情况,比如我们重构一个旧系统,旧的接口数据结构和新接口数据结构存在差异的时候可以使用适配器来兼容两个接口。比如我们在对接多个数据源到同一个数据分析系统,这个数据分析系统目前只接受json格式的数据形式,而不同的数据源格式各种各样,有XML,Probuf,纯文本,数据库...这种情况下可以在数据分析系统与各个数据源数据交互的部分使用一个适配器对数据进行转接。更有甚者可以使用双向适配器来兼容不同的接口。

适配器模式首先需要定义一个客户端需要的接口,一般成为target接口,适配器实现这个接口,然后通过任何方式,往Adapter中注入Adapee,这个就是需要改造的旧接口。在Adapter的target接口方法中使用Adapee来实现对应的目标接口功能。

  • 外观模式(Facade):为程序库、 框架或其他复杂系统提供一个简单的外部接口

外观类为包含许多活动部件的复杂子系统提供面向客户端的 简单,功能有限 的接口。也就是说外观类的重点在于以简单的方式提供用户关心的,而不是大而全的接口。

它的主要目的就是让外部减少与子系统内部多个模块的交互,从而让外部能够更简单得使用子系统,如果你的程序需要与包含几十种功能的复杂库整合, 但只需使用其中非常少的功能, 那么使用外观模式会非常方便。

外观类负责把客户端的请求转发给子系统内部的各个模块进行处理。在使用外观模式的时候建议所有客户端代码仅通过外观来与子系统进行交互。外界不能直接调用子系统接口

Facade 的方法本身并不进行功能的处理,只是实现一个功能的组合调用。并且一般不会在外观类中定义一些子系统没有的功能,它主要负责组合已有功能来实现客户端的需求,而不是添加新的功能实现。

一般系统只需要一个外观类,所以可以使用单例或者私有化构造方法,并将其他方法声明为静态方法。

外观模式的好处有如下几点:

  1. 由于Facade类封装了各个模块交互的过程,如果今后内部模块调用关系发生了变化,只需要修改Facade实现就可以了。
  2. 如果没有外观类,客户端需要和子系统内部多个模块交互,客户端和这些模块之间都有依赖关系,任何一个模块的变动都可能会引起客户端的变动。使用外观类之后客户端就不需要去关心系统内部模块的变动情况了,客户端只需要和这个外部类有依赖关系,这样当系统内部多个模块发生变化的时候,这个变化可以被这个外观类吸收和消化,并不影响到客户端。
  • 代理模式(Proxy): 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理

代理模式通过创建一个代理对象来代替真实的对象,在客户端看来代理对象和真实对象并没有两样。实际的工作还是通过代理对象传到真实对象中。代理模式通过新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。

如果需要在类的主要业务逻辑前后执行一些工作(比如鉴权,添加Log),我们无需修改类的实现就能完成这项工作

  1. 服务接口(Service Interface) 声明了服务接口。 代理必须遵循该接口才能伪装成服务对象。
  2. 服务(Service) 类提供了一些实用的业务逻辑。
  3. 代理(Proxy) 类包含一个指向服务对象的引用成员变量。 代理完成其任务 (例如延迟初始化、 记录日志、 访问控制和缓存等) 后会将请求传递给服务对象。 通常情况下, 代理会对其服务对象的整个生命周期进行管理。
  4. 客户端 (Client) 能通过同一接口与服务或代理进行交互, 所以你可在一切需要服务对象的代码中使用代理。

注入服务可以通过初始化代理对象的构造函数中注入,也可以通过在调用某个方法的时候,将服务对象作为参数传入。如果我们需要对一个已有的系统进行重构,这时候往往没有现成的服务接口,这时候从服务类中抽取接口并不总是可行的,因为这样做的话,需要对服务的所有客户端进行修改, 让它们使用接口。这样往往改动很大,这时候可以将代理作为服务类的子类,从服务类中继承所有的接口。

总而言之代理和服务的结合可以通过构造函数注入,调用方法作为参数注入,让代理继承服务类这三种方案来实现。

可以考虑新建一个构建方法来判断客户端可获取的是代理还是实际服务。 也可以在代理类中创建一个简单的静态方法, 也可以创建一个完整的工厂方法。可以考虑为服务对象实现延迟初始化。

代理的作用场景:

  1. 延迟初始化(虚拟代理)。 如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。你无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。

  2. 访问控制 (保护代理)。 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。代理可仅在客户端凭据满足要求时将请求传递给服务对象。

  3. 记录日志请求 (日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。

  4. 缓存请求结果 (缓存代理)。 适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。代理可对重复请求所需的相同结果进行缓存, 还可使用请求参数作为索引缓存的键值。

  5. 本地执行远程服务 (远程代理)。 适用于服务对象位于远程服务器上的情形。在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。

  6. 智能引用。 可在没有客户端使用某个重量级对象时立即销毁该对象。代理会将所有获取了指向服务对象或其结果的客户端记录在案。 代理会时不时地遍历各个客户端, 检查它们是否仍在运行。 如果相应的客户端列表为空, 代理就会销毁该服务对象, 释放底层系统资源。

  • 装饰模式(Decorator): 通过将对象放入包含行为的特殊封装对象中来为原对象动态绑定新的行为

装饰器在行为上和代理模式有点类似,都是通过实现同一个接口,然后通过往外部包裹类中注入一个类用来承载实际的操作,在外部包裹类中可以针对需要做一些额外的处理。

装饰器模式通过一个对象包含指向另一个对象的引用,并将部分工作委派给引用对象,一个对象可以包含多个指向其他对象的引用,这些指向关系可以在运行时动态更改,而继承则是通过对象继承父类的行为, 它们自己能够完成这些工作,不需要引用对象。所以它是静态的。装饰器实现了与其被装饰对象相同的接口。因此从客户端的角度来看,这些对象是完全一样的。 装饰器中的引用成员变量可以是遵循相同接口的任意对象。 这使得我们可以将一个对象放入多个装饰器中,并在对象中添加所有这些装饰器的组合行为。

使用场景:

  1. 希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。
  2. 如果用继承来扩展对象行为的方案难以实现或者根本不可行(被final最终关键字限制对某个类的进一步扩展),这种情况可以使用该模式。
  • 桥接模式(Bridge): 桥接模式适合于存在两个维度变化的情形,它将两个维度的变化独立开来,通过组合的方式来解决这个问题

桥接模式将两个维度变化的情形独立区分开来,在使用的时候通过组合的方式来实现各种情况。在桥接模式中称两个独立的部分为抽象部分和实现部分。客户端面向抽象部分,抽象部分持有实现部分的引用。

两者职责的划分:

  • 抽象对象里面的方法,会使用抽象部分接口中的方法。也可能需要调用实现部分对象来完成。这个对象中的方法通常都是和具体的业务相关的方法。
  • 实现部分的接口,提供基本实体操作,抽象部分定义的一般都是基于这些基本操作的业务方法。

可以通过上图来理解上面提到的概念,这里有两个纬度,一个是应用的版本,一个是应用的应用平台。版本可以有v1.0, v2.0, v3.0 ... 应用平台可以分成Windows,Linux,Mac OS,Android,iOS ... 我们可以通过一个v3.0的应用版本注入iOS 对应功能的API来发布一个在iOS系统上运行的v3.0应用版本。

如何在抽象对象中注入实际对象

  1. 由客户端负责创建,并在创建抽象部分接口的时候作为参数设置到抽象部分的对象中去。
  2. 可以在抽象部分对象构建的时候,由抽象部分的对象自己来创建对应的现实部分的对象。
  3. 可以使用抽象工厂或者简单工厂来选择并创建具体的实现部分。
  4. 可以在抽象部分选择并创建一个默认的实现部分,子类可以根据具体的需要改变这个实现。

何时使用桥接模式

不希望抽象和实现部分采用固定的绑定关系的时候,可以采用桥接模式把抽象和实现部分分开,然后在程序运行期间动态得设置抽象部分需要用到的具体实现,还可以动态地切换具体的实现。 如果希望实现部分的修改不会对客户端产生影响,可以采用桥接模式,由于客户是面向抽象的接口在运行,实现部分的修改可以独立于抽象部分,而不会对客户端产生影响。

简而言之:如果你想要拆分或重组一个具有多重功能的庞杂类,或者在几个独立维度上扩展一个类,或者在运行时切换不同实现方法的情况下都可以使用桥接模式。

如果把抽象部分称为业务抽象部分感觉会更好理解,抽象部分负责业务的抽象,具体某部分业务中的实现细节差异部分放在实现部分。

  • 组合模式(Composite): 可以使用它将对象组合成树状结构,并且能像在顶层无差别地使用组合对象和叶子对象

组合模式的最大作用在于组合对象和叶子对象都遵循同一个接口,所以客户端可以无差别对待组合对象和叶子对象,从而可以在顶层使用通用的处理方式来操作,如果顶层是组合对象会通过对应的途径交付给其叶子结点来计算。最终以和叶子对象相同的方式交付给顶层,所以顶层可以无差别地看待组合对象和叶子对象。

  • 享元模式(Flyweight): 摒弃了在每个对象中保存所有数据的方式,而是通过共享多个对象所共有的相同状态,从而在有限的内存中载入更多对象

正常情况下我们创建一个对象在内存中保留一个对象的数据,在对象占用内存很小,或者对象数量不是很多的情况下没什么问题,但是在每个对象都很庞大,并且数量很多的时候,我们就需要考虑是否可以通过享元模式来解决这个问题。享元模式的核心思想是从这些大量的数据中找出重复的,可复用的部分,通过共享来减少内存空间的使用。从而能在有限的内存容量中载入更多对象。

举个例子来说明下,比如我们需要开发一款游戏,我们每次都会随机出现恐龙,野人,怪兽这三类物体,然后我们点击屏幕都会射出子弹来射击这些目标,这种情况我们点击快的时候,或者出现大量恐龙,野人,怪兽的时候就会很卡。内存会严重飙升。这种情况怎么优化?如果用享元模式的化,可以考虑将能够抽取的公共数据给抽取出来一类物体共享。比如这些物体的坐标,速度,行走的方向这些是每个物体独有的,是不能共享的。在享元模式中称为外部状态,那些可以决定一类物体特性的数据,比如恐龙的特征模型,野人的特征模型,怪兽的特征模型,子弹的特征模型,这些数据在一个类的物体间可以共享。称为内部状态也叫享元。不论多少个怪物都只有一份数据。每个怪物都由内部状态和外部状态两部分组成,一个怪物特性数据,一个怪物独有数据。

为了能更方便地访问各种享元, 你可以创建一个工厂方法来管理已有享元对象的缓存池。 工厂方法从客户端处接收目标享元对象的内在状态作为参数, 如果它能在缓存池中找到所需享元, 则将其返回给客户端; 如果没有找到, 它就会新建一个享元, 并将其添加到缓存池中。

简单概括一下:享元模式适合于如下场景:

  • 需要生成数量巨大的相似对象
  • 对象中包含可抽取且能在多个对象间共享的重复状态。
  • 使用享元模式核心在于找到内在状态和外在状态
  1. 内在状态:包含不变的,可在许多对象中重复使用的数据的成员变量。
  2. 外在状态:包含每个对象各自不同的情景数据的成员变量
  • 保留类中表示内在状态的成员变量,并将其属性设置为不可修改.这些变量仅可在构造函数中获得初始数值。
  • 找到所有使用外在状态成员变量的方法,为在方法中所用的每个成员变量新建一个参数,并使用该参数代替成员变量。
  • 可以有选择地创建工厂类来管理享元缓存池,它负责在新建享元时检查已有的享元。如果选择使用工厂,客户端就只能通过工厂来请求享元,它们需要将享元的内在状态作为参数传递给工厂。
  • 客户端必须存储和计算外在状态的数值, 因为只有这样才能调用享元对象的方法。为了使用方便,外在状态和引用享元的成员变量可以移动到单独的情景类中。

行为型模式

  • 责任链模式(Chain of Responsibility):允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者

责任链主要解决的问题是那些处理环节经常变动的情景,责任链将整个问题分成请求或者称为待处理问题和责任链或者称为处理环节这两大部分。请求无需关注责任链的细节,只要描述好待处理的问题即可,而责任链则主要关注为解决这个问题它要怎么去组织整个链的结点。

每次客户端发出一个请求,很多对象都有机会来处理这个请求,我们就可以将这些能够处理这个请求的对象组成一个责任链,客户发出的请求可以顺着这个责任链传递。

这个责任链是可以动态变化的,也就是客户端请求的处理流程是可以变化的。责任链中的各个处理请求的对象时可以替换的。

在标准的责任链模式中,只要有对象处理了请求,这个请求就不再被处理和传递了。但是也有种变体:每个责任链的对象都对这个请求进行一定功能处理,而不是被处理后就停止,这种变体一般称为功能链。

责任链的组织方式:

  1. 在客户端组合责任链,这种称为外部链。
  2. 在Handler类中实现链的组合,各个职责对象自己决定后续的处理对象,这种称为内部链。

每个对象都会按照条件来判断是否属于自己处理的范围,如果是就处理,如果不是就转发请求给下个对象,如果传到最后还是没有被处理就传给默认的处理对象对其进行处理。

在责任链模式中,请求者和接收者之间是一种松散耦合的关系。请求者并不知道接收者是谁,也不知道具体将会如何处理,请求者只是负责向责任链发出请求,而每个处理对象也不管请求者或者是其他的职责对象,只负责处理自己的部分,其他就交给其他的处理对象来处理。

  • 观察者模式(Observer): 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象

观察者模式是一种一对多的关系,多个对象监听某个目标对象的事件,当目标对象的事件发生的时候,所有监听这个事件的对象都将得到通知,并执行对应的方法。这种模型会把目标对象自身通过方法传递给观察者,这样观察者就可以通过这个引用来获取了,还有一种并不会直接将整个对象传递过去而只是传递部分需要的数据过去。

在观察者模式中数据传递有两种方式:

  1. 推模型: 目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息是对象的全部或者部分数据。
  2. 拉模型: 目标对象在通知观察者的时候,只传少量信息,如果观察者需要具体的信息,由观察者主动到目标对象中获取,相当于是观察者从目标对象中拉数据。

  • 模板方法模式(Template Method):在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤

模板方法是通过定义一个算法骨架模版,而将算法中的步骤延迟到子类,这样子类就可以覆写这些步骤的实现来实现算法之间的差异。也就是说模版方法中算法的步骤是固定的,但是算法的关键结点是可变的,子类通过覆写对应的步骤方法,即可重用算法的步骤。

从实现角度来看:模板方法主要通过制定模板,把算法步骤固定下来。至于谁来实现,模板可以自己提供,也可以子类去实现,还可以通过回调让其他类来实现。

在模板里面包含如下几类方法:

  1. 模板方法:用于定义算法的框架。
  2. 具体方法:在模板中直接实现某些步骤的方法,通常这些步骤的实现算法是固定的,而且是不怎么变化的,因此可以将其当做公共功能实现在模板中
  3. 抽象方法,在模板中定义的抽象操作,通常是模板方法需要调用的操作,而且这些操作在父类中还没有办法确定下来。需要子类来实现真正的方法。
  4. 回调方法:通过回调机制来调用其他类中的方法。
  5. 工厂方法,在模板方法中如果需要得到某些对象的实例的话,可以考虑通过工厂方法来获取,把具体的创建对象延迟到子类中去。
  • 访问者模式(Visitor):元素将数据交给visitor处理后返回

如果要给某个已有的对象在运行时时期添加某个方法可以借助访问者模式进行实现,将新行为放入一个名为访问者的独立类中, 而不是试图将其整合到已有类中。现在,需要执行操作的原始对象将作为参数被传递给访问者中的方法,让方法能访问对象所包含的一切必要数据。如果需要该操作能在不同类的对象上执行的话可以在访问者类定义一组访问方法。且每个方法可接收不同类型的参数。需要注意的是这里是在运行时动态添加的而不是像装饰器那样静态添加。访问者模式实现的时候需要记住一句话:“被访问者接受(accept)访问者(visitor)访问自己“ 由于访问者拿到了被访问者所以就可以在访问者内部调用被访问者方法的方法实现对应的访问方法。而访问者里面声明的是针对每个被访问者的一系列的访问方法。

最关键的是被访问者的accept方法,以及访问者的visit方法。

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

你可能会遇到访问者需要访问元素类的部分私有成员变量的情况。 在这种情况下,你要么将这些变量或方法设为公有, 这将破坏元素的封装; 要么将访问者类嵌入到元素类中。 后一种方式只有在支持嵌套类的编程语言中才可能实现。

客户端必须创建访问者对象并通过 “接收” 方法将其传递给元素。

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

状态模式建议为对象的每种可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。

  • 原始对象被称为上下文 (context), 它并不会自行实现所有行为, 而是会保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象。如需将上下文转换为另外一种状态, 则需将当前活动的状态对象替换为另外一个代表新状态的对象。上下文通过状态接口与状态对象交互, 且会提供一个设置器用于传递新的状态对象。

  • 所有状态类都必须遵循同样的接口,而且上下文必须仅通过接口与这些对象进行交互。特定状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换。为了避免多个状态中包含相似代码, 可以提供一个封装有部分通用行为的中间抽象类。状态对象可存储对于上下文对象的反向引用。 状态可以通过该引用从上下文处获取所需信息, 并且能触发状态转移。

  • 上下文和具体状态都可以设置上下文的下个状态, 并可通过替换连接到上下文的状态对象来完成实际的状态转换。

  • 状态模式的实现过程
  • 确定哪些类是上下文。
  • 声明状态接口。虽然你可能会需要完全复制上下文中声明的所有方法,但最好是仅把关注点放在那些可能包含特定于状态的行为的方法上。
  • 为每个实际状态创建一个继承于状态接口的类。然后检查上下文中的方法并将与特定状态相关的所有代码抽取到新建的类中。
  • 检查上下文中的方法, 将空的条件语句替换为相应的状态对象方法。

为切换上下文状态, 你需要创建某个状态类实例并将其传递给上下文。 你可以在上下文各种状态或客户端中完成这项工作。 无论在何处完成这项工作,该类都将依赖于其所实例化的具体类。

  • 策略模式(Strategy):通过定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够在运行时动态切换

策略模式就是把一系列的算法从具体的业务处理中独立封装起来,把它们实现成为独立的算法类,使得他们可以相互替换,通过这种方式来实现客户和算法之间的解耦。它的具体思路是通过一个持有算法的上下文对象,来封装一系列算法,这个对象并不负责决定具体选用哪个算法,实际上, 上下文并不十分了解策略, 它会通过同样的通用接口与所有策略进行交互, 而该接口只需暴露一个方法来触发所选策略中封装的算法即可, 而把算法选择的工作交给了客户,客户选择好具体的算法后设置到上下文对象中让上下文对象持有客户选择的算法,当客户通知上下文对象执行功能的时候,上下文对象则转调具体的算法,这样具体的算法和直接使用算法的客户就能够实现分离了。

在策略模式中上下文对象就是各个策略算法的公共类,可以在这里面存放各个算法所公用的数据以及所需要的公共方法。 公共方法这里有三种组织方式:

  • 在上下文当中实现公共功能,让所有具体的策略算法回调这些方法。
  • 为所有的策略算法定义一个抽象父类,让这个父类去实现策略的接口,然后在这个父类中实现公共的功能。

  • 中介者模式(Mediator): 中介者模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作,从而减少对象之间混乱无序的依赖关系

中介者模式限制组件之间的直接交互并使其相互独立。这些组件必须调用特殊的中介者对象,通过中介者对象重定向调用行为,以间接的方式进行合作。最终,组件仅依赖于一个中介者类,无需与多个其他组件相耦合。这样其他对象就不需要维护对象之间的关系了。扩展关系的时候也只需要扩展或者修改中介者对象就可以了。

在中介者模式中,需要交互的对象称为同事类。它们一般都持有中介者对象的引用。但是也可以将中介者设计为一个单例,在同事类需要中介者引用的时候通过getInstance获取中介者引用。 同时由于中介者需要维护同事对象之间的关系所以也应该拥有各个同事类的引用。但是也可以在中介者处理方法中通过创建,或者获取,或者通过参数传入的方式获取同事对象。 在中介者模式中一旦一个同事发生了变化,需要主动通知中介者,让中介者去处理该变化与其他同事对象的交互。

  • 中介者实现过程中可优化的地方

主要可以考虑如何在中介者和同事对象的持有关系上进行优化,让同事对象不持有中介者,而是在需要的时候直接获取中介者对象,中介者也不再持有同事对象,在中介方法中通过创建,获取,参数传递的形式得到同事对象的引用

  • 命令模式(Command):实现命令的发起对象(命令触发器)和命令的具体实现对象(命令接收者)的完全解耦

在命令模式中涉及到三类关键对象:

  1. 命令类:在标准的命令模式里面,命令实现类没有真正实现命令要求的功能的能力,它相当于一个封装有命令接受者和命令执行参数的一个包裹对象。真正执行命令的功能是接收者。
  2. 接收者:在命令模式中接收者可以是任意的类,它是持有真正执行方法的类,它可以处理多个命令,接收者提供的方法个数,名称功能和命令中的可以不一样,只要能够通过调用接收者的方法来实现命令对应的功能就可以了。
  3. 命令触发器:命令出发器是持有命令的对象,它只负责触发命令,但是请求究竟由谁处理,如何处理,发起请求的对象是不知道的,也就是发起请求的对象和真正实现的对象是解偶的,发起请求的对象只管发出命令,其他的就不管了。

命令模式的优点是实现了命令的发起对象(命令触发器)和命令的具体实现对象(命令接收者)的完全解耦,这样很方便扩展新的命令,只要实现新的命令对象,然后在装配的时候,把具体实现对象设置到命令中,就可以使用这个命令对象了,不用改变已有的实现。

整个命令模式,在实现过程中,会将命令执行者,也就是命令接收器以及执行命令所需要的参数封装到一个命令类中。然后再将这个命令类丢到命令触发器中,客户端会持有命令触发器在适合的时刻触发这个命令触发器。

  • 备忘录模式(Memento):允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

我们在需要生成一个对象快照的时候往往会遇到下面两种情况:要么通过暴露类的所有内部细节的方式类外部创建当前对象的快照,但是这种方式过于脆弱,绝大部分对象会使用私有成员变量来存储重要数据,这样别人就无法轻易查看其中的内容;要么会限制对其状态的访问权限,这种方式往往在外部由于无法访问这些对象的内容而无法生成快照。但是这两种情况考虑的都是通过外部生成某个类的快照的方式,这种会导致封装 “破损” ,一些对象试图超出其职责范围的工作。 由于在执行某些行为时需要获取数据, 所以它们侵入了其他对象的私有空间,而不是让这些对象自己来完成实际的工作。

备忘录模式将创建状态快照的工作委派给实际状态的拥有者Originator对象。这样其他对象就不再需要从 “外部” 复制编辑器状态了,编辑器类拥有其状态的完全访问权,因此可以自行生成快照。模式建议将对象状态的副本存储在一个名为备忘录(Memento)的特殊对象中。 除了创建备忘录的对象外,任何对象都不能访问备忘录的内容。 其他对象必须使用受限接口与备忘录进行交互,它们可以获取快照的元数据,但不能获取快照中原始对象的状态。

同时备忘录模式中允许你将备忘录保存在通常被称为负责人 (Caretakers) 的对象中。 由于负责人仅通过受限接口与备忘录互动, 故其无法修改存储在备忘录内部的状态。 同时, 原发器拥有对备忘录所有成员的访问权限, 从而能随时恢复其以前的状态。

  • 迭代器模式(Iterator):让你能在不暴露集合底层表现形式(列表、 栈和树等)的情况下遍历集合中所有的元素

迭代器的关键点在于把对聚合对象的遍历和访问的功能从聚合对象中分离出来,可以在访问一个聚合对象的内容的时候,无须暴露出该聚合对象的内部表示,从而提高聚合对象的封装性,这样不但简化了聚合对象,并且可以让迭代器和聚合对象可以独立变化和发展,从而大大加强系统的灵活性。

迭代器可以用在如下场景:

  1. 不想暴露所要访问的聚合对象的内容。 当集合背后为复杂的数据结构, 出于使用便利性或安全性的考虑,希望对客户端隐藏其复杂性时,可以使用迭代器模式。

  2. 希望为遍历不同的对象提供一个统一的接口。 迭代器模式把聚合对象和访问聚合的机制实现了分离,通过这种模式可以在迭代器上实现不同的迭代策略。

在迭代器模式中会将实际的可遍历对象封装在一个CollectionWrapper的对象,这些对象遵循相同的集合包裹接口,这样在上层看来包裹着不同可遍历对象的CollectionWrapper是一致的,再在创建迭代器的时候使用CollectionWrapper传递给迭代器,这样在迭代器看来可以通过相同的方式操作可遍历对象。所以在上层屏蔽了底层实现的差异,并且不暴露底层实现的容器细节。

  • 解释器模式(Interpreter):

前端常见架构模式

以往的总结内容

推荐书籍:

About

设计模式

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published