第8章 可修改性

生存下来的物种,既不是最强壮的,也不是最聪明的,而是最能适应变化的。

——查尔斯·达尔文(Charles Darwin)

变化总会发生。

一项又一项研究表明,典型软件系统的大部分成本产生于其首次发布 之后 。如果说变化是宇宙中唯一不变的东西,那么软件变化不仅持续不断,而且无处不在。进行软件变更,可能是为了添加新功能、更改甚至淘汰旧功能;可能是为了修复缺陷、强化安全或提升性能;可能是为了增强用户体验;可能是为了接纳新技术、新平台、新协议、新标准;也可能是为了让系统协同工作,即便它们原本并非为此设计。

可修改性关乎变化,我们关注它,是为了降低变更的成本与风险。为了规划可修改性,架构师必须考虑四个问题:

  • 什么会发生变更? 系统的任何方面都可能发生变更:系统所实现的功能、平台(硬件、操作系统、中间件)、系统运行的环境(必须与之互操作的其他系统、与外界通信所使用的协议)、系统展现出的特性(性能、可靠性,甚至是未来的修改)以及其容量(支持的用户数量、同时进行的操作数量)。
  • 变更发生的可能性有多大? 人们无法针对所有潜在的变更来规划一个系统,否则系统永远无法完成,或者即便完成了,成本也会过高,而且很可能在其他方面出现质量属性问题。尽管任何事情都 有可能 发生变更,但架构师必须做出艰难抉择,判断哪些变更有可能发生,进而确定哪些变更将得到支持,哪些不会。
  • 何时进行变更以及由谁来进行变更? 在过去,最常见的情况是对源代码进行变更。也就是说,开发人员必须进行变更,然后进行测试,再在新版本中部署。然而现在,何时进行变更的问题与由谁来进行变更的问题交织在一起。终端用户更改屏幕保护程序,显然是对系统的某一方面进行了变更。同样明显的是,这与更改系统以便使用不同的数据库管理系统不属于同一类别。变更可以在实现阶段(通过修改源代码)、编译阶段(使用编译时开关)、构建阶段(通过选择库)、配置设置阶段(通过一系列技术,包括参数设置)或执行阶段(通过参数设置、插件、硬件分配等)进行。变更可以由开发人员、终端用户或系统管理员来实施。能够学习和自适应的系统,对于何时进行变更以及 “谁” 进行变更这个问题,给出了完全不同的答案 —— 系统自身就是变更的主体。
  • 变更的成本是多少? 提高系统的可修改性涉及两类成本:
    • 引入使系统更具可修改性的机制的成本。
    • 使用这些机制进行修改的成本。

例如,进行变更最简单的机制是等待变更请求到来,然后修改源代码以满足请求。在这种情况下,引入机制的成本为零(因为没有特殊机制),使用该机制的成本就是修改源代码并重新验证系统的成本。

在另一个极端,像用户界面生成器这样的应用程序生成器,它将通过直接操作技术生成的设计好的用户界面描述作为输入,然后可能生成源代码。引入这种机制的成本就是获取用户界面生成器的成本,这可能相当可观。使用该机制的成本包括生成输入提供给生成器的成本(此成本可能很高,也可能微不足道)、运行生成器的成本(几乎为零),以及最后对结果进行的任何测试的成本(通常远低于手工编码)。

在这个范围的更远处,是能够发现自身环境、学习并自我修改以适应任何变化的软件系统。对于这些系统,进行修改的成本为零,但实现和测试学习机制的成本可能相当高,而这种能力正是通过这些成本换来的。

对于 次类似的修改,一种变更机制的简化合理性判断是:

N × 不使用该机制进行变更的成本 ≤ 创建该机制的成本 + (N × 使用该机制进行变更的成本)

