CH01-模式分类

在计算机编程世界,有多种不同的方式来构建一个解决方案以用于解决某些问题。然而,有些人会考虑是否存在一种正确的方式来实现特定的任务。答案是肯定的;通常总是会存在正确的方式,但是在软件开发领域,通常会存在多种方式来实现一个任务。存在一些将程序员引导向正确方案的因素,基于此,人们往往也能得到预期的结果。这些因素可以包含多种事物—实际使用的语言,算法,可执行文件类型,输出格式,代码结构等等。本书中我们将会选择使用 Scala 语言。当然,有很多种方式来使用 Scala,不过这里我们仅会关注“设计模式”部分。

本章中,我们将会介绍什么是设计模式以及他们存在的意义。我们将会贯穿多种不同类型的设计模式。本书的目的在于提供有用的实例,以帮助你在学习过程中能够轻松运行他们。因此,这里会提供一些环境搭建的细节。下面是我们要涉及的几个首要话题:

  • 什么是一个设计模式以及它们为什么会存在?
  • 主要类型的设计模式及其特性
  • 选择正确的设计模式
  • 搭建开发环境

最后一点可能与设计模式本书关联不大。不过正确的构建项目总归是一个好的想法,同时也能使未来的工作更加轻松。

设计模式

在我们深入 Scala 设计模式之前,需要解释一下它们实际的含义、存在的意义,以及为什么值得去精通它们。

软件是一个宽泛的主题,人们可以用它们来做数不尽的事。乍一看,所做的这些事的是完全不同的—游戏、网站、移动手机应用或用于不同产业的特殊系统。然而软件的构建构过程却有很多相似性。无论需要创建的软件为哪种类型,通常人们都需要解决相似的问题。比如,网站和游戏都需要访问数据库。同时贯穿整个时间,开发者们通过经验来学习如何为不同的任务来组织他们的代码。

设计模式的一个正式定义能够让你理解其在构件优雅高效关键中的实际价值。

The formal definition for design patterns

一个设计模式是一个用于解决反复出现的问题的可复用解决方案。它并非一段已完成的代码片段,而是作为一个模板,用于帮助解决一些特殊问题或一些列问题。

设计模式是软件开发社区通过很长的时间总结出的最佳实践。它们被认为能够帮助编写高效、可读、易测试、易扩展的代码。在某些情况下,它们可能是某些表达不够优雅的编程语言(比如 Java..)的目标实现的结果。这意味着大多特性丰富的语言可能甚至都不需要一个设计模式。Scala 就属于这种富特性语言,有些情况下,它会让一些设计模式的应用显得过时或者更加简洁。我们将会在本书中看到它是如何做到这一点的。

一个编程语言中的功能缺陷或优势同样决定了其是否能够实现相比其他语言更多的设计模式。

Scala 和设计模式

Scala 是一个混合型语言,它同时结合了面向对象和函数式编程。这不仅让他能够拥有一些经典的面向对象设计模式,同时提供了多种不同的方式来利用其特性以编写高效、可读、易测试、易扩展的代码。这种混合的性质可以使用更加清晰的技术让一些经典的设计模式变得过时,或者让不能成为可能。

设计模式的需要及其收益

每个人都需要设计模式并在编写代码之前需要深入了解他们。像我们之前提到的,它们能够帮助编写高效、可读、易测试、易扩展的代码。所有这些特性对于行业中的公司来说都是非常重要的。

尽管有些情况下更需要的是快速编写一个原型并落地,然而更常见的情况是软件的一部分代码需要不断的演变。可能你已经拥有一些对已存在的烂代码扩展经验,但无论如何,这是一项挑战并且会花费很长时间,甚至有时会感觉重写会相对简单。此外,这也可能会给系统引入新的 Bug。

代码可读性有些时候也是需要引起重视的。当然,尽管有些人使用了设计模式但仍然会让他的代码难以理解,但通常来说设计模式会带来一些帮助。大型系统通常是由很多人协同开发,每个人也必须知道到底发生了什么。同时,新加入到团队中的人员也能更简单快速的融入进来,如果他们是基于程序中写的很好的部分工作的话。

