design-patterns-for-humans-zh/vuepress/docs/README.md
2021-09-22 22:32:04 +08:00

37 KiB
Raw Blame History

::: tip 本项目仍在润色过程中 :::

献给中文读者的设计模式教程 / Design Patterns for Humans ZH

🎉 对设计模式的超简单解读! 🎉

设计模式的话题经常害得我们心神不宁。在这里,我将试着用尽可能简单的方式来解释它们,让它们深深地刻在您(还有我)的脑海里。

本项目基于 Design Patterns for Humans,案例的 JavaScript 代码来自 JavaScript Design Patterns for Humans


译者在学习的过程中,想要使用自己的语言风格来翻译这篇“给人类写的”设计模式教程。奈何才疏学浅,如有谬误,敬请斧正。

如果您喜欢这篇教程,不妨看看项目的原作者的博客,或是在 Twitter 上跟他打声招呼。

献给中文读者的设计模式教程 / Design Patterns for Humans ZH

🚀 介绍 / Introduction

设计模式是为解决某些普遍存在的问题提出的方案与指导原则。它们不是类,程序包或库文件,没法直接放到您的应用程序里然后等待神奇的事情发生。确切地说,它们是一种指导原则,旨在告诉您可以选用某种方法,来解决某些问题。

设计模式是为解决某些普遍存在的问题提出的方案与指导原则。

维基百科这样描述它们:

在软件工程领域,软件设计模式是为软件设计过程中,对给定上下文普遍存在的问题,所提出的通用、可重用的解决方案。它不是完整的设计实现,无法直接转译为源代码或机器码。正相反,它是面向如何解决问题的描述或模板,并可以在很多不同的情况下使用。

⚠️ 请注意 / Be Careful

  • 设计模式并不是解决您所有问题的万全之策。
  • 不要强迫去使用它们;否则很可能发生不好的事情。
  • 请记住,设计模式是一种指导方案,用来解决问题,而不是找到问题;所以不要想太多。
  • 如果在正确的地方以正确的方式使用设计模式,它们或许能成为您的得力帮手;否则可能导致您的代码混乱不堪。

🐢 在开始之前 / Before you start

  • 所有的设计模式示例都基于 JavaScript 的 ES6 规范实现。
  • 由于 JavaScript 中不存在实现接口的说法,因此我们在代码示例中使用了隐式接口,这意味着只有一个类具有一个接口应当有的属性和方法,这个类就被认为实现了这个接口。为了让您更容易分辨当前正在使用的接口,我们在每个示例中都添加了注释信息。

🏗️ 创建型设计模式 / Creational Design Patterns

简单来说

创建型设计模式关注如何实例化一个对象,或一组相关的对象。

维基百科这样描述

在软件工程领域,创建型设计模式是处理对象创建机制的设计模式,试图以符合要求的方式来创建对象。创建对象的基础方式可能导致设计问题或增加设计复杂度。创建型设计模式通过以某种方式控制对象创建的过程,来解决这个问题。

🏠 简单工厂模式 / Simple Factory

现实生活中的例子

想象,您正在修建一栋房子,而您需要门。您可以穿上木匠的衣服,拿上木头、胶水、钉子以及其它需要的工具,在您的房子里亲自制作一扇门;或者,您只需要打个电话给工厂,让他们把制造好的门送到您身边,这样您不必了解任何关于制作门的知识,也不必处理制作门所带来的麻烦。

简单来说

简单工厂为客户端生成一个实例,而不向客户端暴露任何实例化操作的逻辑。

维基百科这样描述

在面向对象编程OOP工厂是用于创建其它对象的对象——更准确地说工厂是一个函数或方法通过调用它的某个方法假设为 "new")可以返回拥有不同原型或类的对象。

编程示例

首先,我们定义了门的接口和并实现了它

<<< @/fragments/javascript/simpleFactory.js#WoodenDoor

接下来,我们创建了制造并返回门的工厂

<<< @/fragments/javascript/simpleFactory.js#DoorFactory

最后,可以这样使用工厂

<<< @/fragments/javascript/simpleFactory.js#makeDoor

什么时候使用?

当创建对象不仅只有赋值操作,还会涉及到一些逻辑过程时,把它放到一个专用的工厂中(而不是在每个地方编写重复的代码)是很有意义的。

🏭 工厂方法模式 / Factory Method

现实生活中的例子

