我们现在深入探讨结构设计的细节,这是软件架构师的关键职责之一。结构设计主要包括两个活动:架构特性分析(本章将讨论)和逻辑组件设计(将在第8章中讨论)。架构师可以按任何顺序(甚至并行)执行这两个活动,但它们将在一个关键的结合点汇合。
当公司决定通过软件解决特定问题时,它会收集该系统的需求列表(有许多技术可以用来获取这些需求,具体内容将在第8章中讨论)。在本书中,我们将这些需求称为问题领域(或简称领域)。你在第1章中了解到,架构特性是系统的关键方面,这些方面与问题领域无关,但对系统的成功至关重要。在本章中,我们将深入定义这一术语,并讨论具体的架构特性。
架构师通常会在定义领域时进行合作,但还必须定义、发现并分析所有与领域功能直接无关的软件功能:架构特性。架构师在定义架构特性时的角色是软件架构与编码和设计的一个重要区别。架构师在设计软件解决方案时,还必须考虑许多其他因素,如图4-1所示。
“非功能性需求”这一术语的长期存在
许多组织用各种术语来描述架构特性,其中包括“非功能性需求”这一术语,这一术语最初是为了将架构特性与功能性需求区分开来。我们不喜欢这个术语,因为它自贬,并且从语言角度来看有负面影响:如何说服团队关注一些被称为“非功能”的东西呢?另一个流行的术语是“质量属性”,我们也不喜欢这个词,因为它暗示的是事后对质量的评估,而不是设计。
我们更喜欢使用“架构特性”这一术语,因为它描述了对架构成功至关重要的关注点,从而也影响整个系统的成功,同时不会低估这些关注点的重要性。在《Head First Software Architecture》(O'Reilly,2024)一书中,我们将架构特性称为系统的能力;与此相对,领域则代表系统的行为。
有时,某些术语会变得“固守不变”,而“非功能性需求”似乎是软件架构师中一个特别“粘”的术语,它仍然在许多组织中很常见。这个术语最早出现在软件工程文献中是在1970年代末期,大约与功能点分析同时出现,功能点分析是一种将系统需求分解为“功能点”的估算技术,每个功能点代表某种工作单元。理论上,团队可以在分析过程结束时将所有功能点相加,从而获得对项目的某些洞察。遗憾的是,它提供了一种虚假的确定性,但由于许多估算方案的主观性问题,功能点分析已经不再使用。
然而,从那个时代留下的一个洞察是,创建一个系统的大部分工作涉及的是该系统的能力,而不是其需求。他们将这些工作称为“非功能点”,这也导致了“非功能性需求”这一术语的普及。
架构特性与系统设计
要被视为架构特性,一个需求必须满足三个标准。它必须指定一个非领域的设计考虑因素,影响设计的某些结构方面,并且对应用的成功至关重要或具有重要性。我们定义的这些相互关联的部分在图4-2中进行了说明,其中包括这三个组成部分以及一些修饰词。
让我们更仔细地看看这些组成部分:
架构特性指定了一个非领域的设计考虑因素
软件架构中的结构设计包括架构师的两个活动:理解问题领域和发现系统需要支持的功能,以确保成功。领域设计考虑因素涵盖系统的行为,而架构特性定义了系统的能力。综合来看,这两个活动定义了结构设计。
设计需求指定了应用程序应该做什么,而架构特性指定了如何实现这些需求以及为什么做出某些选择:简而言之,就是项目成功的操作和设计标准。
例如,特定的性能水平通常是一个重要的架构特性,但通常不会出现在需求文档中。更为相关的是:没有任何需求文档明确说明设计必须“防止技术债务”,但这是一个常见的设计考虑因素。我们在《从领域关注点中提取架构特性》一章中深入讨论了显性和隐性特性之间的区别。
架构特性影响设计的某些结构方面
架构师尝试在项目中描述架构特性的主要原因是为了提取重要的设计考虑因素。架构师是否能通过设计来实现它,还是这个架构特性需要特殊的结构考虑才能成功?
例如,安全性几乎是每个项目的关注点,所有系统在设计和编码时都必须采取某些基本的预防措施。然而,当架构师确定架构需要特殊的结构来支持安全性时,安全性就上升为一个架构特性。
考虑两个常见的架构特性:安全性和可扩展性。架构师可以通过在单体系统中使用良好的编码习惯来解决安全性问题,包括加密、哈希和加盐等常用技术(本书第6章将讨论架构健壮性函数)。相比之下,在分布式架构(如微服务)中,架构师将构建一个更为强健的服务,并采取更严格的访问协议——这是一种结构化方法。因此,架构师可以通过设计或结构来解决安全性问题。另一方面,考虑到可扩展性:没有多少巧妙的设计可以让单体架构在某个点之后继续扩展。超过这个点,系统必须转向分布式架构风格。
架构师特别关注操作性架构特性(见《操作性架构特性》),因为这些特性最常需要特殊的结构支持。
架构特性对应用程序的成功至关重要或具有重要性
应用程序可以支持大量的架构特性……但不应支持这么多。系统支持的每个架构特性都会增加其设计的复杂性。因此,架构师应努力选择最少的架构特性,而不是最多的。
我们将架构特性分为隐性架构特性和显性架构特性。隐性架构特性很少出现在需求中,但它们对于项目的成功是必要的。可用性、可靠性和安全性几乎支撑着所有应用程序,但它们很少在设计文档中指定。架构师必须利用他们对问题领域的理解,在分析阶段揭示这些架构特性。例如,高频交易公司可能不需要在每个系统中都明确规定低延迟,因为该领域的架构师已经知道其重要性。显性架构特性出现在需求文档或其他具体指令中。
在图4-2中,选择三角形是有意为之:每个定义元素都支持其他元素,而这些元素又支持系统的整体设计。由三角形创造的支点表明这些架构特性通常是如何相互作用的。这也是架构师为何如此频繁地使用“权衡”这一术语的原因。
架构特征(部分列举)
架构特征涵盖了一个广泛的复杂性范围,从低级代码特征(如模块化)到复杂的操作性关注点(如可扩展性和弹性)。虽然并没有真正的普遍标准,尽管人们曾尝试对其进行编纂,但每个组织都会自行解释这些术语。此外,由于软件生态系统变化如此之快,新概念、术语、度量和验证不断涌现,为定义架构特征提供了新的机会。
尽管架构特征的数量庞大且范围广泛,难以量化,但架构师确实会对其进行分类。以下部分描述了其中的一些广泛类别,并提供了一些示例。
操作性架构特征
操作性架构特征涵盖了诸如性能、可扩展性、弹性、可用性和可靠性等能力。表4-1列出了其中一些操作性架构特征。
表4-1. 常见的操作性架构特征
| 术语 | 定义 |
|---|---|
| 可用性 | 系统需要多长时间保持可用;如果是全天候24/7,则需要采取措施确保系统在任何故障发生时能快速恢复运行。 |
| 连续性 | 系统的灾难恢复能力。 |
| 性能 | 系统的性能表现;衡量方式包括压力测试、峰值分析、常用功能的分析和响应时间。 |
| 可恢复性 | 业务连续性要求:在发生灾难时,系统需要多快恢复上线。这包括备份策略和冗余硬件要求。 |
| 可靠性/安全性 | 系统是否需要具备故障安全性,或是否存在关系到生命安全的关键任务。如果系统发生故障,是否会给公司带来巨大财务损失?这通常是一个光谱问题,而非二元选择。 |
| 鲁棒性 | 系统在运行过程中处理错误和边界条件的能力,例如在网络连接或电源中断时的表现。 |
| 可扩展性 | 随着用户或请求数量增加,系统是否能够继续良好地执行和操作。 |
操作性架构特征与运维和DevOps问题有着密切的重叠。
结构性架构特征
架构师负责确保代码结构的合理性。在许多情况下,架构师对代码的质量有单独或共同的责任,包括模块化、可读性、组件之间耦合的控制程度、代码可读性及其他内部质量评估标准。表4-2列出了几个结构性架构特征。
表4-2. 结构性架构特征
| 术语 | 定义 |
|---|---|
| 配置能力 | 最终用户通过界面改变软件配置的难易程度。 |
| 可扩展性 | 架构是否能够容纳扩展现有功能的变化。 |
| 可安装性 | 系统在所有必要平台上安装的难易程度。 |
| 可复用性/重用性 | 系统的通用组件在多个产品之间的复用程度。 |
| 本地化 | 在数据字段的入口/查询屏幕中支持多种语言。 |
| 可维护性 | 修改和增强系统的难易程度。 |
| 可移植性 | 系统在多个平台(如Oracle和SAP DB)上运行的能力。 |
| 可升级性 | 在服务器和客户端上升级到新版的难易程度。 |
云特征
软件开发生态系统不断变化和发展;最近的一个优秀示例是云计算的到来。当本书的第一版出版时,云计算已经存在,但还未普及。如今,大多数系统至少在某种程度上与云系统进行交互。以下是一些云提供商架构特征。
表4-3. 云提供商架构特征
| 术语 | 定义 |
|---|---|
| 按需可扩展性 | 云提供商根据需求动态扩展资源的能力。 |
| 按需弹性 | 云提供商在资源需求激增时的灵活性;类似于可扩展性。 |
| 基于区域的可用性 | 云提供商通过计算区分资源,以增强系统的韧性。 |
| 基于地区的隐私和安全性 | 云提供商在不同国家和地区存储数据的法律能力。许多国家有法律规定其公民的数据存储位置(并且通常限制其存储在该地区外)。 |
为了本书的第二版,我们在每个架构风格章节中添加了描述该风格如何适应和促进云计算考虑的内容。
跨领域架构特征
虽然许多架构特征可以归类为容易识别的类别,但有些特征超出了这些类别,或者无法分类,但它们仍然构成了重要的设计约束和考虑因素。表4-4描述了其中一些特征。
表4-4. 跨领域架构特征
| 术语 | 定义 |
|---|---|
| 可访问性 | 所有用户,包括残障人士(如色盲或听力障碍者)都能轻松访问系统的能力。 |
| 可归档性 | 系统在特定时间后归档或删除数据的限制。 |
| 身份验证 | 安全要求,确保用户的身份真实性。 |
| 授权 | 安全要求,确保用户只能访问应用程序中的某些功能(按用例、子系统、网页、业务规则、字段级别等)。 |
| 法律要求 | 系统运行的立法约束,如数据保护法律(例如GDPR)或财务记录法律(如美国的Sarbanes-Oxley),或有关应用程序构建或部署的任何规定。 |
| 隐私性 | 系统的加密能力,能够隐藏事务数据,即使是公司内部员工(如数据库管理员或网络架构师)也无法查看。 |
| 安全性 | 关于数据库加密、内部系统之间的网络通信加密、远程用户访问身份验证和其他安全措施的规则和约束。 |
| 可支持性 | 应用程序所需的技术支持级别;包括调试错误所需的日志记录和其他设施的要求。 |
| 可用性/可达成性 | 用户使用该应用程序/解决方案达到目标所需的培训水平。 |
任何关于架构特征的列表都会不完整;任何软件项目都可能根据独特的因素发明架构特征。我们刚才列举的许多术语并不精确且具有歧义,有时是因为微妙的差异或缺乏客观定义。例如,互操作性和兼容性可能看起来等同,这在某些系统中确实如此。然而,它们有所不同,因为互操作性意味着与其他系统的集成容易,这反过来要求有发布的、文档化的API。而兼容性则更关心行业和领域标准。另一个例子是可学习性:一种定义是“用户学习使用软件的难易程度”,而另一种定义是“系统能够自动学习其环境,以便使用机器学习算法进行自我配置或自我优化的程度”。
许多定义是重叠的,例如可用性和可靠性。以互联网协议IP为例,IP支持TCP。IP是可用的,但不可靠:数据包可能会乱序,接收方可能需要重新请求丢失的数据包。
没有一个完整的标准列表来定义这些类别。国际标准化组织(ISO)发布了一个按能力组织的列表,它与我们在这里列出的内容有所重叠,但主要是建立了一个不完整的类别列表。以下是一些ISO定义的内容,已更新术语并添加了现代关注的类别:
性能效率 在已知条件下,性能相对于资源使用的衡量标准。包括时间行为(响应、处理时间和/或吞吐率的衡量)、资源利用(使用的资源类型和数量)和容量(超出已设定的最大限制的程度)。
兼容性 产品、系统或组件在共享相同硬件或软件环境下,能与其他产品、系统或组件交换信息和/或执行其必要功能的程度。包括共存性(能高效地执行必要功能,同时共享环境和资源与其他产品)和互操作性(两个或更多系统能够交换和利用信息的程度)。
可用性 用户能有效、高效并令人满意地使用系统以完成其预定目的的程度。包括适用性识别性(用户能识别软件是否符合他们的需求)、可学习性(用户学习如何使用软件的难易程度)、用户错误保护(防止用户犯错)和可访问性(使软件适用于具有广泛特征和能力的人群)。
可靠性 系统在规定条件下、规定时间内能正常运行的程度。此特性包括子类别,如成熟度(软件在正常操作下是否满足可靠性需求)、可用性(软件是否可操作和可访问)、容错性(软件是否能在硬件或软件故障的情况下按预期运行)和可恢复性(软件是否能在故障后恢复任何受影响的数据,并重新建立系统所需的状态)。
安全性 软件保护信息和数据的程度,以确保人们或其他产品或系统根据其类型和授权级别获得适当的数据访问权限。此类特性包括保密性(数据只能由授权访问的人查看)、完整性(软件防止未经授权的访问或修改软件或数据)、不可否认性(是否可以证明某些操作或事件确实发生过)、可追溯性(是否可以追踪用户的操作)和真实性(验证用户身份的能力)。
可维护性 开发者修改软件以改进、修复或适应环境和/或需求变化的有效性和效率的程度。此特性包括模块化(软件是否由离散组件构成的程度)、可重用性(开发者能在多个系统中或构建其他资产时使用某个资产的程度)、可分析性(开发者能多容易地收集关于软件的具体度量)、可修改性(开发者是否能在不引入缺陷或降低现有产品质量的情况下修改软件的程度)和可测试性(开发者和其他人能多容易地测试软件)。
可移植性 开发者能将系统、产品或组件从一个硬件、软件或其他操作或使用环境转移到另一个环境的程度。此特性包括适应性(开发者能多有效且高效地将软件适配到不同或不断变化的硬件、软件或其他操作/使用环境)、可安装性(软件是否能在指定环境中安装和/或卸载)和可替代性(开发者能多容易地用其他软件替代其功能)。
ISO列表中的最后一项涉及软件的功能方面:
功能适配性 此特性表示产品或系统提供的功能在特定条件下满足明确和隐含需求的程度。此特性包含以下子特性:
- 功能完整性:功能集覆盖所有指定任务和用户目标的程度。
- 功能正确性:产品或系统提供正确结果并具备所需精度的程度。
- 功能适当性:功能在完成指定任务和目标中的作用。
然而,我们认为功能适配性不应出现在此列表中。它并不描述架构特性,而是构建软件的动机需求。这表明关于架构特性与问题领域之间关系的思考已发生变化。我们将在第七章中讨论这种演变。
软件架构中的诸多模糊性
在架构师中,持续的挫败感来源之一就是许多关键概念缺乏明确的定义,包括软件架构本身的活动!标准的缺失导致公司为常见的事物定义自己的术语。这常常导致整个行业的混乱,因为架构师们要么使用不清晰的术语,要么更糟的是,使用相同的术语却表示截然不同的含义。
尽管我们希望如此,但我们无法在软件开发界强制推行标准的命名法。然而,为了帮助避免基于术语的误解,领域驱动设计建议组织在员工之间建立并使用一个通用语言。我们遵循并推荐这一建议。
权衡与最坏中的最小架构
我们之前提到,架构师应该只支持那些对系统成功至关重要或重要的架构特性。系统只能支持我们列出的少数架构特性,原因有很多。首先,支持某一架构特性很少是免费的。每个被支持的特性都需要架构师的设计努力、开发人员的实施和维护工作,可能还需要结构性支持。
其次,架构特性彼此之间以及与问题域是协同作用的。尽管我们更希望情况不是这样,但每个设计元素都与其他元素相互作用。例如,为了提高安全性所采取的措施几乎肯定会对性能产生负面影响:应用程序必须进行更多的实时加密、间接操作(用于隐藏秘密)以及可能会降低性能的其他活动。飞机驾驶员在学习飞行直升机时常常会遇到困难,直升机有每只手和每只脚的控制。改变一个控制会影响其他所有控制,因为它们是协同作用的。飞直升机是一项平衡的练习,这很好地描述了选择架构特性时的权衡过程;它们与其他架构特性和领域设计是协同作用的:改变一个特性通常意味着改变另一个。就像我们费力的直升机飞行员一样,架构师必须学会应对这些交织在一起的元素。
第三,正如我们之前讨论的,架构特性缺乏标准定义意味着组织在面对模糊性时会遇到困难。虽然业界永远无法创建一个不可变的架构特性列表(因为新的架构特性不断出现),但每个组织都可以创建自己的列表(或通用语言)并定义出客观的定义。
最后,架构特性的数量不仅在不断增加,而且过去十年里类别的数量也在增加。例如,几十年前,架构师几乎不关心操作性问题,这些问题被认为是一个独立的“黑盒”。然而,随着微服务等架构的流行,架构师和运维人员必须更加紧密和频繁地合作。随着软件架构的复杂化,它往往与组织的其他部分交织在一起。
因此,架构师能够设计一个系统并最大化每一个架构特性是非常罕见的。更多时候,决策往往是几个竞争性关切之间的权衡。
提示
永远不要追求最好的架构;追求最差中的最小架构。
试图支持过多的架构特性会导致泛化的解决方案,这些解决方案试图解决所有业务问题。设计很快变得笨重,因此这样的架构很少能奏效。
努力设计尽可能迭代的架构。架构越容易改变,大家就越不需要担心第一次尝试时发现完全正确的东西。敏捷软件开发的最重要的一课就是迭代的价值;这一点在所有软件开发层面都适用,包括架构。