可测试性有助于在编写或扩展代码时避免开发者引入 Bug。有些情况下,代码可能会很糟或甚至没法测试。设计模式一般也能很好的解决这些问题。

虽然效率通常与算法相关,设计模式同样也能对其造成影响。比如一个对象,需要很长时间才能完成初始化,并且其实例会被应用的各个部分使用,但这是可以使用单例来代替的。你可以在本书的后续章节中看到更多正确的实例。

模式分类

软件开发是一个非常宽泛的话题,这也使得大量的事情可以通过编程来完成。每件事都是不同的,这导致对软件质量的需求也多种多样。所有这些事实导致了大量不同的设计模式被发明。而这又进一步被现有的具有不同功能和表现力水平的编程语言所促进。

本书关注于从 Scala 的视角来看设计模式。如前所述,Scala 是一个混合型语言。这导致一些非常有名的设计模式不再被需要—比如“null 对象”设计模式,可以简单的使用 Scala 中的Option替代。其他的设计模式同过不同的方式也变得可行—装饰器模式可以使用叠加特质实现。最终,一些新的设计模式变得可用,他们尤其适用于 Scala 这种编程语言—比如蛋糕模式、皮条客等。我们将关注所有这些模式,并通过 Scala 的丰富特性使我们的代码更加简洁,从而使这些模式变得更加清晰。

尽管有多种不同的设计模式,但他们都可以被划分为不同的主要分类:

  • 创建型
  • 结构型
  • 行为型
  • 函数式
  • Scala 特有设计模式

一些特定于 Scala 的模式可以被归类到最后一个组。他们均可以作为现有模式的补充或替代。他们是 Scala 独有的,并且利用了一些其他语言中没有的高级语言特性或简单特性的优势。

前三个分类包含了著名的 Gang of Four 设计模式。每本设计模式的书都会涵盖这些,因此我们也不例外。而剩余的部分,尽管它们仍能归类于前三类,但都特定于 Scala 和函数式编程语言。在后续的一些章节中,我们将解释这些分类的主要特点并介绍它们实际所属的设计模式。

创建型设计模式

创建型设计模式用于处理对象的创建机制。它们的目的是在当前的场景中以合适的方式创建对象实例。如果不使用这些模式,将会带来更多不必要的复杂性同时也需要更多的知识。创建型模式背后的思想主要有一下几点:

  • 具体类的知识封装
  • 隐藏实际的创建过程和对象组合的细节

本书中我们将关注如下几种创建型模式:

  • 抽象工厂
  • 工厂方法
  • 惰性初始化
  • 单例
  • 对象池
  • 建造者
  • 原型

下面的一些小节给出了这些模式的简要定义,并会在后续章节中进行深入分析。

抽象工厂

用于封装那些拥有通用主题的单个工厂。使用时,开发者为抽象工厂创建一个具体实现,像工厂模式的使用方式一样使用其方法来创建对象。可以认为是用来帮助创建类的又一层抽象(类-工程-抽象工厂)。

工厂方法

创建实例时无需显式指定实例所拥有的具体类—它会基于工厂在运行时决定。这些工厂可能包括操作系统、不同的数据类型或输入参数。它使开发者能够调用一个方法而不是调用一个具体的构造器。

惰性初始化

用于延迟一个对象的创建或求值过程,直到第一次需要使用它的时候。在 Scala 中它会变得更加简单,而不像 Java 这种 OO 语言。

单例

限制一个类只能创建一个实例。如果应用中的多个类都要使用这样的实例,将会返回同一个实例给所有使用者。这种模式可以通过 Scala 的语言特性轻松实现。

对象池

创建一个已经初始化并等待使用的对象池。一旦有人需要使用池中的对象则会立即返回,使用完成后手动或自动放回到池中。最常用的场景是数据库连接,创建代价较为昂贵,通常是一次创建后服务于不同的客户端请求。

建造者