以招聘经理为例。一个人不可能面试所有的职位。根据职位空缺情况,招聘经理必须决定面试的步骤,并将面试委派给其他不同的面试官。

简单来说

工厂方法模式提供了一种将实例化的逻辑分派给子类的方法。

维基百科这样描述

在基于类的编程中,工厂方法模式是一种创建型模式,它使用工厂方法来处理创建对象的问题,而不必指定将要创建的对象所基于的具体类。这是通过调用工厂方法来创建对象所实现的——要么在接口中指定并由子类实现,要么在基类中实现并可选地由派生类覆盖——而不是通过调用构造函数实现。

编程示例

以上面的招聘经理为例。首先,我们定义面试官的接口和它的一些实现

<<< @/fragments/javascript/factoryMethod.js#Interviewer

接着,让我们编写 HiringManager(招聘经理)类

<<< @/fragments/javascript/factoryMethod.js#HiringManager

现在所有的子类都可以继承它并提供需要的面试官

<<< @/fragments/javascript/factoryMethod.js#extendHiringManager

最后,我们可以这样使用它

<<< @/fragments/javascript/factoryMethod.js#takeInterview

什么时候使用?

当类中存在一些通用的处理过程,而所需的子类要在运行时动态决定时,工厂方法模式非常有用。换句话说,当客户端不知道它可能需要什么具体的子类时。

🔨 抽象工厂模式 / Abstract Factory

现实生活中的例子

现在让我们拓展简单工厂模式里门的例子。根据您的需要,您可能会从木门店买到木门,从铁门店买到铁门,或是从对应的商店买到塑料门。接着,您可能需要有着不同专业能力的师傅来安装这些门,比如让木匠安装木门,让焊工安装铁门等等。如前所述,现在门与安装师傅之间存在一种依赖关系,木门需要木匠,铁门需要焊工等等。

简单来说

工厂的工厂;一个工厂,它将独立但是相关(或互相依赖)的工厂组成一组,而不用指定这些工厂的具体类。

维基百科这样描述

抽象工厂模式提供了一种封装一组具有共同主题的独立工厂的方法,而不必指定它们的具体类。

编程示例

根据上面门的例子。首先,我们定义了 Door(门)接口和它的一些实现

<<< @/fragments/javascript/abstractFactory.js#Door

接着我们为每一种类型的门定义了安装师傅

<<< @/fragments/javascript/abstractFactory.js#DoorFittingExpert

现在我们有了抽象工厂,它允许我们创建一系列相关联的对象,即木门工厂能够制造木门并提供安装木门的师傅,而铁门工厂能够制造铁门并提供安装铁门的师傅。

<<< @/fragments/javascript/abstractFactory.js#DoorFactory

最后我们可以这样使用它

<<< @/fragments/javascript/abstractFactory.js#makeDoor

正如您看到的,木门工厂已经封装了 carpenter(木匠)和 wooden door(木门),而铁门工厂已经封装了 iron door(铁门)和 welder(焊工)。因此,它确保了对于每一个制造出来的门,我们都能得到正确的安装师傅。

什么时候使用?

当对象之间存在相互依赖的关系,涉及的创建逻辑并不那么简单时。

👷 生成器模式 / Builder

译注:又名建造模式

现实生活中的例子

想象您在哈帝斯汉堡店里,点了一份“大哈迪汉堡”,接着店员就把汉堡递给你,毫无疑问 这是一个简单工厂的例子。但是在一些情况下,制作汉堡可能会包括额外的步骤逻辑。举个例子,您想要一份定制的汉堡,这里有很多选项:要什么面包片?喜欢哪款酱汁?想吃哪种奶酪?诸如此类。在这种情况下,就需要用到生成器模式了。

简单来说

生成器模式允许您创建不同风格的对象,同时避免污染构造函数。当一个对象可能存在多种风格时,或者当一个对象的创建过程包含很多步骤时,生成器模式非常有用。

维基百科这样描述

生成器模式是一种创建型软件设计模式旨在找出重叠构造函数反面模式Telescopic Constructor Anti-pattern的一个解决方案。

既然已经提到,那么请允许我补充一下什么是重叠构造函数反面模式。在某一时刻我们都看到了这样一个构造函数:

<<< @/fragments/javascript/builder.js#TelescopicConstructor

