23种设计模式学习笔记

23种设计模式学习笔记

  • 前言
  • 创建型模式(五种)
    • 1:Abstract Factory 抽象工厂(Kit:工具箱)
    • 2:Builder 生成器
    • 3:Factory Method工厂方法(虚拟构造器:virtual constructor)——对象创建型模式
    • 4:Prototype原型模式
    • 5:Singleton单件模式
  • 结构型模式(七种)
    • 1: Adapter适配器模式(wrapper包装器)
    • 2: Bridge桥接模式(Handle/Body)
    • 3: Composite组合模式
    • 4: Decorator装饰器模式(wrapper:包装器)
    • 5: Facade外观模式
    • 6: Flyweight享元模式
  • 行为型模式(十一种)
    • 1:Chain of Responsibility责任链模式
    • 2:Command命令模式
    • 3:Interpreter解释器模式
    • 4:Iterator迭代器模式(cursor:游标)
    • 5:Mediator中介者模式
    • 6:Memento备忘录模式
    • 7:Observer观察者模式(dependent:依赖,publish-subscribe:发布-订阅)
    • 8:state状态模式(object for state:状态对象)
    • 9:Strategy策略模式
    • 10:Template Method模板方法
    • 11:Visitor访问者模式
  • 面向对象设计的三大特性和五大原则(SOLID)
    • 五大原则
    • 三大特性

前言

本篇文章主要基于《深入设计模式》、《设计模式》、开源框架 protoActor-go 以及作者工作当中的一些浅显的感悟总结归纳而成。才疏学浅,期待共同进步(本文持续更新)。
想要深入学习的同学可以直接跳转链接:
https://refactoringguru.cn/design-patterns


创建型模式(五种)

创建型设计模式抽象了实例化过程,他们帮助一个系统独立于如何创建、组合和表示他的那些那些对象:

1:Abstract Factory 抽象工厂(Kit:工具箱)

  1. 书本描述翻译:提供一个接口用于创建一系列相关或者相互依赖的对象,而无需指定他们具体的类(这是书本的原话,听晕了没有?)。翻译:一个创建对象的函数的输入是一个接口(由不通的类实现),不同的类实现的函数可以创建不通的具有相同属性(实现了相同的接口)的对象。

  2. 模块结构
    (1)抽象产品(Abstract Product) 系列产品的 抽象接口
    (2)具体产品(Concrete Product) 抽象接口的不同类型实现
    (3)抽象工厂(Abstract Factory) 创建各种不同抽象产品的方法的接口
    (4)具体工厂(Concrete Factory) 特指抽象工厂接口当中构建方法的具体实现,每个具体工厂仅能创建特定的 具体产品

  3. 使用场景
    (1)代码需要与多个不同系列的相关产品交互,但是由于 无法提前获取相关信息,或者出于对未来扩展性的考虑,你不希望现有的代码基于未来的具体类进行构建。在这种情形下,你可以采用抽象工厂模式。
    抽象工厂为产品提供了一个接口,可以用于创建属于一个系列的产品对象。只要未来的 某个类 实现了该接口,那么他就能够生成应用程序所期待的可以用来使用的产品类型。
    (2)如果你有一个基于一组抽象方法的类,且其主要功能因此变得不明确,那么在这种情况下可以考虑采用抽象工厂模式。
    在设计良好的程序中,每一个类仅能负责一件事情。如果 一个类与多种类型产品 交互,就可以考虑将工厂方法抽取到独立的工厂类或者具备完整功能的抽象工厂类当中。

  4. 举例
    (1)protoActor-go 框架当中通过 props 输入参数生成不同的 Actor 就是一种典型的抽象工厂模式的应用。框架使用者们可以指定 props 这个工厂当中包含各种不同的 actor、mailbox、dispatcher、各种 Middleware 等等的 生成器(producer)。不同的生成器都实现了 抽象工厂 的接口**(xxxProducer)**,在调用的时候能够生成相应的实现了相同 抽象产品 接口的不同 Actor、mailbox等,从而实现拉起不同的 Process 并返回最终的 pid 信息。
    (2)参考《深入设计模式》一书中的今典例子,阿迪、耐克两个不同的工厂分别生产各自的鞋子和袜子。


2:Builder 生成器

  1. 书本描述翻译:将一个复杂对象的创建与他的表现分离,使得同样的构建过程可以创建不同的表示。翻译:工厂模式的升级版本,即创建的对象由多个组成部分构成,各个组成部分有各自的构建方法。
  2. 模式结构
    (1)生成器(Builder)接口 声明在所有类型生成器中通用的产品构造步骤。
    (2)具体生成器(Concrete Builder) 提供构造过程的不同实现。具体生成器也可以构造不遵循通用接口的产品。
    (3)产品(Product) 是最终生成的对象。由不同生成器构造的产品无需属于同一类层次结构或接口。
    (4)主管(Director)类 定义调用构造步骤的顺序,这样你就可以创建和复用特定的产品配置。
    (5)客户端(Client) 必须将某个生成器对象与主管关联。
  3. 举例
    (1)protoActor-go 框架当中的props的来源,这里的props就是一个builder,一般实际 的开发当中会实现一个builder函数用于创建props不同的builder可以生成具有不同组成部件的props。

3:Factory Method工厂方法(虚拟构造器:virtual constructor)——对象创建型模式