用于那些拥有大量构造器参数的对象,否则开发者需要为多种不同的创建方式编写多种不同的辅助构造器。这不同于工厂模式,其目的是支持多态。很多种现代化的库都运用了这种模式,Scala 中实现这种模式也非常简便。

原型

这种模式支持在已有的对象实例使用clone()方法来创建对象。用于某些创建起来非常昂贵的特殊资源,或者不愿使用抽象工厂模式的时候。

结构型设计模式

用于在多种不同的实体间建立联系以构造大型的结构。定义了各个组件的组织方式,以灵活的使多个模块在一个大型系统中协作。结构型模式的主要特性如下:

  • 将多个对象的实现结合的组合使用
  • 通过多种不同的组件构建大型系统并维持一个高级别的灵活性

本书中将关注如下几种结构型模式:

  • 适配器
  • 装饰器
  • 桥接
  • 组合
  • 门面
  • 享元
  • 代理

适配器

该模式支持一个接口可以使用一个已存在的类的接口。接入一个客户端希望你的类暴露一个doWork()方法。你可能在其他类中已经有这样的实现了,但是方法的调用方式不同而且不兼容。或许需要更多的参数。或者是一些开发者不拥有修改权限的库。这时适配器可以用于包装这些功能并暴露需要的方法。适配器用于集成已存在的组件。在 Scala 中,可以通过隐式类来轻松实现适配器。

装饰器

适配器是子类化的轻量级替代方案。它支持开发者扩展一个对象的功能而不影响用一个类的其他实例。将已被扩展的类的对象包装到另一个扩展了该类的对象,并重写需要被修改功能的方法。这种模式在 Scala 中可以通过特质叠加来实现。

桥接

用于将一个抽象与其实现之间解耦,从而使各自的变化独立。多用于当一个类和它的功能会经常变化。这种模式使我们想起适配器,但不同在于适配器用于一些已存在的而且无法改变的东西,而桥接则用在构建的时候。桥接可以让我们避免向客户端暴露多种不同的具体类。当我们深入该主题的时候你可能会理解的更清晰,不过现在,假如我们想拥有一个FlieReader类,它要支持多种不同的平台。使用适配器可以避免该类需要为不同的平台提供不同的实现类。在 Scala 中,我们可以使用自类型(self-type)来实现桥接。

组合

组合是一个分区设计模式,表示一组对象能够被当做一个单独的对象处理。它支持开发者能够统一处理单个对象或组合,并使用不复杂的代码来构建复杂的层次结构。比如一个树结构,一个节点可以包含另一个节点,等等。

门面

门面的目的在于为客户端提供一个简单的接口来隐藏系统的复杂性和实现细节。这使代码更具可读性并使外部代码减少依赖。它像是系统的一个简化过的包装器,当然也可以与前面提到的其他模式结合使用。

享元

该模式通过在系统中共享对象来优化内存使用。对象需要包含尽可能多的数据。比如一个单词处理器,每个字符的图形表示与其他相同的字符进行共享,本地信息将会仅包含字符的位置,被在内部保存。

运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。

代理

代理模式支持开发者对其他对象进行包装以提供一个接口。同时能够提供额外的功能,比如安全性或线程安全。该模式可以与享元模式结合使用,那些共享对象的引用可以包装在代理对象的内部。

行为类设计模式

基于对象之间特殊的相互作用方式增加他们之间的灵活性。创建类模式大多描述对象的创建,结构类模式大多用于描述静态结构,而行为模式则描述一个过程或流向。它使这个流程更简单且更易理解。

行为类模式的主要特性:

  • 描述一个过程或流向
  • 流程更加简单且易于理解
  • 完成那些由对象难于或不可能完成的任务

本书将关注的行为类模式:

  • 值对象
  • Null 对象
  • 策略
  • 命令
  • 职责链
  • 解释器
  • 迭代器
  • 中介
  • 备忘
  • 管擦者
  • 状态
  • 模板方法
  • 访问者

值对象