正如您所看见的,构造函数的参数数量很快就会失控,理解参数的含义也可能因此变得困难。此外,如果在以后您还想添加更多的构造选项,它的参数列表还会继续增长。这就被称作重叠构造函数反面模式。

编程示例

明智的选择是使用生成器模式。首先我们定义我们想要制作的汉堡

<<< @/fragments/javascript/builder.js#Burger

接着我们编写了生成器

<<< @/fragments/javascript/builder.js#BurgerBuilder

最后可以这样使用它

<<< @/fragments/javascript/builder.js#newBurger

JavaScript 版本特别提示:当您发现一个函数或方法的参数太多(一般超过 2 个参数都被认为是太多)时,应当使用一个对象参数,来取代多个参数。理由有二:

  1. 它可以让您的代码看上去更整洁,因为只有一个参数。
  2. 您不需要担心参数的顺序,因为参数将根据对象的命名属性传递。

举个例子,应当使用:

<<< @/fragments/javascript/builder.js#goodBurger

来取代:

<<< @/fragments/javascript/builder.js#badBurger

什么时候使用?

当一个对象可能有多种风格,并想要避免重叠构造函数时。生成器模式与工厂模式的关键区别是,当创建对象过程只有一个步骤时,应使用工厂模式;当创建对象过程存在多个步骤时,应使用生成器模式。

🐑 原型模式 / Prototype

现实生活中的例子

还记得多莉吗?那只被克隆的羊!不谈羊的细节,这里全部的关键在于克隆。

简单来说

基于已存在的对象,通过克隆创建新的对象。

维基百科这样描述

在软件开发领域,原型模式是一种创建型设计模式。当创建的对象类型由一个原型实例确定时,使用原型模式,这个原型实例将被克隆来生成新的对象。

简而言之,原型模式允许您创建已存在对象的副本,并根据您的需要修改这个副本,而不用经历从头开始创建新的对象并配置整个对象的麻烦。

编程示例

首先我们定义了我们想要克隆的羊

<<< @/fragments/javascript/prototype.js#Sheep

现在我们有了 SheepPrototype 对象,它将克隆给定了原型的对象。它的构造函数接受 Sheep 对象

<<< @/fragments/javascript/prototype.js#SheepPrototype

最后我们可以这样使用它

<<< @/fragments/javascript/prototype.js#cloneSheep

JavaScript 版本特别提示:此编程示例是原型模式的经典实现,但是 JavaScript 能够使用内建原型工具更有效地实现原型模式。

什么时候使用?

当需要的对象与现有的对象很相似时;或与克隆的方式相比,直接创建对象的成本更高时。

💍 单例模式 / Singleton

现实生活中的例子

一个国家里同时只能有一位总统。当职责需要时,这位总统就必须采取行动。这里的总统即是单例。

简单来说

确保只创建了特定类的唯一对象。

维基百科这样描述

在软件工程领域,单例模式是一种软件设计模式,它将类的实例化限制为一个对象。当只需要一个对象来协助整个系统的运行时,单例模式很有帮助。

实际上单例模式被认为是一种反面模式Anti-pattern应当避免过度使用它。单例模式不一定总是不好的也能够有一些有效的用例但仍应当谨慎使用它因为它会在您的应用中引入全局状态在一个地方对它的修改可能会影响到其它地方并且调试起来可能会变得非常困难。使用单例模式的另一个坏处是它会使您的代码紧密耦合此外模拟Mock译者注在单元测试中常用 Mock 的方法来模拟构造一些复杂或不容易获取的对象)单例可能会很困难。

编程示例

在 JavaScript 中,单例可以使用模块模式实现。私有变量和方法隐藏在函数闭包中,而公有方法有选择地暴露出去。

<<< @/fragments/javascript/singleton.js#president

在这里,presidentsPrivateInformationname 为私有变量。但是,name 可以通过对外暴露的 president.getName 方法访问到。

<<< @/fragments/javascript/singleton.js#getPresidentName

🔩 结构型设计模式 / Structural Design Patterns

简单来说

结构型设计模式主要关注对象的组成,或者换句话说,关注实体之间如何相互使用。再或者另一种解释是,它有助于回答“如何构建软件的组件”这个问题。

维基百科这样描述

在软件工程领域,结构型设计模式是通过识别一个简单的方法,这个方法实现了实体之间的关系,来简化设计的设计模式。

🔌 适配器模式 / Adapter

现实生活中的例子

