编写软件模式

我花了很多写作精力来编写模式。我时不时地会被问到为什么我要这样做以及什么构成了一个好的模式。这是一篇简短的文章,介绍我对模式的看法,以及我给那些有兴趣自己编写模式的人的建议。

2006年8月1日



当你查看多个软件系统时,你经常会发现一些相似之处。一组程序元素以大致相同的方式在许多不同的地方协同工作,即使它们被不同的名称和行为上的偶然差异所掩盖。经验丰富的程序员学会了用特定的方式解决常见问题,并从他们以前见过的东西中进行粗略的复制,同时调整这些副本以更好地适应它们的新家。

如果可能,我们希望将这些通用解决方案捕获为库或框架。但通常情况下,差异太大,难以将其表示为单个库。更糟糕的是,我们可能会发现我们希望从用完全不同的编程语言编写的系统中复制解决方案。

因此:在 1990 年代初期,软件界的一群人提出了软件模式的概念,以捕捉这些通用解决方案。通过以某种结构化的格式将它们写下来,我们可以更好地分享这种原本隐含的知识。而且,与库相比,这种写作还可以解释解决方案何时适用以及哪些迹象会导致另一种方法。

什么是模式

模式的常见定义是它是“在特定环境下解决问题的方案”。这个定义一直让我觉得不太有用。

对我来说,模式主要是一种对主题进行分块的方式。分块很重要,因为编写软件需要大量的知识。因此,需要有方法将知识划分,这样你就不需要记住所有知识——你需要的是在需要时能够获取特定块的知识。只有这样,你才需要细节。

解决方案为分块提供了有用的焦点。当一些年轻的渴望学习的程序员向一些经验丰富的资深人士(即任何超过 30 岁的人)询问如何处理特定情况时,听到资深人士说“哦,你需要一个标识映射”。然后,同事可以在一些合适的模式书籍中查找标识映射。

因此,为了使这种分块起作用,每个模式都应该命名一个解决方案。这个解决方案应该具体,至少在我们讨论的层面上应该是具体的。一旦你得到参考,你应该能够离开并使用该模式。如果你成功了,这个名字应该进入这个行业的词汇。这可能需要一段时间才能做到,但当你说到“代理”时,任何合理的专业人士都应该知道你的意思。

模式应该具有重复性,这意味着该解决方案必须适用于许多不同的情况。如果你谈论的是一次性的东西,那么给它添加一个名字就不值得了。

这里有趣的一点是,一个单一的解决方案通常会导致一个重复的模式。这通常发生在你看到两个不同的单一解决方案时,它们在表面上看起来完全不同,但具有更深层的相似性——克里斯托弗·亚历山大称之为“解决方案的核心”。

让我举个例子。我正在查看我们早期的一个 Java Web 项目。在这个项目中,团队不允许使用 JSP。因此,他们编写了一组 Java 类,这些类遍历域对象结构,并为特定域对象生成相应的 HTML。他们注意到,在为字段、表格等输出通用 HTML 结构的代码中出现了重复。因此,他们将所有 HTML 输出代码提取到一个第二个实用程序类中,该类具有像renderField (String label)这样的方法。当他们这样做时,他们注意到,他们可以通过更改实用程序类中的代码来彻底改变整个 Web 应用程序的外观。

后来,我看到了另一个项目。他们使用 XSLT 将 XML 转换为 HTML 页面。但他们需要支持多个组织,这些组织希望以自己的格式显示相同的数据。因此,他们将转换分为两个步骤,首先生成一个中间 XML,其中包含像 field 和 table 这样的元素,第二阶段实际上生成 HTML。他们将为每个组织提供不同的第二阶段。

虽然现在写起来似乎很明显,但当我第一次看到这两个项目时,我感觉到它们的方法有一些相似之处。然而,我花了几个月的时间才理解关键点——将转换分为两个步骤:逻辑页面和物理(HTML)页面。这就是我写成两步视图的“解决方案的核心”。模式最大的智力挑战之一是在所有围绕着真实项目所需的东西中找到并隔离这个核心。

模式与配方

一种流行且非常有效的技术写作形式是食谱风格(例如Perl CookbookRails Recipes)。食谱和模式书籍之间有很多相似之处。两者都强调问题-解决方案风格。