1: 意图:定义一个用于创建对象的接口,让子类决定实例化对象的类型。Factory Method 使一个类的实例化延迟到其子类。
2:动机Factory Method 模式提供了一个解决方案。它封装了哪个 Document 子类将被创建的信息并将这些信息从框架当中分离出来。Application 的子类重定义 Application 的抽象操作 CreateDocument 以返回适当的 Document 子类对象。一旦一个 Application 子类对象实例化,他就可以实例化与应用相关的文档,而无需知道这些 文档的类 , 我们称 CreateDocument 是一个工厂方法,因为它负责 “生产” 一个对象。
3:模式结构
(1)产品(Product) 将会对接口进行声明,对于所有由创建者及其子类构建的对象,这些接口都是通用的。
(2)具体产品(Concrete Product) 是产品接口的不同实现
(3)创建者(Creator) 类声明返回产品对象的工厂方法,该对象的返回类型必须与产品接口相匹配。我们也可以将工厂方法声明为 抽象方法,强制要求每个子类以不同的方式实现该方法。
(4)具体创建者(Concrete Creator) 将会重写基础工厂方法,使其返回不同类型的产品。
注意: 工厂方法不一定是死板地只会创建实例,他还可以返回缓存、缓存池或者其他来源的已有对象。
4:适用场景
(1)在编写代码的过程当中,如果我们无法预知对象确切类别及其依赖关系的时候,我们就可以使用工厂方法。
(2)如果你希望用户能扩展你的软件库或者框架的内部组件。这个时候可以选择工厂方法。
(3)如果你希望复用现有对象来节省系统资源,而不是每次重新创建对象,可以使用工厂方法。
5:举例
(1)在 protoactor-go 框架中,比较核心的一个部件就是 mailbox,该模块是实现各个 Actor 之间通信的基础。框架本身提供了原始的 defaultMailbox 以供使用,但是框架只是提供了一个通用的符合大多数业务场景能够通用的邮箱机制。但是往往我们在某些特定的业务场景下需要实现自己特有的 mailbox 这个时候我们可以定义一个本项目组特有的 mailbox模块 该模块只需要实现 产品接口,然后就可以通过实现框架提供的 创建者接口。从而将自己设计创建的邮箱嵌入到框架当中一共本项目组使用。


4:Prototype原型模式

  1. 意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆他们可能比每次用合适的状态手工实例化该类更方便一些。
  2. 模式结构
    (1)原型接口 将对克隆方法进行声明。在绝大多数情况下,其中只会有一个名为 clone 克隆 的方法。
    (2)具体原型类 实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况,例如克隆关联对象和梳理递归依赖等。
    (3)客户端(Client) 可以复制实现了原型接口的任何对象
  3. 适用场景
    (1)如果你需要复制一些对象,同时又希望代码独立于这些对象所属的具体类,可以使用原型模式。这一点考量通常出现在 处理第三方代码通过接口传递过来的对象时。就算不考虑代码耦合的情况,你的代码也不能依赖于这些对象所属的具体类,因为在模块化或者微服务分离的开发中,本端代码很多时候并不知道那些类的具体信息。
    (2)如果子类的区别仅在于对象的初始化方式,那么你就可以使用该模式来 减少子类的数量。因为用户在使用这些子类的 目的可能就是为了创建特定类型的对象。在原型模式当中,你可以使用一系列预生成的、各种类型的对象作为原型。客户端不必根据需求对子类进行初始化,只需要找到合适的原型并对其进行克隆即可。
  4. 举例
    (1)我们常用的 Linux 操作系统的文件系统,该系统实现对各个文件或者文件夹的管理。文件在很多情况下会涉及到 拷贝 等操作。一般情况下,各个文件类都会实现 clone() 方法,当我们需要复制一份相同文件的时候,只需要调用该文件的 clone() 方法,我们就可以得到一个新的相同文件(文件夹)。
    (2)在 protoActor-go 框架当中,会有一个专门的 PidSet 用于保存一个 ActorSystem 当中已经拉起的全部 process 信息。如果我们需要复制一份新的 pidSet 那么我们只需要调用它的 clone() 方法就可以实现了。

5:Singleton单件模式

  1. 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  2. 模式结构
    (1)单例(Singleton)类 声明了一个名为 getInstance() 获取实例 的静态方法类返回其所属类的一个相同实例。单列的构造函数必须对 客户端(Client) 代码隐藏,调用获取实例方法必须是获取单例对象的唯一方式。
  3. 适用场景
    (1)如果程序中的某个类对于所有客户端只能有一个可用的实例,就可以使用单例模式。单例模式禁止通过特殊构建构建方法以外的任何方法来创建自身的类对象,该方法可以创建一个新的对象,但是如果该对象已经被创建,则返回已有的对象
    (2)如果你需要更加严格地控制全局变量,可以使用单例模式。
  4. 举例
    (1)在代码中我们很多时候会不得不使用全局变量这么恶心的东西,因此我们有必要保证该全局变量真的有且只能有一个实例,这就需要用到单列模式,以确保不会有第二个全局变量实例被创建出来。

结构型模式(七种)

结构型涉及如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。

1: Adapter适配器模式(wrapper包装器)

  1. 意图:将一个外部类的接口转换为内部客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。翻译:将一个外部接口添加一层 wrapper包装 转换为可供内部调用的新的接口。在分布式系统开发中,不同模块的开发相互独立,模块之间的接口并不一定完全符合对方的要求。此时就需要通过适配器模式将对应的接口转换为满足各项目组内部要求的可供调用的接口。
  2. 模式结构
    (1)对象适配器 实现时使用了构成原则,适配器实现了其中一个对象的接口,并且对另外一个对象进行封装。
    (2)类适配器 这一实现使用了继承机制,适配器同时继承了两个对象的接口。
  3. 适用场景
    (1)当你希望使用某个类,但是其接口与其他代码不兼容时,可以使用 适配器类。适配器模式允许你创建一个中间层类,该中间类可以作为代码与遗留类、第三方类或者提供怪异接口的类之间的转换器。
    (2)如果你需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法,但是这些共同的方法不是所有这一继承体系中的子类所具有的共性。这时候我们就可以对每个子类进行扩展,将缺少的功能添加到新的子类中,但是,你必须在所有的新的子类中重复添加这些代码,这么一来代码就会出现代码坏味道。将缺失的功能添加到一个适配器类当中是一个优雅的解决方案。这样你就可以将缺少功能的类或者对象封装在适配器当中,从而能够实现动态地获取新的功能。想要实现这一模式,目标类 必须要能够抽象出一个 通用的接口,适配器的成员变量应当遵循该通用接口,这样的行为模式和 装饰器模式 非常相似。
  4. 举例
    (1)在我们的代码开发中,很少情况下是可以直接将工具箱的接口拿过来直接使用的,这就需要通过 Adapter 将其转化为我们需要的接口来使用。