您的存储卡里有一些图片,您想要将它们传输到您的电脑中。为了传输图片,您需要某种与电脑端口兼容的适配器,这样才能将存储卡连接到您的电脑。在这种情况下,读卡器就是一个适配器。 另一个例子是电源适配器,一个三脚插头无法插入到两脚插座中,它需要用到电源适配器使其与两脚插座兼容。 再举个例子,一位翻译者将一个人说的话翻译给另一个人(译注:这里的翻译者就是适配器)。

简单来说

适配器模式允许您将与其它不兼容的对象包装到一个适配器中,让这个对象与另一个类兼容。

维基百科这样描述

在软件工程领域,适配器模式是一种设计模式,它允许一个现有类的接口用作另一个接口。适配器模式常用于使现有的类与其它的类一起工作,而无需修改它们的源码。

编程示例

考虑一个游戏,我们有一个猎人,他要狩猎狮子。

首先我们定义了 Lion(狮子)接口,所有类型的狮子都需要实现这个接口

<<< @/fragments/javascript/adapter.js#Lion

而猎人需要 Lion 的某种实现来狩猎。

<<< @/fragments/javascript/adapter.js#Hunter

现在假设我们在我们的游戏里添加了一只 WildDog(野狗),猎人也可以狩猎它。但是我们无法直接添加野狗,因为它有着不同的接口。为了让它与我们的猎人兼容,我们必须创建一个适配器

<<< @/fragments/javascript/adapter.js#WildDog

于是,通过 WildDogAdapter,在我们的游戏中就可以使用 WildDog 了。

<<< @/fragments/javascript/adapter.js#newWildDog

🚡 桥接模式 / Bridge

现实生活中的例子

您有一个包括很多页面的网站,现在您计划让用户修改网站的主题。您会怎么做?为每个页面的每个主题创建一份副本,或是创建单独的主题并根据用户的偏好加载它们?桥接模式允许您实现后者,如下图所示。

不使用和使用桥接模式的区别

简单来说

桥接模式是偏好于使用组合的模式,而不是继承。实现的细节从一个模组层次推送给另一个具有单独模组层次的对象。

维基百科这样描述

桥接模式是一种用在软件工程领域的设计模式,旨在“将抽象与其实现解耦,使得两者可以独立改变”。

编程示例

翻译一下刚刚关于我们网站的例子。现在我们定义了 WebPage(网站)的模组层次

<<< @/fragments/javascript/bridge.js#Webpage

以及单独的主题模组层次

<<< @/fragments/javascript/bridge.js#Theme

最后,结合使用两个主题模组层次

<<< @/fragments/javascript/bridge.js#useTheme

🌿 组合模式 / Composite

现实生活中的例子

每个组织都由雇员组成。这些雇员具有一些共同点:有一定的薪水酬劳,担负某些任务职责,可能需要向某人汇报,可能拥有下属等等。

简单来说

组合模式使得客户端以统一的方式处理不同的对象。

维基百科这样描述

在软件工程领域,组合模式是一种分离设计模式。组合模式描述了一组对象的处理,与对一个对象的单个实例的处理相同。组合模式的目的是将对象“组合”进树结构中,来表示部分整体的层次结构。实现组合模式使得客户端使用统一的方式处理不同的对象和组合体。

编程示例

以前面我们雇员的例子为例。现在我们有两种不同的雇员类型

<<< @/fragments/javascript/composite.js#Employee

接着我们定义了一个组织类,它包含这两种类型的雇员

<<< @/fragments/javascript/composite.js#Organization

最后我们可以这样使用它

<<< @/fragments/javascript/composite.js#getNetSalaries

装饰器模式 / Decorator

现实生活中的例子

想象您经营了一家提供多种服务的汽修厂。那么您要如何计算应该收取的费用?您可以选择一种服务,然后将服务的价格动态地累计到账单上,直到您得到了最终的费用。在这个例子中,每一种类型的服务都是一个装饰器。

简单来说

装饰器模式允许您将对象放入一个装饰器类的对象中,动态地改变这个对象在运行时的行为。

维基百科这样描述

在面向对象编程中,装饰器模式是一种设计模式,它允许静态或动态地向单个对象中添加行为,而不会影响同一个类的其它对象的行为。装饰器模式通常有助于遵循单一责任原则,这是因为它允许在具有独特关注领域的类之间,划分出各自的功能。