我认为两者之间的最大区别在于构建词汇的概念。食谱往往更具体,通常与特定的编程语言和平台相关联。即使模式与平台相关联,它们也试图描述更通用的概念。

因此,食谱比模式更侧重于问题,而不是解决方案。

虽然我对写作的兴趣在于模式,但这反映了我对一般设计原则的兴趣,而不是对两种风格的相对有用性的判断。两者都因为同一个基本原因而有效——它们基于某人今天想要完成的具体事情进行分块。因此,我发现两者都非常有效。你也可以从中学到伟大的原则,但正是对特定问题的答案把你吸引到桌边。

为什么模式很重要?

当我想到对模式的需求时,我发现特别吸引我的一句话是,对模式的兴趣部分来自“...观察到项目尽管使用了最新的技术,但由于缺乏普通的解决方案而失败”[PLoPD 1]。模式提供了一种组织和命名这些普通解决方案的方法,使人们更容易使用它们。

由于这些解决方案是普通的,因此该领域的专家在模式书籍中不会发现任何新东西。对于这些人来说,模式书籍的最大价值在于帮助他们将这些解决方案传递给他们的同事。

尽管我喜欢模式,但我认为模式并非适用于所有情况。即使在我最新的模式书籍中,我也使用了模式和叙述文本的混合。我认为模式有助于集中叙述,并为我提供了一种将解决方案的细节与对它们的概述讨论分开的好方法。模式是一种交流媒介,就像任何交流技巧一样,有些情况下它们很有效,而有些情况下它们效果很差。实践和熟悉可以帮助你判断区别。

模式的重要组成部分

任何看过模式的人通常都会被大多数模式使用常规形式编写的事实所震惊。一旦你查看两组模式,你就会意识到几乎没有两个模式作者使用相同的形式。不同的模式形式都有其特定的特点,任何模式作者都会倾向于选择一种与他们内在偏好相符的形式。

尽管形式多样,但大多数模式确实具有共同的元素。我将在后面讨论不同的形式,但我认为如果我先介绍一些一般原则,会更容易做到。

模式是解决方案

几乎所有关于模式的写作都包含一个定义,类似于“模式是解决问题的方案”。虽然我并不反对这种说法,但我认为它确实倾向于低估了模式主要关于解决方案的这一点。

我认为有必要说这一点,因为模式周围有一种神秘感。为了消除这种神秘感,我们永远不能忘记编写模式的全部目的是描述一个重复出现的、有用的解决方案。成功与否完全取决于以一种其他人可以在适当的时候复制该解决方案的方式来做到这一点。其他一切都是次要的——这意味着无论我们选择如何编写模式,无论我们采用什么形式——都必须支持这一点。我经常看到模式编写者,包括我自己,迷失在特定的格式中,而忘记了这个简单的优先级。因此,每当写作变得困难时,请记住解决方案才是最重要的。

那么问题呢?好吧,任何解决方案都是解决问题的方案。如果没有相应的问题,你怎么会有解决方案呢?理解问题(或问题,一个模式可以解决多个问题)是理解解决方案的关键部分。思考问题有助于你专注于“解决方案的核心”。它还有助于我们避免陷入过多的工具导向的讨论。因此,理解问题很重要——实际上是至关重要的。但解决方案应该仍然是模式的重点。

一个令人难忘的名字

模式工作的一个有价值的特点是它开发了一种词汇,我们可以用它来谈论如何做事情。通过命名重复出现的解决方案,我们可以逐渐建立一个软件设计的词汇,超越我们通常要处理的技术问题。当我了解到 Java 的监听器和 .NET 的委托是实现观察者模式的方式的一部分时,我更好地理解了它们。名称“观察者”给了我一个钩子,让我可以理解新的技术概念——这些概念在不同的技术中通常有不同的名称。

选择好名字很难,我发现自己一直在修改名字,即使是在截止日期很近的时候。由于它们将成为词汇,因此值得付出很多努力来获得好名字。只要想想那个皱巴巴的资深人士需要说些什么。

因此,名字应该简短,但当然很难想出简洁的名字。我的名字往往是两个或三个词——因为我觉得大多数好的单字名字都被占用了。