这里,N 是预期将使用该可修改性机制的修改次数,但这也是一种预测。如果实际发生的变更次数少于预期,那么昂贵的修改机制可能就不合理。此外,创建可修改性机制的成本本可以用在其他方面(机会成本),比如添加新功能、提升性能,甚至用于非软件方面的投资,如招聘或培训。而且,这个等式没有考虑时间因素。从长远来看,构建一个复杂的变更处理机制可能成本更低,但你可能等不及它完成。然而,如果你的代码经常被修改,不引入一些架构机制,只是在已有变更之上不断叠加变更,通常会导致大量的技术债务。我们将在 第 23 章 中探讨架构债务这一主题。

变化在软件系统的生命周期中如此普遍,以至于针对特定类型的可修改性都有了专门的名称。以下突出介绍一些常见的类型:

  • 可扩展性 是关于容纳更多的某种事物。就性能而言,可扩展性意味着添加更多资源。性能可扩展性有两种类型:横向可扩展性和纵向可扩展性。横向可扩展性(向外扩展)是指为逻辑单元添加更多资源,例如向服务器集群中添加另一台服务器。纵向可扩展性(向上扩展)是指向物理单元添加更多资源,例如为单台计算机添加更多内存。这两种扩展方式都会出现的问题是,如何有效地利用这些额外资源。所谓 “有效”,是指额外资源能够使系统的某些质量得到可衡量的提升,添加资源时不需要付出过多努力,并且不会过度干扰系统运行。在基于云的环境中,横向可扩展性被称为 弹性 。弹性是一种使客户能够从资源池中添加或移除虚拟机的属性(有关此类环境的进一步讨论,请参阅 第 17 章)。
  • 可变性 是指系统及其支持构件(如代码、需求、测试计划和文档)能够以预先规划的方式支持生成一组彼此不同的变体的能力。可变性在产品线中是一个尤为重要的质量属性,产品线是一组相似但在特性和功能上有所不同的系统。如果与这些系统相关的工程资产能够在产品线的成员之间共享,那么整个产品线的成本就会大幅下降。这可以通过引入一些机制来实现,这些机制允许选择构件,并使其适应产品线范围内不同产品环境中的使用。软件产品线中可变性的目标是在一段时间内便于构建和维护该产品线中的产品。
  • 可移植性 是指为在一个平台上运行而构建的软件,能够轻松更改以在不同平台上运行的程度。实现可移植性的方法是尽量减少软件对平台的依赖,将依赖关系隔离到明确标识的位置,并编写软件使其在 “虚拟机”(例如 Java 虚拟机)上运行,该虚拟机封装了所有平台依赖。描述可移植性的场景涉及将软件迁移到新平台,要求付出的努力不超过一定水平,或者计算软件中必须更改的位置数量。处理可移植性的架构方法与处理 可部署性 的方法相互交织,我们将在 第 5 章 中讨论可部署性这一主题。
  • 位置独立性 是指在分布式软件中,两个部分进行交互时,其中一个或两个部分的位置在运行时之前是未知的。或者,这些部分的位置可能在运行时发生变化。在分布式系统中,服务通常被部署到任意位置,这些服务的客户端必须动态发现它们的位置。此外,分布式系统中的服务在部署到某个位置后,通常必须使其位置可被发现。为实现位置独立性而设计系统,意味着位置易于修改,且对系统其他部分的影响最小。

8.1 可修改性通用场景

基于这些考虑,我们可以构建可修改性的通用场景。表 8.1 总结了该场景。

表 8.1 可修改性通用场景

场景部分 描述 可能的值
来源 引发变更的主体。大多数情况下是人类行为者,但系统也可能是具有学习或自我修改能力的,在这种情况下,变更源就是系统自身。 终端用户、开发人员、系统管理员、产品线负责人、系统自身
触发事件 系统需要适应的变更。(在此分类中,我们将修复缺陷视为一种变更,即对原本可能无法正常工作的部分进行更改。) 添加/删除/修改功能的指令,或更改质量属性、容量、平台或技术的指令;向产品线添加新产品的指令;将服务位置更改到另一地点的指令
构件 被修改的构件。具体的组件或模块、系统平台、用户界面、运行环境,或与之交互的其他系统。 代码、数据、接口、组件、资源、测试用例、配置、文档
环境 进行变更的时间或阶段。 运行时、编译时、构建时、初始化时、设计时
响应 进行更改并将其整合到系统中。 以下一项或多项:
<li>进行修改
<li>测试修改
<li>部署修改
<li>自我修改
响应度量 为实现变更所耗费的资源。 成本涉及以下方面:
<li>受影响工件的数量、规模、复杂程度
<li>工作量
<li>耗费时间
<li>资金(直接支出或机会成本)
<li>此次修改对其他功能或质量属性的影响程度
<li>引入的新缺陷
<li>系统完成适配所需时长