2: Bridge桥接模式(Handle/Body)

  1. 意图 桥接是一种结构设计型模式,可以将业务逻辑或者一个大类拆分为不同的层次结构,从而能够独立地进行开发。
  2. 适用场景
    (1)如果你想要拆分或者重组一个具有多重功能的复杂类(例如一个能与多个数据库服务器进行交互的类),那么可以考虑使用桥接模式。类的代码行数越多,弄清其运作方式就越困难,对其进行修改所花费的时间也就越长,一个功能上的变化可能需要在整个类的多个范围内进行修改,而且常常会产生错误,甚至是严重的副作用。为此,桥接模式 可以将复杂的类拆分为多个类(模块)的层次结构,也就是将各个功能划归到不同的类当中单独实现 (单一职责原则),这样一来在修改或者添加某些功能的时候,我们就可以只修改对应的一个或者几个类,而不会影响到其他模块的功能。该模式可以很大程度的简化代码的维护工作,将修改代码可能导致的额外风险降到最低。
    (2)如果你想要在几个独立的维度上扩展一个类,可以使用该模式。桥接模式建议将每个维度抽取为独立的类层次,初始类将相关工作委派给属于对应类层次的对象,无需自己完成相应的工作。
    (3)如果你需要在运行的时候切换不同的实现方法,可以使用桥接模式。通过桥接模式 替换抽象部分的实现对象,具体操作就和给成员变量赋新值一样简单。
  3. 举例:在实现 DCI 架构当中, 顶层 Role 通过组合 内层 Role 实现 领域 对象的组合创建,从而实现对实际 值对象 的操作。这里的 顶层role 就可以理解为一个 原本具备多个复杂功能的庞大类,使用桥接模式将这个 庞大的复杂类 按照功能进一步细分出多个实现具体功能的 特定类(obj)内层role。不同的实际对象只要实现了这个 内层role接口 就都可以被组合到顶层role这个原本庞大的类当中,或者按照业务需求被编排到其他的顶层role当中已完成各种各样的新的业务逻辑。这就使得我们的代码变得很灵活。各个功能模块之间也能够实现解耦。

3: Composite组合模式

  1. 意图:将对象组合成 树形结构 以表示 部分—整体 的层次结构。Composite 使得用户对 单个对象组合对象 的使用具有一致性。也就是对于组合之后的树形结构只需要针对 “根节点” 进行操作就可以了,组合结构会自动的完成对整个树形结构的遍历。
  2. 协作:用户使用 Component 类接口与组合结构中的对象进行交互。如果接收者是一个叶节点,则直接处理请求。如果接收者是 Composite,它通常将请求发送给它的子部件,在转发请求之前或之后可能执行一些辅助操作。
  3. 适用场景
    (1)如果你需要实现树状对象结构,可以使用组合模式。组合模式为你提供了 两种共享公共接口的基本元素类型简单叶子节点复杂容器。容器中可以包含简单叶子节点或者其他容器。
    (2)如果你希望客户端代码能够以相同的方式处理简单和复杂的元素,可以使用该模式。组合模式中定义的所有元素公用同一个接口,在这一接口的帮助下,客户端不必在意其所使用的对象的具体类。
  4. 举例
    (1): 典型的 DCI 架构当中,各个领域对象(Obj)的组合就是一种典型的组合模式,在不同的业务流程模型当中,往往需要使用到不同的领域对象(obj)将这些领域对象组合新的领域对象从而实现一个新的领域模型(这里更偏向于 桥接模式?)。
    (2): Linux 文件系统 当中的有两个典型的对象 file 和 folder,他们之间就是一种典型的组合结构,对于任何一个文件内容的搜索,只需要调用最外层根节点的实现的 search() 方法就行了,组合结构会自动循环遍历所有的子文件夹和子文件。

4: Decorator装饰器模式(wrapper:包装器)

  1. 意图:动态地给一个对象添加一些额外的职责或者绑定新的行为。就增加功能来说,Decorator 模式相比生成子类更为灵活。
  2. 动机:有时我们希望给某些对象而不是类添加一些新的功能——一种较为灵活的方式是将该对象嵌入另外一个对象当中,通过 “另外一个对象” 添加额外的功能。“另外一个对象” 实现了相同的接口,这就使得对象可以无限的套嵌,每一层的对象在执行 公共的接口功能 之前都会被上一层对象进行 装饰
  3. 适用场景
    (1)如果你希望在无需修改代码的情况下既可以使用对象,且希望在运行的时候能够 给对象新增额外的功能,可以考虑使用装饰器模式。
    (2)如果用继承来扩展对象行为的方案难以实现或者根本不可行,可以使用该模式。某些语言可以通过 final 关键字来限制对于某个对象的扩展,在这种情况下复用该行为的唯一方法就是使用装饰器模式,用封装器对其进行封装。
  4. 举例
    (1)在 protoActor-go 框架当中,msg 消息是被封装在 actorContext 当中进行传递的,在消息被从 mailbox 当中取出以后如果发现对应的 contextDecoratorChain 不为 nil 那么将会把消息进行一层新的封装然后在发送到对应 actorreceive 函数进行处理——这里还涉及到了 责任链模式
    (2)同一个消息想要发送给多个不同的客户端,那么通常简单从 OOP 的角度类考虑的化,我们直接创建多个针对不同客户端的类就行了,但是从面向未来 的角度来看,该方法很容易导致类的数量急剧增多,从而导致项目难以维护。
    封装器 是装饰器的别称,是一个能与其他 “目标对象” 连接的对象。同时,封装器包含与目标对象相同的一系列方法,它会把收到的请求委派给自己的目标对象。但是,封装器可以在将请求 委派(转发) 给自己的目标对象之前对其进行 处理(装饰),这样的处理可能改变目标对象最终执行的结果。
  5. 相关模式
    1)Decorator 不同于 Adapter 模式,因为装饰器仅改变对象的职责而不改变它的接口;而适配器将给对象一个全新的接口。
    2)可以将装饰器视为退化的、仅有一个组件的组合 Composite。然而装饰仅给对象添加一些额外的职责——它的目的不在于对象聚集。
    3)用一个装饰可以改变对象的外表;而 Strategy 模式使得你可以改变对象的内核。这是改变对象的两种途径。