值对象是不可变的并且他们的相等性并不基于他们的标示符(identity),而是基于他们的字段相等性。可以被用作数据传输对象、颜色、货币金额、数字等等。他们的不变型使其多用于多线程编程。Scala 中鼓励不可变性,值对象也作为自然存在的事情(源生提供不可变型对象)。

Null 对象

Null 对象表示值并不存在并定义一个中性的行为。该技术使对 null 引用的检查不再必要并且使代码更简明。Scala 添加了一个可选值的概念,即 Option,可以替代该模式。

策略

该模式支持在运行时选择算法。它定义了一类能够替换的封装算法,给客户端提供一个通用的接口。算法的选择取决于应用运行时能够决定的因素。在 Scala 中,可以向方法传递一个函数作为参数,基于该函数将会执行一个不同的动作。

命令

将一个稍后会触发的动作的信息保存为一个对象,信息中包含了:

  • 方法名
  • 方法的拥有者
  • 参数值

然后客户端来决定调用者在何时执行哪个命令。该模式在 Scala 中可以通过传名参数这种语言特性实现。

职责链

请求的发送者与其接收者进行解耦。这种方式能够使多个对象来接收请求并且保持逻辑分离。接收者组成的链条对请求进行传递,尝试处理,否则传递给下一个接收者。可以变化为一个处理者可以将请求同时分发给多个不同的处理者。

类似于函数组合,在 Scala 中通过叠加特质实现。

解释器

该模式基于语言是否能够通过其静态语法来描述一个著名领域。它定义了每个语法规则的类以解释给定语言中的句子。这些类很可能代表层次,因为语法通常也是分层的。解释器可以用在不同的解析器,比如 SQL 或其他语言。

迭代器

用于当一个迭代器穿过容器并访问其对象时。用于将容器与执行在容器上的算法解耦。迭代器应该提供的是对聚合对象的元素的顺序访问,而不必暴露被迭代集合的内部表示。

中介

封装一个应用中不同类之间的通信。对象通过中介进行通信而不是直接相互作用,以减少他们之间的依赖并解耦。

备忘

提供将一个对象回滚到上一个状态的能力。通过三个对象实现:originator, caretaker, memento。originator 是初始状态,caretaker 会修改 originator,memento 包含 originator 返回的状态。originator 知道如何处理一个 memento 以便存储它的上一个状态。

观察者

支持创建一个发布、订阅系统。有一个特殊的对象称为主题,当它的状态发生任何改变时会通知所有的观察者。在 GUI 工具包中处理事件时比较常用。与响应式编程也相关,比如 Akka。

状态

类似于策略模式,使用一个状态对象封装同一个对象的不同行为。它通过避免大量的条件语句来提高代码的可读性和易维护性。

模板方法

在一个方法中定义一个算法的纲要,然后传递一些实际的步骤到子类。支持开发者在不修改算法结构的前提下修改其中的一些步骤。可以是一个抽象类中的方法并调用其他的抽象方法,这些方法在子类中进行实现。

访问者

表示一个执行在对象结构的元素上的操作。支持开发者在不修改原始类的情况下定义一个新的操作。

Scala 中通过传递函数到方法来实现。

函数式模式

我们将从 Scala 的视角来研究前面所提到的设计模式。这意味着它们会看起来与在其他语言中有所不同,但他们并没有专门的被设计为用于函数式编程。函数式编程比面向对象编程拥有更强的表现力。它拥有其自己的设计模式来帮助开发者活的更轻松。我们将会关注:

  • Monoids(幺半群、幺元)
  • Monads(单子)
  • Functors(函子)

在我们了解过 Scala 的一些函数式编程概念之后将会贯穿这些,我们将会提高一些 Scala 世界中比较有趣的设计模式。

Monoids

Monoid 是一个来自数学的概念。我们会在本书的后续部分深入分析需要理解它的更多理论。现在则只需要记住“Monoid 是一个拥有可结合的二元运算和一个单位元的代数结构”。一些你需要知道的关键字:

  • 可结合的二元运算:(a+b)+c = a+(b+c)
  • 单位元:a+i = i+a = a,这里 i 则为单位元,比如整数中的 0,字符串中的空串““