图8.1 展示了一个具体的可修改性场景:一位开发人员希望更改用户界面。此更改将在设计时对代码进行,完成更改并测试耗时将不到三小时,且不会产生副作用。

A sample concrete modifiability scenario is presented.

图8.1 具体可修改性场景示例

8.2 可修改性的策略

用于控制可修改性的策略,其目标在于控制变更的复杂程度,以及进行变更所需的时间和成本。图 8.2 展示了这种关系。

The goal of modifiability tactics to control the response diagram is presented. The stimulus is the changes that arrive. The response is the changes made within time and budget.

图 8.2 可修改性策略的目标

为了理解可修改性,我们从软件设计中一些最早且最基本的复杂度度量(耦合和内聚)入手,它们最早在 20 世纪 60 年代就有相关描述。

一般来说,影响单个模块的变更,相较于影响多个模块的变更,实施起来更容易且成本更低。然而,如果两个模块的职责在某种程度上存在重叠,那么单个变更很可能会同时影响这两个模块。我们可以通过衡量对一个模块的修改传播到另一个模块的概率,来量化这种重叠。这种关系被称为 “耦合”,高耦合是可修改性的大敌。降低两个模块之间的耦合度,将降低任何影响其中一个模块的修改的预期成本。降低耦合的策略是在原本高度耦合的两个模块之间设置各种类型的中介。

“内聚” 衡量的是模块职责之间的关联紧密程度。通俗来讲,它衡量的是模块的 “目的一致性”。目的一致性可以通过影响模块的变更场景来衡量。模块的内聚性是指影响某一职责的变更场景也会影响其他(不同)职责的概率。内聚性越高,给定变更影响多个模块的概率就越低。高内聚有利于可修改性,低内聚则不利于可修改性。如果模块 A 的内聚性较低,那么可以通过去除不受预期变更影响的职责来提高内聚性。

影响变更成本和复杂度的第三个因素是 “模块的规模”。在其他条件相同的情况下,模块越大,变更就越困难、成本越高,并且更容易出现错误。

最后,我们需要关注变更发生在软件开发生命周期中的哪个阶段。如果忽略为修改准备架构的成本,我们希望变更尽可能晚地绑定。只有当架构做好了适当准备以适应变更时,才能在生命周期后期成功进行变更(即快速且低成本地进行)。因此,可修改性模型中的第四个也是最后一个参数是 “修改的绑定时间”。平均而言,一个为在生命周期后期适应变更而做好充分准备的架构,其成本会低于一个迫使在早期进行相同修改的架构。系统的准备程度意味着,对于在生命周期后期发生的变更,某些成本将为零或非常低。

现在我们可以理解,各种策略及其产生的影响,会作用于这些参数中的一个或多个:减小模块大小、提高内聚性、降低耦合度以及推迟绑定时间。这些策略如 图8.3 所示。

A flowchart of the modifiability tactics.

图8.3 可修改性策略

提高内聚

有几种策略涉及在模块之间重新分配职责。采取这一步骤是为了降低单个变更影响多个模块的可能性。

  • 拆分模块。如果正在被修改的模块包含内聚性不强的职责,那么修改成本可能会很高。将该模块重构为几个内聚性更强的模块,应该会降低未来变更的平均成本。拆分模块不应只是简单地将一半代码行放入每个子模块;相反,应该合理且恰当地得到一系列本身具有内聚性的子模块。
  • 重新分配职责。如果职责 A、A′和 A″(所有相似职责)分散在几个不同的模块中,就应该将它们放在一起。这种重构可能涉及创建一个新模块,也可能涉及将职责移动到现有模块。识别要移动职责的一种方法是,假设一系列可能的变更场景。如果这些场景始终只影响模块的某一部分,那么也许其他部分有独立的职责,应该被移动。或者,如果某些场景需要对多个模块进行修改,那么也许受影响的职责应该被组合到一个新模块中。