5: Facade外观模式

  1. 意图:为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这一接口使得这一子系统更加容易使用。
  2. 动机:将一个系统划分为若干个子系统有利于降低系统的复杂性。一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小。达到该目标的途径之一就是引入一个 外观 对象,它为子系统当中众多一般的设施提供了一个单一而简单的界面,或者换句话说,它将众多的可以归为一类的对象和方法组合之后,对外提供了一个统一的访问接口。
  3. 效果:在大型软件系统中降低编译依赖性至关重要。在子系统类改变时,希望尽量减少重编译工作以节省时间。采用 Facade 可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。同时提高了代码的 可复用性,在代码功能需要在模块之间迁移的时候可以很方便的改变功能的位置。
  4. 适用场景
    (1)如果你需要一个指向复杂子系统的直接接口,且该接口的功能有限,则可以考虑使用外观模式。子系统通常会随着时间的推进变得越来越复杂。即使在我们使用了设计模式的情况下,通常我们也会需要创建更多的子类。尽管在多种情况下子系统可能是更灵活或者易于复用的,但是其所需要的配置和样板代码数量将会增长的更快。为了解决这个问题,外观将会提供指向子系统中最长用功能的快捷方式,使其能够满足客户端的大部分需求。
    (2)如果需要将子系统组织为多层结构,可以使用外观模式。创建外观来定义子系统中各层次的入口。可以要求子系统仅使用外观来进行交互,从而减少子系统之间的耦合。
  5. 举例protoActor-go 框架的使用使得消息可以按照信息传递的方式在不同的 Actor 之间传递,这就使得我们能够认为的编排消息处理的 “时间和地点”,很多时候处于系统性能的考虑,原先在某一个 Actor 上面执行的业务功能需要迁移到另外一个 Actor 上面去执行,如果我们不按照功能模块将一组同类功能进行组合的化,我们就很容易遗漏某些重要或者不重要的功能,或者反复实现一些已经存在的功能。如果我们能够提前规划好,创建一个封装了所需功能的 外观类。那么我们在功能迁移的时候只需要改变一行或者有限的几行代码的执行位置,就能够实现所有功能的统一迁移。

6: Flyweight享元模式

  1. 意图:运用共享技术有效地支持大量细粒度的对象,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象共有的相同状态,让你能够在有限的内存容量中载入更多的对象。
  2. 动机:有些应用程序得益于在其整个设计过程中采用对象技术,但是简单化地实现代价极大。在一个对象当中存在一些成员变量,这些变量在对象被调用的过程中会被 自己 不断的改变但是其他对象只能读取而不能改变,称作 内在状态,通常这些变量可以位于 “对象之中”。与之对应的存在一种 外在状态 这些对象常常能够被其他对象 从外部 改变。
    假如我们能够从一组类当中抽象出各自特有的外在状态,那么对应剩下的就是内在状态,我们将仅存储内在状态的对象称为 享元
  3. 适用场景
    (1)仅在程序必须使用大量对象且没有足够的内存容量时使用享元模式。
  4. 举例:参考《深入设计模式》这本书当中今典的例子。太多了这里就不像描述了

行为型模式(十一种)

行为型模式涉及算法和对象间职责的分配。行为型模式不仅描述对象或类的模式,还能描述他们之间的通信模式。

1:Chain of Responsibility责任链模式

  1. 意图:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连城一套链,并沿着这条链传递该请求,直到有一个对象处理它为止。
  2. 意图:给多个对象处理一个请求的机会,从而解耦发送者和接收者。该请求沿着对象链传递直至其中一个处理它。
  3. 适用场景
    (1)当程序需要使用不同的方式处理不同种类的请求,而且请求类型和顺序预先未知的时候,可以使用责任链模式。该模式将多个处理者连接成一条线,在接收到请求之后,请求(事件)会 询问 每个处理者是否能够对其进行处理,这样所有的处理者(组合的特定功能的类)都能够有机会来处理请求(event)。
    (2)必须按照顺序调用多个处理者(模块)对事件(event)进行处理的时候,可以使用该模式。
    (3)如果所需要的处理者及其顺序必须在运行时进行改变,可以使用责任链模式。如果在处理者类中有对引用成员变量的设定方法,你将能够动态地插入和移除处理者,或者改变其顺序。
  4. 举例:在 protoActor-go 框架当中,对于各类外部输入消息的处理,我们定义了各种各样的消息中间件,在正式进入对于的 Actor 处理之前消息中间件会对 msg 消息进行一系列的预处理。考虑到框架的通用性需要适用于任何一种 msg 都能够拓展对应的中间处理流程,在这种情况下非常适合采用责任链模式进行处理。