如果我看到模式是做某事的替代方法,那么我喜欢使用不同的形容词来修饰一个共同的名词。因此,页面控制器和前端控制器是控制器的两种不同形式。严格来说,它们是 P of EAA 中输入控制器的形式,但我只有在真正需要的时候才会使用三个词(因为我觉得我在单表继承及其替代方案中确实需要这样做)。

我喜欢让我的模式名称是名词短语。模式的宝贵品质之一是它们创造了一种词汇,而用名词来做更容易。动词需要更多的语法变化才能融入散文,这使得一致地使用名称变得更加困难。我喜欢想象一个经验丰富的资深开发人员告诉她的同事:“你需要在这里使用一个<模式名称>”。

为什么以及如何

当我们谈论解决方案时,很容易专注于解决方案本身以及如何应用解决方案。更难谈论的是解决方案何时适用以及什么条件适合它,或不适合它。这就是为什么模式作者强调问题,因为这会让我们专注于模式的触发因素。这也是为什么模式作者谈论力量,因为力量是一种探索模式的适应症和禁忌症的方法。

每当我认为自己有一个模式时,我都会尝试思考何时使用该模式。这通常会让我想到替代模式,这就是为什么我的模式通常成组出现的原因。

我特别怀疑只描述一组备选方案的整个模式语言。P of EAA 的触发因素之一是我对那些谈论用于 J2EE 的“唯一架构”的人感到厌烦。软件系统,即使是在企业应用程序等特定领域内,也生活在一个多元化的世界中。有很多做事的方法,而且通常在某些情况下,大多数方法都是正确的。所以每当你认为“你永远不应该那样做”时,都要认真思考。可能会有一个时间,而且这不仅会让你想到另一个模式,还会帮助你更好地理解你的主要模式。

代码示例

许多人担心模式中的示例,尤其是代码示例。毕竟,模式是关于解决方案的深刻相似之处,这些解决方案每次使用时看起来都不同。确实有理由担心一些读者会将示例视为模式,将模式视为美化的宏。

在我看来,许多人通过示例更好地理解事物。当给出示例时,他们就可以开始抽象出一般原则。这当然是我工作的方式。因此,我宁愿给出示例,并冒着缺乏抽象的风险,也不愿避免示例,而让读者完全迷失在抽象中。

如果你非常担心对模式的特定解释,那么一个有用的方法是使用多个示例。同一个模式的不同示例可以帮助说明共同的线索。不同的示例可以使用同一个平台的不同方法,或者使用不同的平台。

当然,有些人不会看代码示例,因为任何代码示例都包含很多细节。因此,我尝试使我的代码示例可以跳过 - 我的意思是,我编写模式使其在没有代码示例的情况下也能理解。代码示例只是一个奖励。

在编写代码示例时,如何使其复杂化存在一种张力。如果我把它做得太简单,人们可能会认为它不现实,但如果我把它做得太复杂,人们就必须理解一堆与模式无关的东西才能理解模式。如果太复杂,我们就会达到迈克尔·费瑟斯所说的 MEGO 点(“我的眼睛变得模糊”),我完全失去了他们。我宁愿犯太简单的错误。如果我弄清楚了简单的东西,那么我(或其他人)以后可以添加更复杂的东西,以及模式之间的交互。我宁愿人们理解一点点,也不愿他们无法理解很多。这种愿望得到了模式所涉及的块状化的加强 - 读者只需要阅读那个模式就能理解该模式。

常见的模式形式

每个作者都倾向于创建自己独特的模式形式,但某些模式形式已经变得更加知名。这些通常被新作者精确地使用,或者至少作为起点。

亚历山大形式

许多人将克里斯托弗·亚历山大的 模式语言 (APL) 视为模式世界的重要影响。亚历山大写的是建筑,他对软件模式的一些早期倡导者产生了重大影响。他以一种在软件模式世界中被称为亚历山大形式的形式写了他的模式书。除了他书中的模式之外,你也可以在 领域驱动设计 中找到这种形式的良好示例。在网上,一个很好的例子是 Josh Kerievsky 的 知识水龙头 模式。

与许多标准形式一样,我们在实践中实际上看到了亚历山大形式的相当多的变化。我将通过引用 APL 中对该形式的描述来描述它。