降低耦合

现在我们来看降低模块之间耦合的策略。这些策略与 第 7 章 中描述的可集成性策略有重叠,因为减少独立组件之间的依赖(为了可集成性)与减少模块之间的耦合(为了可修改性)类似。

  • 封装。见 第 7 章 的讨论。
  • 使用中介。见 第 7 章 的讨论。
  • 抽象公共服务。见 第 7 章 的讨论。
  • 限制依赖。此策略限制给定模块与之交互或依赖的模块。在实践中,通过限制模块的可见性(当开发人员看不到某个接口时,就无法使用它)和授权(只允许授权模块访问)来实现这一策略。在分层架构中可以看到限制依赖策略,其中一层只允许使用更低层(有时只允许使用紧邻的下一层),并且在使用包装器时,外部实体只能看到(并因此依赖)包装器,而看不到它所包装的内部功能。

延迟绑定

由于人的工作几乎总是比计算机工作成本更高且更容易出错,所以尽可能让计算机处理变更几乎总能降低变更成本。如果我们设计的工件具有内在的灵活性,那么运用这种灵活性通常比手工编写特定变更代码成本更低。

参数可能是引入灵活性最广为人知的机制,其使用让人联想到抽象公共服务策略。参数化函数 f(a, b) 比类似的假设 b = 0 的函数 f(a) 更通用。当我们在生命周期中与定义参数不同的阶段绑定某些参数的值时,我们就是在推迟绑定。

一般来说,在生命周期中我们能绑定值的时间越晚越好。然而,设置便于后期绑定的机制往往成本更高,这是一种众所周知的权衡。因此本章前面给出的等式就发挥作用了。只要允许后期绑定的机制具有成本效益,我们就希望尽可能晚地绑定。

以下策略可用于在编译时或构建时绑定值:

  • 组件替换(例如,在构建脚本或 makefile 中)
  • 编译时参数化
  • 面向切面编程

以下策略可用于在部署、启动或初始化时绑定值:

  • 配置时绑定
  • 资源文件