type Props struct {
	spawner                 SpawnFunc
	producer                Producer
	mailboxProducer         mailbox.Producer
	guardianStrategy        SupervisorStrategy
	supervisionStrategy     SupervisorStrategy
	dispatcher              mailbox.Dispatcher
	receiverMiddleware      []ReceiverMiddleware
	senderMiddleware        []SenderMiddleware
	spawnMiddleware         []SpawnMiddleware
	receiverMiddlewareChain ReceiverFunc
	senderMiddlewareChain   SenderFunc
	spawnMiddlewareChain    SpawnFunc
	contextDecorator        []ContextDecorator
	contextDecoratorChain   ContextDecoratorFunc
}

对应的各类 chain 也是可以框架使用者自己设计指定的,通过指定各个切片类型的 MiddleWare 从而实现组装消息处理 链条


2:Command命令模式

  1. 书本描述翻译:将一个请求封装成一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。翻译:给不同的消息分配对应的不同的处理方法,处理方法可以自己定义,但是不影响消息的分配。
  2. 适用的场景
    (1)如果你需要通过操作来参数化对象,可以使用命令模式。命令模式可以将特定的方法调用转化为独立对象。这一改变带来了许多有趣的应用。我们可以 将命令作为该方法的参数 进行传递,将命令 保存在其他对象 中,或者在运行的时候切换已链接的命令等。
    (2)如果你想要将操作放入队列中,操作的执行或者远程执行操作,可以使用命令模式。命令对象(event) 如同其他对象一样,可以实现序列化,从而能够方便地写入文件数据库当中,一段时间过后,该对象被恢复成为最初的命令对象。因此,我们可以实现延迟或者计划命令执行的时间。
    (3)如果想要实现操作回滚可以使用命令模式。有很多方法可以实现撤销和恢复功能,但是命令模式可能是其中最常用的一种,为了能够回滚操作,你需要实现已执行操作的历史记录功能。命令历史记录是一种包含所有已执行命令对象以及相关程序状态备份的栈结构。这里可能需要结合 备忘录模式 一起实现。
  3. 举例
    (1)在protoActor-go框架当中,消息的最终处理依靠的是各个actor,每一个actor就是一个receiver,当对应的mailbox收到msg消息之后,会通过invoker调用对应的receiver处理msg消息。

3:Interpreter解释器模式

1:书本翻译描述:给定一个语言,定义他文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。翻译:对于某一类特定的问题,定义一套固定是解决方法。当遇到该特定类型的问题时,直接调用对应的 expression 解决该问题。
2:举例:暂无?


4:Iterator迭代器模式(cursor:游标)

  1. 书本描述翻译:提供一种方法顺序地访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表示。
  2. 适用场景
    (1)当集合背后为复杂的数据结构,而且你希望对客户端隐藏其复杂特性的时候(出于使用便利性或者安全性考虑),可以使用迭代器模式。迭代器封装了于复杂数据结构进行交互的细节,为客户端提供了多个访问集合元素的简单方法,这种方式不仅对客户端来说非常方便,而且能够避免客户端在直接与集合交互的时执行错误或者有害的操作,从而起到保护集合的作用。
    (2)该模式可以减少程序中重复的遍历代码,重要数据结构对象遍历的算法往往体积非常庞大,将这些代码嵌入到业务代码逻辑当中将会使得业务代码非常难以维护。因此,最好将这样的遍历代码放到特定的迭代器中以使得代码更加的精炼和简洁而易于维护。
    (3)如果你希望代码能够遍历不同的甚至无法预知的数据结构,可以使用迭代器模式。带模式为集合和迭代器实现了一些 通用的接口,如果你在代码中使用了这些接口,那么将其他实现这些接口的集合和迭代器传递给它,它将仍然可以正常的运行。
  3. 举例
    (1)对于代码当中的很对map和list我们会封装一组对于的方法,专门用来访问操作map&list当中的元素,这就是一种最初始的迭代器。

5:Mediator中介者模式

1:书本描述翻译:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变他们之间的交互。翻译:将各个对象之间的可能存在的复杂的相互调用逻辑封装到一个指定的 mediator类 当中,从而限制各个对象之间的直接交互,迫使他们都通过一个中介对象进行合作,从而减少对象之间混乱无序的依赖关系。
2:作用:中介者模式建议停止组件之间的直接交流从而使得组件之间彻底解耦,这些组件必须通过特殊的中介对象组合重定向调用行为,从而实现以间接的方式进行合作。最终的效果就是,组件仅仅依赖于一个中介者类,而无需与其他多个组件相耦合。
对于各个组件来说,中介者看上去完全就是一堵墙,发送者不知道墙的对面会是谁来处理自己的请求,接受者也不知道墙的对面是谁给我发了请求。
3:适用场景
(1)当一些对象和其他对象紧密耦合以至于难以对其进行修改的时候,可以使用中介者模式将他们之间的 依赖关系 抽象出来成为一个单独的类。
(2)当一个组件过于依赖于其他组件而无法再不同的系统中复用的时候,可以采用中介者模式,这样不同的应用就可任通过各自的 中介者类 对其进行复用
(3)如果为了在不同的情形下面复用一些基本的行为,导致你被迫需要创建大量的组件子类的时候,可以使用中介者模式,通过中介者实现各个组件之间的不同的组合方式。
4:举例:、
(1)典型的MVC架构模型
(2)参考《深入设计模式》一书中的火车站的例子,两列火车先后到达同一个车站,两列 火车对象 之间不会产生任何通信。那么 车站类 就充当了这两列火车对象之间的中介者,当前一列火车离开的时候会向车站发出通知,车站确认前一列火车离开之后方可允许下一列火车进站。


6:Memento备忘录模式