为了方便和清晰,每个模式都具有相同的格式。首先,有一张图片,它展示了该模式的典型示例。其次,在图片之后,每个模式都有一个介绍性段落,它通过解释模式如何帮助完成某些更大的模式来为模式设置上下文。然后是三个菱形,标志着问题的开始。在菱形之后,有一个标题,以粗体字显示。这个标题用一两句话概括了问题的本质。在标题之后是问题的正文。这是最长的部分。它描述了模式的经验背景、其有效性的证据、模式在建筑中表现出来的不同方式的范围等等。然后,再次以粗体字显示,就像标题一样,是解决方案 - 模式的核心 - 它描述了解决所述问题所需的物理和社会关系领域,在所述上下文中。这个解决方案总是以指令的形式给出 - 这样你就确切地知道你需要做什么来构建模式。然后,在解决方案之后,有一个图表,带有标签来指示其主要组件。

在图表之后,还有三个菱形,表明模式的主体部分已经结束。最后,在菱形之后,有一个段落将模式与语言中所有那些较小的模式联系起来,这些模式是完成该模式、装饰它、填充它所必需的。

-- [alexander-apl]

APL 中的模式平均每页有六页。

亚历山大形式是一种非常叙事性的形式,标题相对较少。因此,当你阅读它时,它比大多数替代方案更流畅。问题的粗体摘要句和解决方案的粗体摘要句非常突出,并允许你快速浏览大量模式。

在使用亚历山大形式的软件模式中,一个常见的变体(由 泥球 使用)是将主要部分(问题的正文)分成两部分。第一部分,在问题标题之后,扩展了问题及其周围的问题。第二部分被移到解决方案摘要之后,并描述了解决方案的细节。

我听说理查德·加布里埃尔批评了这一点,理由是它迫使你重复很多关于替代方案和权衡的讨论。我以前没有对此过多考虑,但我发现我同意他的观点。切断正文会破坏模式的流程,并使其变得更加零碎,因为你担心问题应该在问题的部分讨论还是解决方案的部分讨论。

大多数模式书籍将模式组织为相对独立的部分,就像参考书一样。 埃文斯 将模式嵌入到一般叙事书籍的流程中。亚历山大形式帮助他做到了这一点,因为模式的流程比更结构化的模式形式更具叙事性。

GOF形式

GOF 形式是用于具有开创性的 四人帮 书籍的形式,它真正将模式推向了软件世界。它是一个非常结构化的形式,将模式分解成许多标题:意图、动机、适用性、结构、参与者、协作、后果、实现、示例代码、已知用途和相关模式。GOF 模式相当大,每页有十二页。

波特兰形式

波特兰形式得名于这样一个事实,即在第一个模式会议上,来自俄勒冈州波特兰的几个人都使用了类似的形式。一个很好的在线示例是 [cunningham-checks]

波特兰形式完全是文本形式,而且非常短,通常每页模式不到一页。几段话描述了问题,然后是“因此”一词,以排版方式强调,然后是几段话描述了解决方案。

Coplien形式

之所以这样称呼,是因为它与吉姆·科普林最密切相关,我也听说它被称为规范形式,尽管我不确定这些人指的是哪个规范。一个很好的在线示例是 [coplien-fault-patterns]

我看到这种形式变化很大。关键要素是针对问题、上下文、力量和解决方案的标题部分。大多数作者也会添加一些额外的部分。每个部分都是几段话,力量部分通常是一系列项目符号。这种形式的模式通常相当短 - 两页左右。

POSA形式

这种形式得名于 POSA 书籍。与 GOF 类似,它是一个非常结构化且相当大的形式,尽管它们的标题不同:摘要、示例、上下文、问题、解决方案、结构、动态、实现、解决的示例、变体、已知用途、后果和另请参见。模式通常只有十多页长。这种形式的一个重要部分是模式之前有一个叙事章节,总结了模式并描述了总体主题。

P of EAA形式

我称之为标准形式有点俏皮,因为除了我之外没有人使用它。但我写模式已经很久了,尝试过各种风格,而这是我最喜欢的风格。它相当叙事性,有几个部分:它是如何工作的、何时使用它以及一个或多个示例。长度平均约为八页,但从一页到十多页不等。 是一个最近的示例。

选择你的模式形式

正如你从常见形式的列表中看到的那样,你可以用很多不同的方式编写你的模式,实际上比这还要多。我只提到了那些通常被提及的 - 每本书通常都会使用自己的形式,而且许多论文展示了更多变化。因此,我的主要建议是记住你的模式形式是一个个人选择。不同的形式适合不同的作者,因为不同的写作风格适合不同的个性。最重要的是找到一种适合你的写作风格以及你想传达的想法的形式。