用于在运行时绑定值的策略包括以下几种:

  • 发现(见 第 7 章
  • 解释参数
  • 共享存储库
  • 多态性

将可修改性机制的构建与使用该机制进行修改区分开来,就有可能涉及不同的利益相关者:一个利益相关者(通常是开发人员)提供机制,另一个利益相关者(管理员或安装人员)稍后使用该机制,可能是在完全不同的生命周期阶段。安装一种机制,以便其他人无需更改任何代码就能对系统进行变更,有时被称为将变更 “外部化”。

8.3 基于策略的可修改性调查问卷

基于 8.2 节 中描述的策略,我们可以设计一组受策略启发的问题,如表 8.2 所示。为全面了解为支持可修改性而做出的架构选择,分析师需提出每个问题,并将答案记录在表格中。这些问题的答案随后可作为进一步工作的重点,如文档调查、代码或其他工件分析、代码逆向工程等。

表8.2 基于策略的可修改性调查问卷

策略组 策略问题 支持与否(是/否) 风险 设计决策与定位 推理与假设
提高内聚 你是否通过拆分模块来提高模块的内聚性?例如,如果你有一个庞大且复杂的模块,能否将其拆分为两个(或更多)内聚性更强的模块?        
  你是否通过重新分配职责来提高模块的内聚性?例如,如果一个模块中的职责并非服务于同一目的,那么这些职责应被划分到其他模块中。        
降低耦合 你是否始终如一地进行封装功能操作?这通常包括将受审查的功能隔离开来,并为其引入一个明确的接口。        
  你是否始终使用中介来避免模块之间耦合过紧?例如,如果A调用具体功能C,你可以引入一个抽象层B,在A和C之间进行协调。        
  您是否以系统的方式限制模块之间的依赖关系?还是任何系统模块都可以自由地与任何其他模块进行交互?        
  在您提供若干类似服务的情况下,您是否抽象出通用服务?例如,当您希望您的系统能够跨操作系统、硬件或其他环境变化进行移植时,经常会使用此技术。        
延迟绑定 系统是否会定期推迟重要功能的绑定,以便在生命周期的后期进行替换?例如,是否存在可以扩展系统功能的插件、附加组件、资源文件或配置文件?        

8.4 模式

可修改性模式将系统划分为模块,使得这些模块可以分别开发和演进,模块之间几乎没有交互,从而支持可移植性、可修改性和重用性。可能有比任何其他质量属性更多的旨在支持可修改性的模式。我们在此介绍一些最常用的模式。

客户端 - 服务器模式

客户端 - 服务器模式由一个服务器同时为多个分布式客户端提供服务组成。最常见的例子是一个 Web 服务器为网站的多个同时用户提供信息。

服务器与其客户端之间的交互遵循此顺序:

发现:

  • 通信由客户端发起,客户端使用发现服务来确定服务器的位置。
  • 服务器使用约定的协议响应客户端。

  • 交互:
    • 客户端向服务器发送请求。
    • 服务器处理请求并作出响应。

关于此顺序有几点值得注意:

  • 如果客户端的数量超过单个服务器实例的容量,服务器可能有多个实例。
  • 如果服务器相对于客户端是无状态的,则独立处理来自客户端的每个请求。
  • 如果服务器相对于客户端维护状态,则:
    • 每个请求必须以某种方式标识客户端。
    • 客户端应发送 “会话结束” 消息,以便服务器可以删除与该特定客户端相关的资源。
    • 如果客户端在指定时间内未发送请求,服务器可能会超时,以便可以删除与客户端相关的资源。

优点

  • 服务器与其客户端之间的连接是动态建立的。服务器事先不知道其客户端 - 即服务器与其客户端之间的耦合度低。
  • 客户端之间没有耦合。
  • 客户端的数量可以轻松扩展,并且仅受服务器容量的限制。如果服务器容量超过,则服务器功能也可以扩展。
  • 客户端和服务器可以独立演进。
  • 多个客户端可以共享公共服务。
  • 与用户的交互被隔离到客户端。这一因素导致了用于管理用户界面的专门语言和工具的开发。

权衡:

  • 此模式的实现方式使得通信通过网络(甚至可能是互联网)进行。因此,消息可能会因网络拥塞而延迟,导致性能下降(或至少不可预测)。
  • 对于通过其他应用程序共享的网络与服务器通信的客户端,必须为实现安全性(特别是保密性)和保持完整性做出特殊规定。

插件(微内核)模式

插件模式有两种类型的元素 - 提供核心功能集的元素和通过一组固定接口向核心添加功能的专用变体(称为插件)。这两种类型通常在构建时或之后绑定在一起。

使用示例包括以下情况:

  • 核心功能可能是一个精简的操作系统(微内核),提供实现操作系统服务所需的机制,例如低级地址空间管理、线程管理和进程间通信(IPC)。插件提供实际的操作系统功能,例如设备驱动程序、任务管理和 I/O 请求管理。
  • 核心功能是为其用户提供服务的产品。插件提供可移植性,例如操作系统兼容性或支持库兼容性。插件还可以提供核心产品中未包含的其他功能。此外,它们可以充当适配器,实现与外部系统的集成(见 第 7 章)。

优点

  • 插件提供了一种受控机制来扩展核心产品,并使其在各种环境中有用。
  • 插件可以由与微内核开发人员不同的团队或组织开发。这允许开发两个不同的市场:核心产品市场和插件市场。
  • 插件可以独立于微内核演进。由于它们通过固定接口进行交互,只要接口不变,这两种类型的元素就不会有其他耦合。

权衡:

  • 因为插件可以由不同的组织开发,所以更容易引入安全漏洞和隐私威胁。

分层模式

层模式以这样一种方式划分系统,使得模块可以分别开发和演进,各部分之间几乎没有交互,这支持可移植性、可修改性和重用性。为了实现这种关注点分离,层模式将软件划分为称为层的单元。每一层都是一组提供内聚服务的模块的分组。层之间的 “允许使用” 关系受到一个关键约束:关系必须是单向的。

层完全划分了一组软件,并且每个分区都通过公共接口公开。创建层是为了根据严格的排序关系进行交互。如果(A,B)处于此关系中,我们说分配给层 A 的软件被允许使用层 B 提供的任何公共设施。(在几乎无处不在的垂直排列的层表示中,A 将绘制在 B 上方。)在某些情况下,一层中的模块需要直接使用非相邻的下层模块,尽管通常只允许使用相邻的下层模块。这种较高层的软件使用非相邻下层模块的情况称为 “层桥接”。此模式不允许向上使用。

优点

  • 因为一层被限制仅使用下层,所以下层软件可以更改(只要接口不变)而不影响上层。
  • 较低级别的层可以在不同的应用程序中重用。例如,假设某一层允许跨操作系统的可移植性。此层在任何必须在多个不同操作系统上运行的系统中都有用。最低层通常由商业软件提供 - 例如操作系统或网络通信软件。
  • 因为 “允许使用” 关系受到约束,所以任何团队必须理解的接口数量减少。

权衡:

  • 如果分层设计不正确,实际上可能会碍事,因为没有提供高层程序员所需的低级抽象。
  • 分层通常会给系统带来性能损失。如果从最顶层的函数进行调用,它可能必须遍历许多下层才能由硬件执行。
  • 如果发生许多层桥接实例,系统可能无法满足其可移植性和可修改性目标,而严格分层有助于实现这些目标。

发布 - 订阅模式

发布 - 订阅是一种架构模式,其中组件主要通过异步消息进行通信,有时称为 “事件” 或 “主题”。发布者不知道订阅者,订阅者只知道消息类型。使用发布 - 订阅模式的系统依赖于隐式调用;即,发布消息的组件不会直接调用任何其他组件。组件在一个或多个事件或主题上发布消息,其他组件注册对发布的兴趣。在运行时,当发布消息时,发布 - 订阅(或事件)总线通知所有注册对该事件或主题感兴趣的元素。通过这种方式,消息发布导致其他组件(中的方法)的隐式调用。结果是发布者和订阅者之间的松散耦合。

发布 - 订阅模式有三种类型的元素:

  • 发布者组件。发送(发布)消息。
  • 订阅者组件。订阅然后接收消息。
  • 事件总线。作为运行时基础设施的一部分管理订阅和消息分发。

优点

  • 发布者和订阅者是独立的,因此耦合松散。添加或更改订阅者只需要注册事件,不会对发布者造成更改。
  • 系统行为可以通过更改发布的消息的事件或主题轻松更改,从而可能影响哪些订阅者接收并处理此消息。这个看似小的更改可能会产生很大的影响,因为可以通过添加或抑制消息来打开或关闭功能。
  • 事件可以轻松记录,以便记录和回放,从而重现手动难以重现的错误条件。

权衡:

  • 发布 - 订阅模式的某些实现可能会对性能(延迟)产生负面影响。使用分布式协调机制将改善性能下降。
  • 在某些情况下,组件无法确定接收发布消息需要多长时间。通常,在发布 - 订阅系统中,系统性能和资源管理更难以推理。
  • 使用此模式可能会对同步系统产生的确定性产生负面影响。由于事件导致的方法调用顺序在某些实现中可能会有所不同。
  • 使用发布 - 订阅模式可能会对可测试性产生负面影响。事件总线看似小的更改 - 例如与哪些事件相关联的组件的更改 - 可能对系统行为和服务质量产生广泛影响。
  • 某些发布 - 订阅实现限制了灵活实现安全性(完整性)的可用机制。由于发布者不知道其订阅者的身份,反之亦然,端到端加密受到限制。从发布者到事件总线的消息可以唯一加密,从事件总线到订阅者的消息可以唯一加密;然而,任何端到端加密通信都要求所有涉及的发布者和订阅者共享相同的密钥。

8.5 扩展阅读

软件工程及其历史的认真学习者应该阅读两篇关于可修改性设计的早期论文。第一篇是埃德加・迪克斯特拉(Edsger Dijkstra)1968 年关于 T.H.E. 操作系统的论文,这是第一篇谈论设计使用层的系统以及这种方法带来的可修改性好处的论文 [Dijkstra 68] 。第二篇是大卫・帕纳斯(David Parnas)1972 年的论文,该论文引入了信息隐藏的概念。 [Parnas 72] 建议不是根据模块的功能来定义模块,而是根据其内部化变更影响的能力来定义。

更多关于可修改性的模式在《Software Systems Architecture: Working With Stakeholders Using Viewpoints and Perspectives》[Woods 11] 中给出。

解耦级别指标 [Mo 16] 是一个架构级别的耦合指标,可以深入了解架构在全球范围内的耦合程度。此信息可用于跟踪随时间的耦合情况,作为技术债务的早期预警指标。

在 [Mo 19] 中描述了一种检测模块性违规以及其他类型设计缺陷的全自动方法。检测到的违规可作为重构的指南,以提高内聚性并减少耦合。

打算用于软件产品线的软件模块通常充满了变化机制,允许它们快速修改以用于不同的应用程序 - 即在产品线的不同成员中。产品线中组件的变化机制列表可以在 Bachmann 和 Clements [Bachmann 05]、Jacobson 及其同事 [Jacobson 97] 以及 Anastasopoulos 及其同事 [Anastasopoulos 00] 的作品中找到。

层模式有多种形式和变体 - 例如 “带有边车的层”。 [DSA2] 的 第 2.4 节 对它们进行了分类,并讨论了为什么(令人惊讶的是,对于一个半个多世纪前发明的架构模式)您曾经见过的大多数软件层图都非常模糊。如果您不想购买这本书,那么 [Bachmann 00a] 是一个很好的替代品。

8.6 问题讨论

1. 可修改性有多种形式并且有许多名称;我们在本章的开头部分讨论了一些,但该讨论只是触及表面。查找一个涉及质量属性的 IEEE 或 ISO 标准,并编制一份提及某种形式的可修改性的质量属性列表。讨论差异。

2. 在您为问题 1 编制的列表中,哪些策略和模式对每个特别有帮助?

3. 对于由于问题 2 而发现的每个质量属性,编写一个表达它的可修改性场景。

4. 在许多自助洗衣店中,洗衣机和烘干机接受硬币但不找零。而是由单独的机器找零。在普通的自助洗衣店中,每台找零机对应六到八台洗衣机和烘干机。在这种安排中,您看到了哪些可修改性策略在起作用?关于可用性,您能说些什么?

5. 对于问题 4 中的自助洗衣店,描述似乎是按照所述安排机器的目的的特定形式的可修改性(使用可修改性场景)。

6.第 7 章 中介绍的 “包装器” 是一种常见的有助于可修改性的架构模式。包装器体现了哪些可修改性策略?

7. 其他可以提高系统可修改性的常见架构模式包括黑板、代理、点对点、模型 - 视图 - 控制器和反射。根据其封装的可修改性策略对每个进行讨论。

8. 一旦在架构中引入了中间件,某些模块可能会试图绕过它,无论是无意地(因为它们不知道中间件)还是有意地(出于性能、方便或习惯)。讨论一些防止不希望的绕过中间件的架构手段。也讨论一些非架构手段。

9. 抽象通用服务策略旨在减少耦合,但也可能降低内聚性。讨论。

10. 讨论客户端 - 服务器模式是具有运行时绑定的微内核模式这一命题。