重要的是 Monoid 为我们提供了一种可能性:使用通用的方式来处理不同类型的值。它支持我们将成对的(两两的)操作适用于序列;结合律让并行化称为可能,单位元元素让我们知道如何处理空列表。Monoid 让我们更易于描述和实现集合。

Monads

在函数式编程中,Monad 是用于将计算描述为步骤序列的结构。Monad 可用于构建流水线,在一切都是不可变的语言中以更清洁的方式添加带有副作用的操作,实现组合。定义可能听起来晦涩难懂,但是通过几个句子来解释 Monad 恐怕是难以实现的。在本书的后续部分,我们会继续关注它并尝试不使用复杂的数学理论来使其更加清晰。我们将会尝试展示为什么 Monad 有用,他们又带来了什么帮助,直到开发者能够很好的理解他们。

Functors

Functor 来自范畴论,和 Monad 一样,正确的解释它恐怕要花点时间。我们将会在本书后续部分继续关注它,现在你可以理解为 Functor 能够支持我们将类型A => B的一个函数提升为类型F[A] => F[B]的函数。

Scala 特定模式

这个分类中的模式可以被归类到之前提到的那些分类中。然而,它们是 Scala 所特有的并利用了一些本书中将会关注的语言特性,因此我们决定将它们放在一个单独的分类中。

这里我们将会关注如下模式:

  • 透镜(lens)模式
  • 蛋糕模式
  • 皮条客(Pimp my library)模式
  • 叠加特质
  • 类型类
  • 惰性求值
  • 偏函数
  • 隐式注入
  • 鸭子类型
  • 记忆(Memoization)模式

透镜模式

Scala 提倡不可变性。对象的不可变可以保证更少的错误。但是有时候可变性在所难免,透镜模式使我们更好的实现可变性。

蛋糕模式

蛋糕模式是 Scala 中实现依赖注入的方式。常被用于实际的应用,有多中库来帮助开发者实现这种模式。Scala 提供了一种使用语言特性实现这种模式的方式,也就是蛋糕模式。

皮条客模式

很多时候开发者需要使用非常泛型的库。但是有时我们需要更接近于我们场景的东西。这种模式支持为这些不可变的库添加扩展方法。或者用于我们自己的库,并且能够提供更好的可读性。

叠加特质

以 Scala 的方式实现装饰器模式。也可以用于组合功能,基于一些 Scala 提倡的特性。

类型类

通过定义一个行为并且能够被指定类型的类的所有成员所支持,来编写泛型代码。比如,所有的数字都必须支持加法和减法操作。

惰性求值

有时需要处理一些耗时或代价昂贵的操作。但是有些时候这些操作最终并不需要。该技术使一些操作只有在需要的时候才会执行。可以用于应用优化。

偏函数

数学和函数式编程非常近似。这些函数被定义为只能处理接收类型的一个子集。

隐式(implicit)注入

基于 Scala 的隐式功能。当对象被需要时进行隐式注入,只要他们在当前作用域。

鸭子类型

Scala 提供的类似于动态语言的特性。

记忆模式

根据输入,通过记住函数结果来提供优化。

如何选择

我们已经见到相当多的设计模式了。在很多场景下他们可以组合使用。不幸的是,对于如何选择概念来设计我们的代码并没有确定的回答。有多种因素会影响最后的决定,你也要问自己下面所列的问题:

  • 这段代码是趋于稳定还是将会被修改
  • 是否需要动态决定算法的选择
  • 代码是否会被他人使用
  • 是否有约定的接口
  • 计划使用哪种库
  • 是否有性能需求或限制

这绝不是一个详尽的问题清单。有大量的因素会指示我们如何来构建系统的决定。然而,真正重要的是要拥有一个清晰的说明(clear specification),如果有些东西看起来没遗漏了,它总是要在第一时间被检查。

在剩余的章节中,我们将会对是否要使用一种设计模式给出一些特定的建议。它们将会帮助你在开始编写代码之前去问正确的问题并作出正确的决定。

总结