一个好的第一步是从阅读开始。阅读大量不同的模式书籍和论文。专注于内容,但问问自己哪些形式对你来说最舒服。为了真正欣赏这一点,你需要以从头到尾的风格阅读它们,但也需要查找和跳过它们。你可能已经在其他工作中做了很多次 - 哪些模式对你来说最有效?

一旦你对你喜欢哪种形式(或形式)有了想法,就开始写作。尝试用几种不同的模式形式进行实验。一个有用的练习是用几种不同的形式编写同一个模式,看看哪种形式对你来说最有效。让一些人来审查它们,并告诉你哪些形式对他们来说读起来最舒服。不要害怕在这里尝试,我花了许多年才找到一种适合我的模式形式。

一旦你选择了基本的模式,不要让模式过度影响内容。我注意到这个问题尤其出现在像GOF和POSA这样的结构化模式中——人们觉得他们必须在每个模式的每个标题中都填写一些内容。但并非所有模式都需要相同的处理方式。你会发现一些元素你希望在每个模式中都存在,但许多元素是可选的。与其使用一个弱占位符,不如直接省略它。

一个重要的问题是,你更喜欢叙事风格,还是更喜欢带有许多标题的结构化风格。通常,人们在开始时喜欢标题,因为它指导他们如何写作。我倾向于更喜欢叙事风格,因为它往往会导致更流畅的写作。

模式的大小差异很大。波特兰模式通常会在几段文字中完成一个模式,而POSA可能会持续几十页。你的选择很大程度上取决于你想深入到什么程度。如果你要探索实现问题并提供示例代码,你将不可避免地拥有更长的模式。在这种情况下,更多的结构通常更有用,尽管我即使对于长模式也只使用几个标题。

常见问题

当你坐下来写模式时,许多问题会出现在许多模式作者身上。这些问题不一定有正确答案,但我至少可以给出我对它们的看法。

将模式排列成结构

人们遇到的一个常见问题是如何构建他们正在编写的模式。模式鼓励分块,并且很容易专注于这些块。但是,你如何将这些块整合到有意义的东西中呢?我见过许多人难以找到模式集合的整体结构。

我在这里最大的建议是“不要担心——我也不会”。我更喜欢专注于单个模式,描述我遇到的有趣解决方案。一旦我开始将一组模式组合在一起,我就会考虑如何构建它们,并寻找需要更多模式来覆盖的明显差距。

特别是,你不必在深入模式之前花很多时间试图获得正确的整体结构。我发现,直到我深入描述模式的细节,我才真正理解它们。

记住,最终,拥有大量组织不佳的优秀模式,比拥有一个非常好的结构但下面是弱模式更有价值。

模式和模式语言

我经常被人们围绕模式制造的许多神秘感所困扰。一个常见的引发这种尘埃的领域是模式和模式语言的问题。这通常伴随着“这不是模式语言,它仅仅是模式的目录”。

模式语言背后的想法再次来自亚历山大。这个想法是,你有一套模式,它们具有引导你从一个模式到另一个模式的结构。你从(通常是)一些非常战略性的模式开始,每个模式都会把你带到一个点,在那里你必须决定是否应用其他模式。模式语言有一个连接各种模式的流程。

如果模式语言对你来说很容易,那就很好——但我认为仅仅是一本松散的模式集合的书并不是一件坏事。当然,我的书都不是模式语言,GOF也不是。模式语言也很难写——我见过人们在试图将它们组合在一起时陷入困境。记住,模式的价值在于它们所表达内容的实用性,从这个意义上说,我认为模式语言是一种结构机制——我对上面所说的内容也适用。

(知识水龙头是一个很好的在线模式语言示例。)

模式的粒度

我最担心的问题之一是,我的模式在概念上应该有多大。这不是关于写它们需要多少页,而是关于一个模式涵盖了多少概念范围。

当你开始深入研究模式时,你开始意识到,你经常可以选择将两个相关概念变成单独的模式,或者将它们组合成单个模式的变体。GOF 中的一个很好的例子是代理模式,它描述了四种变体(远程代理、虚拟代理、保护代理和智能指针)。你可以将它们写成四个单独的模式,或者写成一个模式,包含四个变体,或者写成一个摘要模式,包含四个进一步的模式,每个模式对应一个变体。