编程示例

不妨以咖啡为例。首先,我们编写了一个简单的咖啡类来实现咖啡接口

<<< @/fragments/javascript/decorator.js#Coffee

我们想要让代码具有可扩展性,当需要的时候可以对它进行修改。让我们来编写一些额外选项(装饰器)

<<< @/fragments/javascript/decorator.js#ExtraCoffee

现在,来一杯咖啡吧

<<< @/fragments/javascript/decorator.js#getCoffee

📦 门面模式 / Facade

译注:也常被译为外观模式

现实生活中的例子

您是如何启动电脑的?您会说:“按下电源按钮!”。之所以如此确信,是因为您正使用着电脑为外界提供的简单接口。而在电脑内部,它必须做很多事情来实现启动事件。复杂子系统的简单接口,就是我们所说的门面。

简单来说

门面模式为复杂的子系统提供了一个简化的接口。

维基百科这样描述

门面是一个对象,它为更大的代码主体提供了简化的接口,例如一个类库。

编程示例

拿上面启动电脑的例子来说。首先我们编写了电脑类

<<< @/fragments/javascript/facade.js#Computer

接着我们编写了它的门面

<<< @/fragments/javascript/facade.js#ComputerFacade

最后就可以使用电脑的门面了

<<< @/fragments/javascript/facade.js#useComputerFacade

🍃 享元模式 / Flyweight

现实生活中的例子

您喝过摊位上新鲜的茶吗?摊主通常会沏很多杯茶,一杯给您,然后将其它的留待给其他消费者,以达到节省资源(例如燃气)的目。享元模式就是关于这件事——共享。

简单来说

享元模式通过在相似对象间共享尽可能多的数据,来减少内存使用或计算开销。

维基百科这样描述

在计算机编程中,享元模式是一个软件设计模式。享元是一个对象,它与其它相似的对象共享尽可能多的数据,来减少内存开销。当简单的重复行为将占用不可接受数值的内存时,可以使用享元模式来表示大量级的对象。

编程示例

翻译前边我们关于茶的例子。首先我们编写了茶和茶的制作类

<<< @/fragments/javascript/flyweight.js#TeaMaker

接着我们编写了 TeaShop(茶店)类,它将处理点单和上茶事件

<<< @/fragments/javascript/flyweight.js#TeaShop

最后我们可以像下面一样使用它

<<< @/fragments/javascript/flyweight.js#serveTea

🎱 代理模式 / Proxy

现实生活中的例子

您曾刷卡来通过门禁吗?有很多方法可以通过门禁,刷卡或是按下跳过安保的按钮。门是拿来被打开的,但这里添加了一层代理到门禁上,为它增添了一些安保功能。让我通过下面的代码示例更好地解释它。

简单来说

使用代理模式,类能够行驶另一个类的功能。

维基百科这样描述

在其最一般的形式,代理是一个类,作为其它类的接口发挥功能。代理是被客户端调用的封装或中介对象,用于访问系统背后真正提供服务的对象。使用代理可以直接地指向真正的对象,或者可以提供额外的逻辑。在代理模式下,可以添加附加的功能,例如,当对真正对象的操作会消耗大量资源时进行缓存,或是在调用真正对象的操作之前检查前置条件。

编程示例

以上面我们的门禁为例。首先,我们编写了门的接口和它的实现

<<< @/fragments/javascript/proxy.js#Door

接着我们编写了代理,它可以确保门的安全

<<< @/fragments/javascript/proxy.js#Security

最后这是使用门禁的方法

<<< @/fragments/javascript/proxy.js#openCloseSecurityDoor

🤹 行为型设计模式 / Behavioral Design Patterns

简单来说

行为型设计模式主要关注对象之间责任的分配。与结构型设计模式不同的是,它们不仅指定了对象的结构,还概述了对象之间消息传递(通信)的模式。换句话说,它们有助于回答了“如何在软件组件中执行行为?”这个问题。

维基百科这样描述

在软件工程领域,行为型设计模式是识别并实现对象之间的常见通信模式的设计模式。如此一来,这些模式使得通信变得更加灵活。

🔗 责任链模式 / Chain of Responsibility

现实生活中的例子