1:书本描述翻译:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
2:模式结构
(1)原发器(Originator) 原发器类(客户端类)可以生成自身状态的快照,也可以在需要的时候通过快照恢复自身状态。
(2)备忘录(Memento) 是原发器状态快照的值对象(value object),通常做法是将备忘录设置为不可变的,通过构造函数一次性传递数据
(3)负责人(Caretaker) 仅知道 “何时” 以及 “为何” 捕捉原发器的状态、以及何时恢复状态。负责人通过保存 备忘录栈,来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将从栈顶获取备忘录,并将其传递给原发器的 恢复(restoration) 方法。
3:适用场景
(1)当你需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。备忘录模式允许你复制对象中的全部状态(包括私有成员变量),并将其独立于对象进行保存。该模式在处理事务回滚的时候必不可少。
(2)当需要限制其他对象直接访问本对象的成员变量的时候,可以使用该模式。备忘录让对象自学负责创建其对象状态的快照,任何其他对象都不能够读取快照,这有效的保障了数据的安全性。
4:举例
最常见的我们日常使用的文本编辑器,几乎所有的文本编辑器都具备操作撤销的功能。通过该功能用于可以按照先后顺序撤销先前执行的每一步操作。


7:Observer观察者模式(dependent:依赖,publish-subscribe:发布-订阅)

1:书本描述翻译:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于他的对象都将得到通知并被自动更新。观察者模式是一种行为设计模式,允许你定义一种订阅机制,可以在对象事件发生的时候通知多个 观察 该对象的其他对象。
2:模式结构
(1)事件(event) 事件就是对象之间进行交流的 信息流,在该信息流当中包含着需要传递的各种有用的信息。该信息称作 事件
(2)发布者(Publisher) 会向其他对象发送值得关注的 事件(event),事件会在发布者自身状态改变或者执行特定行为后发生。发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅框架。当事件发生的时候,发送者会遍历订阅列表并调用每个订阅者对象的通知方法。该方法是在订阅者接口中声明的。
(3)订阅者(Subscriber) 可以执行一些操作来回应发布者的通知。所有的具体 订阅者类 都实现了同样的接口,因此发布者不需要与具体的类相耦合(这里有点策略模式的味道)。订阅者通常需要一些上下文信息来正确处理更新。因此,发布者通常会将一些上下文数据作为通知方法的参数进行传递。发布者也可以将自身作为参数进行传递,使订阅者直接获取所需要的数据。
(4)客户端(Client) 会分别创建发布者和订阅者对象,然后为订阅者注册发布者信息。
3:适用场景
(1)当一个对象的状态变化需要改变其他对象,或实际对象是事先未知的或者动态变化的时,可以使用观察者模式。
(2)当应用中的一些对象必须观察其他对象时候,可以使用该模式。但仅能在有限的时间内或者特定的情况下使用。订阅列表的动态的,因此订阅者可以随时加入或者离开列表。
4:举例
(1)在 protoActor-go 框架当中,actorContext 实现的 basePart 接口当中有一个 watch 方法,一个 Actor 通过该方法 watch(pid) 去 monitor 目标 Actor,目标 Actor 在收到 watch 消息之后会将该 pid 保存至 actorContextExtras 的 watchers(PIDSet)当中,当目标 actor 的状态发生改变的时候会广播通知 watchers 当中的每一个 pid 对象。


8:state状态模式(object for state:状态对象)

1:书本描述翻译:允许一个对象在其内部状态发生改变时改变他的行为。对象看起来似乎修改了他的类。翻译:一个对象对外部请求的响应随着自生状态的变化而变化。
有限状态机 程序在任意时刻仅处于几种有限的状态中,在任何特定的状态下程序的行为都不相同,可以瞬间从一个状态切换到另外一个状态。不过,根据当前状态,程序可能会切换到另外一种状态,也可能会保持当前状态不变,这些数量有限且预先定义的状态切换规则则被称作转移。
2:模式结构
(1)上下文(Context) 保存了对于一个具体的 状态对象 的引用,并会将所有与该状态相关的工作委派给它。上下文通过接口与状态对象交互,并且会提供一个设置器用于传递新的状态对象。
(2)状态(State)接口 会声明特定于状态的方法,这些方法能够被其他所有具体状态所理解,因为你不希望某些状态所实现的方法永远不被调用。
(3)**具体状态(Concrete State)**会自行实现状态接口的方法。同时,为了避免多个状态对象当中包含相似的代码,我们可以提供一个封装有部分通用行为的中间抽象类。
状态对象可以存储上下文对象的反向引用,然后可以通过该引用从上下文获取所需要的信息,并且能触发状态转移。
3:使用场景
(1)如果对象需要根据自身当前状态进行不同的行为,同时装态的数量非常多且与状态相关的代码会频繁变更的话,就需要考虑使用状态模式。状态模式建议将所有 特定于状态的代码 抽取到一组独立的类中,这样就可以实现各种不同状态之间的充分解耦,独自实现各自状态自己的功能。
(2)如果某个类需要根据成员变量当前的状态值改变自身的行为,从而需要使用大量的条件语句的时候,就需要考虑使用状态模式。
(3)当相似状态和基于条件的状态机转换中存在许多重复代码的时候,可以使用状态模式将公用的代抽取到一个新的基类当中从而减少代码的复用,提高解耦性。
4:举例
(1)在 protoActor-go 框架当中,每一个 process 都会继承一个 RouterState 对象

type process struct {
	parent      *actor.PID
	router      *actor.PID
	state       State
	mu          sync.Mutex
	watchers    actor.PIDSet
	stopping    int32
	actorSystem *actor.ActorSystem
}

很显然实现 RouterState 接口的类不止一种,因此,process 在不同的时刻调用的 RouterState 接口的具体的实现肯定也是不同的。
(2)假设我们需要需要喝水,天气就是我们依赖的状态。在不同的天气下我们都要喝水,但是不同的天气我们喝水的过程可能就是不一样的,天气热的时候我们需要喝冷水,天气冷的时候我们需要喝热水。


9:Strategy策略模式