这个问题没有简单的答案,或者至少如果存在答案,我很想知道是什么。决定模式之间的边界在哪里是我一直在努力解决的最困难的问题之一。

我确实断言,如果你确实将它们分开,不要尝试有一个整体模式。因此,当我研究对象关系模式中映射继承的模式时,我为单表继承、类表继承和具体表继承选择了不同的模式。我没有尝试创建一个整体的“继承映射”模式来将它们联系在一起。这确实意味着在“何时”部分存在一些重复,因为我必须在每种情况下讨论这三种模式之间的权衡。我可以接受一些重复(只是不要复制粘贴文本,每次都用不同的方式写)。在叙述中存在一种联系——这是我这种风格的叙述目的的一部分。

要具体

在模式的评论中,一个常见的问题是,一个为一个领域编写的模式似乎在其他领域也有意义。爱丽丝写了一个关于数据库交互的模式,鲍勃说类似的建议也适用于网络通信,并建议使模式更通用。

总的来说,我抵制这种泛化。关键问题是作者的经验。如果爱丽丝了解数据库但不了解网络编程,那么她编写的模式应该描述数据库情况。读者可能会认为它适用于他们的专业领域,但最终决定是否适用取决于读者。这样的读者比作者更适合做出这样的判断。

任务而不是工具

软件写作的一大弊病是关注工具而不是任务。面向工具的书籍说——“这里有一个工具箱,我会解释如何使用每个工具”。面向任务的书籍说——“这里有一些你需要完成的任务,这里是如何完成它们(在过程中向你展示工具)”。面向工具的书籍更容易编写,特别是对于软件手册,因为很容易查看一个框架(例如)并识别工具。

但面向任务的书籍要好得多。人们不会带着“如何使用这个小部件”的想法来阅读一本书,而是带着需要完成一些任务并试图找出如何执行这些任务的想法来阅读。使用面向工具的书籍,他们会花时间查看可能的工具,看看它们是否有帮助;这很好,但如果他们能够直接找到他们的任务,那就更好了。这就是为什么面向食谱的书籍如此方便——它们专注于任务。

模式总是存在成为面向工具的危险——毕竟,我们使用模式所做的事情是试图识别概念工具。因此,很容易最终将一本书变成一个面向工具的指南,介绍新命名的工具。

这就是问题部分对模式的重要性。虽然最终我们是在识别工具,但我们可以通过认真思考每个模式(工具)解决的问题来减轻面向工具的危险。我们可以自由地根据自己的选择来塑造模式边界,事实上,这种自由正是模式写作如此困难的原因。努力使工作尽可能地面向任务有助于以一种有用的方式绘制这些边界。

这里没有新东西

人们对模式书籍的一个常见抱怨是,它们对经验丰富的开发人员没有新东西可说。这不仅是事实,而且是模式的全部意义。

模式旨在捕获来自该领域的知识,而不是提出原创想法。因此,模式书籍不可避免地不会为那些在该领域工作了一段时间的开发人员添加惊人的新想法。但即使如此,我认为模式书籍对那些不需要学习这些想法的人来说仍然发挥着重要作用。这个作用是帮助经验丰富的人将他们的经验传达给周围经验不足的人。很少有团队完全由经验丰富的开发人员组成。经验丰富的领导者可以做的最重要的事情之一就是传授他们的技能。

因此,如果你正在评估你所擅长的领域的一些模式,不要指望学习新东西。相反,评估它们如何帮助你将你的知识传达给其他人。尝试使用它们,看看它们是否能帮助人们理解重要的概念。

这就是为什么模式书籍也应该经久耐用。软件设计的许多基本原理变化并不快,即使我们的技术发生了变化。因此,如果一本模式书籍很旧,不要太担心。


进一步阅读

尽管递归性接近于可爱,但Meszaros 和 Doble 的模式构成了一套有价值的关于模式写作的进一步建议。

重大修订

2020 年 8 月 3 日:添加了介绍段落,修复了一些链接。

2006 年 8 月 1 日:为首次出版进行了润色。

2003 年 4 月:制作了初稿,但从未发布。