举个例子,在您的账户中,您设置了三种支付方式(A, BC),其中存放了不同数量的钱。A 账户中有 100 美元,B 中有 300 美元,而 C 中有 1000 美元,按照 AB 最后是 C 的顺序偏好进行支付操作。您尝试购买某个价值 210 美元的东西。使用责任链模式,首先,检查 A 账户是否能购买,如果可以,将进行支付操作然后中止链。如果不足以购买,请求将传递给 B 账户,检查是否能购买,同样如果可以,将链中止;如果不可以,请求将继续传递直到它找到合适的处理者。在这里,ABC 是链上的不同环节,而这整个模式就是责任链。

简单来说

责任链模式有助于构建一条对象链。请求从链的一端进入,从对象到另一个对象依次传递,直到它找到合适的处理者。

维基百科这样描述

在面向对象设计中,责任链模式是由一些命令对象和一系列处理对象组成的设计模式。每个处理对象都包含了它可以处理的命令对象类型的逻辑,其余的将传递给链中的下一个处理对象。

编程示例

翻译一下刚刚我们账户的例子。首先,我们编写了账户基类,它包括将多个账户链接起来的逻辑。继承账户基类,我们编写了具体账户类

<<< @/fragments/javascript/chainOfResponsibility.js#Account

现在,让我们使用上面定义的具体账户类(即银行,贝宝,比特币),构成我们的责任链

<<< @/fragments/javascript/chainOfResponsibility.js#useAccount

👮 命令模式 / Command

现实生活中的例子

举一个常见的例子,您在餐厅点单。您(Client 客户端)告诉服务员(Invoker 调用者)您想要这些菜肴(Command 命令),于是服务员简单地将这些需求转发给厨师(Receiver 接收者),他们知道这些菜肴是什么以及该如何烹制。 另一个例子是您(Client)使用遥控器(Invoker),切换(Command)电视(Receiver)正在播放的频道。

简单来说

命令模式允许您将操作封装到对象中。命令模式背后的核心思想是,提供将客户端与接收者解耦的方法。

维基百科这样描述

在面向对象编程中,命令模式是一种行为型设计模式,它将执行操作或稍后触发事件所需的全部信息封装到一个对象中。信息包括方法名,拥有此方法的对象和此方法参数的值。

编程示例

首先,我们定义了接收者,它实现了可能会执行的每个方法

<<< @/fragments/javascript/command.js#Bulb

我们已经有了每个命令都需要实现的接口,基于这个接口,我们定义了一组命令

<<< @/fragments/javascript/command.js#Command

之后,我们定义了 Invoker,由它来处理客户端的交互命令

<<< @/fragments/javascript/command.js#RemoteControl

最后,让我们来看看该如何通过客户端调用它

<<< @/fragments/javascript/command.js#useRemoteControl

命令模式也可以用来实现基于事务的系统。您执行的命令将保存在历史记录中。如果成功执行了最后的命令,一切都好;如果没有,则可以根据历史记录不断回滚,对所有已执行的命令进行 undo(撤销)操作。

迭代器模式 / Iterator

现实生活中的例子

旧式收音机是解释迭代器模式的好例子,用户可以从某个广播频道开始,使用向后或向前的按钮来遍历收听各个广播频道。或者以 MP3 音乐播放器或电视为例,您可以按下向后或向前的按钮来浏览连续的频道。换句话说,它们都提供了使用迭代器遍历各个频道,音乐或广播的接口。

简单来说

迭代器模式提供了访问对象的所有元素的方法,而不必暴露对象的底层表现形式(译者注:列表、栈和树等)。

维基百科这样描述

在面向对象编程中,迭代器模式是一种使用迭代器遍历容器并访问其所有元素的设计模式。迭代器模式可以将算法与容器解耦;但在某些情况下,算法是必然特定于容器的,因而无法解耦。

编程示例

翻译一下我们上面广播电台的例子。首先我们定义了 RadioStation(广播电台)类。

<<< @/fragments/javascript/iterator.js#RadioStation

接着我们定义了迭代器

<<< @/fragments/javascript/iterator.js#StationList

现在我们可以这样使用它

<<< @/fragments/javascript/iterator.js#useStationList

👽 中介者模式 / Mediator

现实生活中的例子

一个常见的例子是,当您正使用您的手机跟其他人聊天时,中间存在有一个网络提供商,您发出的消息将通过这个网络提供商送达,而非直接发送。在这种情况下,网络提供商充当了中介者的身份。

简单来说