1:定义:策略是一种行为模式,它将一组行为封装到一个类或者对象当中,并在上下文对象内部添加对这些行为对象的引用,并且实现这些对象之间的相互替换。
2:模式结构
(1)上下文(Context) 维护指向具体策略的引用,且仅通过策略接口与该对象进行交流
(2)策略(Strategy)接口 是所有具体策略的通用接口,它声明了一个上下文用于执行策略的方法。
(3)具体策略(Concrete Strategies) 实现了上下文所用的各种不同算法的不同变体,所有策略类都实现了相同的策略接口。
(4)客户端(Client) 会创建一个特定策略对象并将其传递给上下文,上下文会提供一个设置器以便客户端在运行的时候随时替换相关的策略
(5)当下文需要运行算法的时候,它会在自己已连接的策略对象上调用执行方法。上下文不清楚其所涉及的策略类型与算法的执行方式。
3:举例
(1)在 protoActor-go 框架当中每一个 actor 的 props 参数当中都会组合一个 supervisionStrategy 接口用于处理子 actor 的 failing 信息。

type Props struct {
	spawner                 SpawnFunc
	producer                Producer
	mailboxProducer         mailbox.Producer
	guardianStrategy        SupervisorStrategy
	supervisionStrategy     SupervisorStrategy
	dispatcher              mailbox.Dispatcher
	receiverMiddleware      []ReceiverMiddleware
	senderMiddleware        []SenderMiddleware
	spawnMiddleware         []SpawnMiddleware
	receiverMiddlewareChain ReceiverFunc
	senderMiddlewareChain   SenderFunc
	spawnMiddlewareChain    SpawnFunc
	contextDecorator        []ContextDecorator
	contextDecoratorChain   ContextDecoratorFunc
}

不同的actor的Props维护对supervisionStrategy对象的一个引用。一旦接收到子actor发送过来的failing信息,它就将这个消息转发给实现SupervisorStrategy的那个对象,例如allForOneStrategy、oneForOne不同的实例对象各自实现不同的HandleFailure功能。
(2)思考一下 Linux 操作系统常见的构建缓存的情形,由于处于内存当中,故其大小会存在限制,在达到限制上限以后,一些条目就必须被移除以留出空间。此类操作可以通过多种算法进行实现,一些流行的算法包括:
最少最近使用(LRU) 移除最近最少使用的一条条目
先进先出(FIFO) 移除最早创建的条目
最少使用(LFU) 移除使用率最低的一条条目
问题在于我们如何将 缓存类 与这些算法解耦,以便在运行时更改算法。此外,在添加新的算法时,缓存类不会因此改变。这就是 策略模式 发挥作用的典型场景,可以通过创建一系列不同的类来实现一系列不同的算法,但是这些算法全部实现的是相同的接口,这样就可以使得算法之间可以相互替换。这样我们通过给 缓存(caceh)类 赋予不同的 策略类 就可以不同缓存算法的调用。而不需要对这些类进行任何的修改。


10:Template Method模板方法

1:书本描述翻译:定义一个操作中的算法骨架,而将一些步骤延迟到子类当中。TemplateMethod使得子类可以不改变一个算法的结构即可以重定义该算法的某些特定的步骤。
2:模式结构
(1)抽象类(AbstractClass) 声明作为算法步骤的方法,以及依次调用他们的模板方法,算法步骤可以被声明为抽象类型(接口),也可以提供一些默认的实现。
(2)具体类(ConcreteClass) 可以重写所有步骤(接口方法),但是不能重写模板方法自身。
3:举例
(1)在我们代码框架当中的一个典型应用便是NewBaseTemplateProps方法,这是一种典型的模板方法模式。NewBaseTemplateProps()方法规定了一个固定的用于生成 actor.Props 的算法步骤。但是并没有具体地去实现该算法步骤当中的各个函数的详细实现——具体的函数实现以及参数内容完全由该模板方法的调用者实现。
(2)让我们来考虑一个一次性密码功能(OTP)的例子,将 OTP 传递给用户的方式多种多样(短信、邮件等),但是无论采用哪种方式,整个 OTP 的流程都是相同的:生成随机数字、在缓存中保存这组数据以便进行后续验证、准备内容、发送通知、发布;后续引入的任何新 OTP 类型都很有可能需要进行相同的上述步骤。
因此,我们于是就有了这样一种场景,其中某个特定操作的步骤都是相同的,但是实现方式却可能有所不同,这正是适合考虑使用模板模式的情况。
首先,我们定义一个由固定数量的方法组成的基础模板算法,也就是我们的模板方法。然后我们的具体的不同的 处理类 会实现每一个步骤的方法,但是不会改变模板方法。


11:Visitor访问者模式

1:书本描述翻译:访问者模式允许你在结构体中添加行为,而又不会对结构体造成实际的变更。
2:模式结构
(1)访问者(visitor)接口 声明了一系列以对象结构体的具体元素为参数的访问者方法。
(2)具体访问者(Concrete Visitor) 不同的 访问者类 会实现相同的访问者接口。
(3)元素(Element) 接口声明了一个方法(accept(v visitor))来 “接收” 访问者,该方法的入参必须为 访问者接口
(4)客户端(Client) 通常作为集合或者其他复杂对象的代表。客户端通常不知晓所有的具体元素类,因为他们会通过抽象接口与集合中的对象进行交互。
3:适用场景
(1)访问者模式通过在访问者对象中为多个 目标类 提供具有相同操作的变体,让你能在属于不同类的一组对象上面执行同一操作。如果你需要对一个复杂的对象结构中的所有元素执行某些操作,那么可以考虑使用访问者模式。
(2)可以使用访问者模式来清理属于辅助行为的业务逻辑。通过访问者模式将所有的非主要行为抽取到一组访问者类中,使得程序的 主要类 能够更专注于主要的工作。
(3)当某些行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义的时,可以使用该模式将那些特有的行为抽取到单独的访问者类中,只需实现接收相关类的对象作为参数的访问者方法,并将其他方法留空即可。
4:举例
(1)参考《深入设计模式》一书中内容。假设你是一个代码库的维护者,代码库中包含不同形状的结构体,如:方形、圆形、三角形。上述每个形状结构体都实现了通用形状接口。在业务演进的过程中,你的代码库会被各种各样的新的功能所淹没。例如,有个团队想要在你的形状结构体当中添加一个 getArea() 获取面积的请求。
一般情况下我们第一反应想到的肯定是直接将 getArea() 方法添加至形状接口,然后在各个形状类中实现它。这么做没有错,但是考虑到代码的演进,如果在来一百个这样的新的请求呢?我们是不是要在形状接口当中新增100个方法?这种做法显然是不行的。如果这么搞,团队 B 的业务需求岂不是全部落到团队 B 身上了?搞不好再出问题,做好了功劳是别人的,做不好锅自己背。
这个时候 访问者模式 就派上用场了,定义一个 访问者接口(visitor),该接口里面包含了请求者的一系列想要新增的方法,例如:

type visitor interface {
    visitForSquare(square)
    visitForCircle(circle)
    visitForTriangle(triangle)
}

接口当中各个方法的输入就是本项目组的 不同形状的结构体,这样一来请求方想要实现什么样的行为就有请求方在他们自己的 访问者接口 里面自己去执行就好了。出了问题也是他们自己的,我们不背锅~~。


面向对象设计的三大特性和五大原则(SOLID)

五大原则

在程序设计领域,SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖倒置)是由 罗布特.马丁 在21世纪早期引入的,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则一起使用的时候,它们使得程序员开发一个易于维护和扩展的系统变得可能。
1、S 单一功能原则
面向对象编程领域中,**单一功能原则(Single responsibility principle)**规定每个类或者对象都应该仅具有单一的功能职责,并且这些功能因该由这个类完全封装起来。所有由这个类提供的服务都应该严格的和该功能平行(功能平行意味着没有额外的功能依赖)。
马丁把功能(职责)定义为 改变的原因,并且认为 一个类或者一个模块应该有且仅有一个改变的原因。举一个简单的例子:假设我们一个人吃午饭的动作有两个 吃饭 + 喝水,按照单一功能的原则,吃饭和喝水者这两个完全分离的功能需要通过两个不同的类(模块)来实现。如果将这两个功能耦合在一起,在吃饭的同时去喝水显然是很容易 出问题的
2、O 开闭原则
在面向对象编程领域中,开闭原则规定软件中的对象(类、模块等)应该只允许扩展而不允许修改。这意味着对于一个模块而言,你只能在不会改变它源代码的情况下改变他的行为。该约束在产品化的环境当中是非常有价值的。在实际的商业化产品中,改变已有的代码是一件非常麻烦且危险的事情,需要经过代码审查、单元测试、环境验证等一系列的操作,以确保已有产品的功能不会应为新增代码的修改而改变从而导致不可预知的网络事故。
3、L 里氏替换原则
这个原则很简单,一句话描述就是 子类对象能够在使用基类对象的地方替换基类
4、I 接口隔离原则
接口隔离原则指的是,使用者应该不依赖于它不使用的方法。通过拆分非常庞大的且臃肿的接口为一个个更小的按照具体功能模块划分的接口。这样依赖使用者就感知不到那些他们不需要使用或者不能使用的方法。这种细分得到的接口通常被称作 角色接口(role interface),接口隔离的目的是进一步将系统的各个功能模块解耦,从而使其更加容易重构,以及更加方便重新部署。
5、D 依赖反转原则
在面向对象设计的领域中,依赖反转原则指的是一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略则应用在低层次上)形式,高层次的模块不应该依赖于低层次模块的实现细节,依赖关系被颠倒,最终使得低层次的模块依赖于高层次的模块。
该原则规定:高层次的模块不应该依赖于低层次的模块,两者都应该依赖与抽象接口抽象接口不应该依赖于具体实现,而具体实现应该依赖于抽象接口
在传统的应用架构当中,低层次的组件被设计用于被高层次组件使用,这一点提供了逐步构建一个复杂系统的基本思路。在这种结构下,高层次组件直接依赖于低层次的组件去实现所需要的任务,这种对于低层次的依赖限制的高层次组件被复用的可行性。
依赖反转原则的目的 是把高层次组件从对低层次组件的依赖中解耦出来,这样高层次组件就可以组合使用不同层次的组件。通过把高层次组件和低层次组件划分到不同的 包/库 当中的方式促进这种解耦。由于底层组件是对高层组件接口的具体实现,因此低层组件包的编译是依赖于高层组件的。
依赖反转原则同样被认为是应用了适配器模式:高层模块定义了它自己的适配器接口(高层依赖的抽象接口),被适配的对象同样依赖于适配器接口对象(因为它实现了这个接口)。通过这种方式,高层组件则不用依赖于低层组件,因为它仅间接地通过调用适配器接口的多态方法去使用低层组件,而这些方法则是由被适配的模块实现的。


三大特性

1、封装
封装就是将客观的事务抽象为逻辑的实体,实体的属性和功能相结合,形成一个有机的整体。同时对实体的属性和功能实现进行访问控制,向信任的实体开发,向不信任的实体隐藏。通过开放的外部接口可以访问,而无需知道功能如何实现。
2、继承
在继承的机制下可以形成有层级的类,使得低层级的类可以访问高层及的类的方法和属性。继承的方法有很多:实现继承、接口继承等。
3、多态
多态指的是一个类的同名方法,在不同情况下的实现细节不同。多态机制通过 不同的模块 使得共同的外部接口有着不同的内部实现。总的来说,多态有以下目的:一个外部接口可以被多个类使用不同对象调用同个方法,可有不同的实现

你可能感兴趣的:(命令模式,go)