中介者模式在两个对象(称作“同事类”)之间添加了一个第三方对象(称作“中介者”),进而控制这两个对象之间的交互。中介者模式有助于降低类之间通信交流的耦合度。因为现在,它们不再需要了解对方的实现细节。

维基百科这样描述

在软件工程领域,中介者模式定义了一个对象,该对象封装了一组对象之间交互的方式。中介者模式被认为是一种行为型设计模式,因为它可以改变程序运行时的行为。

编程示例

下面是聊天室的最简单示例,其中有若干互相发送消息的用户

首先我们定义了中介者:聊天室

<<< @/fragments/javascript/mediator.js#ChatRoom

接着我们定义了同事类:用户

<<< @/fragments/javascript/mediator.js#User

就可以像这样使用了

<<< @/fragments/javascript/mediator.js#useChatRoom

💾 备忘录模式 / Memento

现实生活中的例子

以计算器(即“原发器”)为例,每当您执行了某些计算操作时,最后一次计算的结果都会被保存到内存(即“备忘录”)中,这样您就可以通过按下某些操作按钮(即“负责人”)来查看,或者如果可能的话,恢复它。

简单来说

备忘录模式通过某种方式捕获并保存对象的当前状态,以便在之后可以使用轻松的方式恢复。

维基百科这样描述

备忘录模式是一种提供了恢复对象到以前状态(通过回滚来撤销)的能力的软件设计模式。

通常在您需要提供某种撤销功能时十分有用。

编程示例

以本文编辑器为例,它不时地保存当前的状态,当您想要时则可以恢复

首先我们定义了备忘录对象,它能够保存编辑器的状态

<<< @/fragments/javascript/memento.js#EditorMemento

接着我们定义了编辑器对象,即原发器,它将会使用到备忘录对象

<<< @/fragments/javascript/memento.js#Editor

最后可以这样使用它

<<< @/fragments/javascript/memento.js#useEditor

😎 观察者模式 / Observer

(也被称为 “发布-订阅模式”

现实生活中的例子

求职者就是很好的例子,他们会订阅一些职位发布网站,当存在匹配的工作机会时会通知他们。

简单来说

观察者模式定义了对象之间的依赖关系,每当一个对象改变它的状态时,它的所有依赖对象将会得到通知。

维基百科这样描述

观察者模式是一种软件设计模式,一个目标对象管理所有依赖于它的观察者对象,并在它状态发生改变时主动通知观察者对象,这通常是通过调用观察者对象的某个方法来实现的。

编程示例

翻译前面我们的例子。首先,我们定义了求职者类,当有新的职位发布时,他们将得到通知

<<< @/fragments/javascript/observer.js#JobSeeker

接着,我们定义了职位发布公告栏,供求职者订阅

<<< @/fragments/javascript/observer.js#JobBoard

现在,可以这样使用它

<<< @/fragments/javascript/observer.js#useJobBoard

🏃 访问者模式 / Visitor

现实生活中的例子

以去杜拜旅游为例,人们只需要通过签证便可进入杜拜。抵达之后,他们可以自行参观杜拜的所有地方,而不必征得更多许可。只需要知道地点,他们就可以访问参观了。访问者模式允许您像这样做,添加一些访问者可以自由访问的地点,而无需访问者做额外的跑腿工作。

简单来说

访问者模式允许您为对象添加进一步的操作,而无需修改它们。

维基百科这样描述

在面向对象编程和软件工程领域访问者模式是一种将算法从执行它的对象的结构中分离出来的方式模式。这种分离带来的实际效果是提供了向已存在对象的结构中添加新的操作方法而无需修改这些对象的结构的能力。它是一种遵循开闭原则Openclosed principle的方法。

编程示例

让我们模拟一个动物园作为例子,这里有几种不同的动物,它们会发出不同的叫声。现在使用访问者模式翻译为代码语言

我们已经实现了动物类

<<< @/fragments/javascript/visitor.js#Animal

接着,实现我们的访问者

<<< @/fragments/javascript/visitor.js#speak

它可以这样使用

<<< @/fragments/javascript/visitor.js#AnimalSpeak

我们可以简单地为动物类添加可继承的层次结构来让动物发出叫声,但接下来当我们需要为动物添加新的行为时,不得不去修改动物类本身。但现在,我们不再需要修改动物类。举个例子,如果我们需要为动物添加跳跃行为,我们可以简单地创建一个新的访问者来实现,如下所示

<<< @/fragments/javascript/visitor.js#jump

像这样使用它

<<< @/fragments/javascript/visitor.js#AnimalJump

💡 策略模式 / Strategy

现实生活中的例子

考虑排序的例子,我们实现了冒泡排序,但随着数据量的增长,冒泡排序变得非常慢。为了解决这个问题,我们又实现了快速排序。但是,尽管快速排序算法在处理较大数据集时表现得很好,在处理较小数据集时它却表现较差。为此,我们定义了一种策略,当处理较小数据集时,采用冒泡排序算法;处理较大数据集时,采用快速排序算法。

简单来说

策略模式允许您根据实际情况切换使用的算法或策略。

维基百科这样描述

在计算机编程中策略模式Strategy pattern也被称为 Policy pattern是一种允许在运行时选择算法行为的行为型设计模式。

编程示例

翻译上面我们的例子,通过 JavaScript 的头等函数特性,我们可以轻松实现这两种策略

<<< @/fragments/javascript/strategy.js#Sort

接着我们定义了客户端,它将决定使用何种策略

<<< @/fragments/javascript/strategy.js#sorter

最后,可以这样使用它

<<< @/fragments/javascript/strategy.js#useSorter

💢 状态模式 / State

现实生活中的例子

想象您正在使用某种绘画应用,您选择了一种画笔来画图。现在,画笔将根据您选择的颜色来改变它的行为:如果您选择了红色,那么将以红色画图,如果选择了蓝色则以蓝色画图。

简单来说

状态模式允许您在状态变化时,改变类的行为。

维基百科这样描述

状态模式是一种以面向对象的形式,实现状态机的行为型设计模式。在状态模式中,通过将每个单独的状态实现为状态模式接口的派生类,并通过调用此模式超类定义的方法实现状态转换,最终实现了状态机。 状态模式可以看作能够通过调用此模式的接口所定义的方法,来改变当前策略的策略模式。

编程示例

以文本编辑器为例,它允许您改变输入文本的状态:如果您选择了加粗,就开始输入加粗的文本,如果选择了斜体则输入斜体的文本等等

首先我们编写了改变输入文本状态的方法

<<< @/fragments/javascript/state.js#inputCase

接着我们编写了文本编辑器类

<<< @/fragments/javascript/state.js#TextEditor

最后可以像这样使用它

<<< @/fragments/javascript/state.js#useTextEditor

📒 模板方法模式 / Template Method

现实生活中的例子

假设我们打算盖一些房子。建房子的步骤可能像这样

  • 打地基
  • 筑墙壁
  • 盖屋顶
  • 加楼层

上述步骤的先后顺序不能被改变:在筑好墙壁之前您没法盖屋顶。但您可以修改任意步骤内部的实现细节,例如可以使用木头、聚酯纤维或石头来筑建墙壁。

简单来说

模板方法模式定义了如何执行某些算法的框架,但将这些步骤的具体实现推迟到了子类。

维基百科这样描述

在软件工程领域,模板方法模式是一种行为型设计模式。模板方法是超类中的方法,通常是抽象超类,它根据大量的高级步骤来定义操作的框架。这些步骤本身由与模板方法中相同的类中附加的帮助类方法实现。

编程示例

想象我们在编写一个工具,它能帮助我们测试代码,检查代码格式,构建应用,生成构建报告(即代码覆盖率,代码格式检查结果等)以及将我们的应用部署到测试服务器上

首先我们编写了基类,它指定了构建算法的框架

<<< @/fragments/javascript/templateMethod.js#Builder

现在我们编写了它的具体实现

<<< @/fragments/javascript/templateMethod.js#extendBuilder

最后可以这样使用它

<<< @/fragments/javascript/templateMethod.js#useExtendedBuilder

🚦 尾声 / Wrap Up Folks

到这里教程的正文内容就结束了。我会持续改进这篇教程,也许您以后想要重新访问这个仓库来查看最新的内容,不妨点一个关注以及星标。对了,我计划撰写同样风格的软件体系架构教程,希望您继续关注。

👬 参与贡献 / Contribution

  • 反馈问题
  • 拉取请求,改进文档
  • 分享此仓库

🔑 项目许可 / License

MIT © LolipopJ

本项目基于 Design Patterns for Humans,案例的 JavaScript 代码来自 JavaScript Design Patterns for Humans