21、软件架构设计:内聚、耦合与系统演进策略

软件架构设计:内聚、耦合与系统演进策略

在软件系统的设计与演进过程中,有许多关键因素需要考虑,其中内聚和耦合是两个核心概念。了解这些概念以及相关的架构设计策略,对于构建高效、可维护的软件系统至关重要。

1. 内聚类型

在软件设计中,内聚是一个重要的考量因素。虽然人们常常将内聚视为一维的可测量指标,但实际上存在多种类型的内聚,开发者和架构师应该有所了解。常见的内聚类型包括:
- 功能内聚 :模块内的所有元素共同完成一个单一的功能,且该功能是不可或缺的。
- 顺序内聚 :模块内的处理元素按照顺序执行,前一个处理元素的输出是后一个处理元素的输入。
- 通信内聚 :模块内的所有元素都使用相同的输入数据或产生相同的输出数据。
- 过程内聚 :模块内的处理元素按照特定的过程顺序执行,但这些处理元素之间可能没有紧密的逻辑联系。
- 时间内聚 :模块内的所有元素都在同一时间内执行。
- 逻辑内聚 :模块内的元素逻辑上相关,但功能上可能不同。
- 偶然内聚 :模块内的元素之间没有明显的逻辑联系,只是偶然地组合在一起。

2. 松耦合系统

耦合是与内聚密切相关的概念。松耦合系统具有两个重要特性:
- 组件关联弱 :组件之间的关系是可断开的,一个组件的变化不会影响其他组件的功能或性能。
- 组件互知少 :系统中的每个组件对其他独立组件的定义了解甚少或一无所知。

松耦合系统的组件可以用提供相同服务的替代实现来替换,并且不受限于相同的平台、语言、操作系统或构建环境。设计或重构为松耦合的 API 可以使提供者和消费者更有效地演进他们的系统。对于提供者来说,松耦合的 API 可以在整个组织内最大程度地推广其服务;对于消费者来说,松耦合的 API 支持更轻松地替换组件、进行测试,并降低管理依赖的成本。

在测试方面,松耦合的 API 更易于进行模拟和虚拟化。在集成和端到端测试时,可以轻松地替换提供者的实现,用简单的存根或虚拟服务返回所需的响应。而高度耦合的 API 则很难进行模拟或存根,通常需要运行 API 提供者作为测试集的一部分,或者使用轻量级(功能较少)或嵌入式版本的服务。

3. 案例研究:参会者领域边界的建立

以会议系统为例,如果参会者服务与底层数据存储高度耦合,并以底层数据模式的格式暴露数据,当服务提供者想要更换数据存储时,会面临两个选择:
- 实现数据转换系统 :实现一个新系统来适应新旧数据格式之间的转换,这可能需要复杂且容易出错的转换代码。
- 修改外部 API :修改外部 API 并让所有消费者采用新的 API,但对于广泛采用的服务来说,这一过程的难度不容小觑。

4. 信息隐藏的力量

当设计一个既高度内聚又松耦合的 API 时,可以受益于信息隐藏原则。信息隐藏是指隔离那些最有可能发生变化的实现决策。通过提供一个稳定的接口,可以保护系统的其他部分不受底层(可变)实现的影响。在 API 设计中,信息隐藏意味着防止提供者的某些方面被消费者访问,可以通过仅使用专注于业务或领域的 API 端点,避免泄露任何内部抽象、特定于实现的数据模型或模式来实现。

5. 最终状态架构选项

在演进和重新设计单体应用程序和 API 时,需要明确系统在变化后应具备的功能。否则,就会像《爱丽丝梦游仙境》中的场景一样,没有明确的目标,也就无所谓选择的方向。以下是几种常见的架构选项及其对 API 设计的影响:
|架构类型|特点|API 设计挑战|
| ---- | ---- | ---- |
|单体架构|软件系统集成在一个整体中,作为单进程、自包含的应用程序运行。对于许多系统,尤其是概念验证应用程序或正在寻找底层业务产品市场契合度的系统,这种架构风格可以在项目开始时实现最快的进展。|容易意外创建高度耦合的设计,在未来进行修改时才会显现出来。遵循最佳实践,如使用领域驱动设计(DDD)和可能的六边形架构,会有长期收益。|
|面向服务的架构(SOA)|应用程序或服务通过网络向其他组件提供服务。早期的“经典 SOA”由于使用了重量级技术和供应商驱动的中间件,名声不佳。|需要避免使用促进高耦合或低内聚的框架或供应商中间件,例如避免在 API 网关或企业服务总线(ESB)中添加业务逻辑。设计基于 SOA 的系统时,最大的挑战之一是确定服务的大小和所有权,即平衡 API 的内聚性、在整个组织中明确代码的所有权,以及考虑多个服务的设计和运行时成本。|
|微服务架构|软件由通过明确定义的 API 进行通信的小型独立服务组成。与经典 SOA 不同,采用“智能端点和哑管道”,避免使用可能与服务高度耦合的重量级中间件。|设计 API 时,最大的挑战之一是确定 API 和底层服务的边界(和内聚性)。在构建或演进到微服务之前,使用领域驱动设计中的上下文映射和事件风暴等技术,通常会对未来的工作有很大帮助。微服务 API 应理想地使用鼓励松耦合的轻量级技术,如 REST、gRPC 以及轻量级的事件驱动或基于消息的技术,如 AMQP、STOMP 或 WebSockets。|
|函数架构|虽然函数作为微服务的下一次演进的最初承诺尚未完全实现,但这种架构在许多组织中得到了很好的应用。对于高度事件驱动的系统,如基于市场的交易系统或图像处理系统,函数架构是一个有用的目标。|设计基于函数的系统和相应的 API 时,最大的挑战通常与正确处理耦合有关。很容易设计出过于简单的函数或服务,需要将它们编排在一起才能提供业务价值,从而导致这些服务及其 API 变得高度耦合。在可重用性和可维护性之间取得平衡可能很困难,因此在选择这种架构风格时,需要认识到团队可能需要一些时间来适应。|

6. 系统演进过程的管理

系统的演进必须是有意识地管理的活动。在对 API 进行更改时,需要关注以下几个方面:

6.1 确定目标

在尝试演进系统之前,需要明确更改背后的动机。目标应进行分类并与团队和组织清晰沟通。在更改过程早期识别不正确的假设和目标,比在编码开始时发现问题成本更低。目标大致分为两类:
- 功能目标 :由最终用户或业务利益相关者驱动的功能或特性更改请求,可能需要进行重构,但重点是编写更多代码或集成更多系统。
- 跨功能目标 :也称为非功能目标,关注系统的“ilities”,如可维护性、可扩展性和可靠性。例如,可维护性更改通常由技术领导团队推动,旨在减少工程师理解、修复或更改系统所需的时间;可扩展性更改通常由预测系统使用量增加或需求增长的业务利益相关者驱动;可靠性工作通常侧重于减少系统内故障的数量和影响。这些目标通常侧重于重构现有系统或引入新的平台或基础设施组件。

6.2 使用适应度函数

为了避免架构迅速过时,需要积极做出决策以防止系统随时间退化。定义适应度函数是一种对系统架构和组成系统的代码工件进行持续审查的机制。可以将适应度函数视为架构的单元/集成测试,以可量化的指标评估架构的“ilities”。适应度函数被纳入构建管道,以持续保证系统目标的实现。常见的适应度函数类别包括:
|适应度函数类别|描述|
| ---- | ---- |
|代码质量|许多团队可能已经在一定程度上实施了此类适应度函数。通过执行测试可以在代码发布到生产环境之前衡量其质量,还可以考虑其他指标,如最小化循环复杂度。|
|弹性|初步的弹性测试是将系统部署到预生产环境,向其中运行样本(或合成)流量,并观察错误率是否低于某个阈值。可以使用 API 网关或服务网格向系统注入故障,以测试系统在特定场景下的弹性和可用性。|
|可观测性|确保服务符合要求(且不退化),并发布可观测性平台所需的指标类型至关重要。可以通过持续的适应度函数来测量和执行之前讨论过的一组良好的 API 指标。|
|性能|性能测试通常是事后才考虑的,但如果能够设定延迟和吞吐量目标,就可以在构建管道中进行测量。实现这一目标的难点之一是获取类似生产环境的数据,以使性能测试有意义。|
|合规性|这部分内容因业务或组织而异,需要评估哪些内容是关键的监控指标,可能包括审计或数据要求,以证明业务按预期运行。|
|安全性|安全性涉及多个方面,例如分析项目中的库依赖关系,检查是否存在已知漏洞;对代码库进行自动化扫描,确保不存在 OWASP 风格的漏洞。|
|可操作性|许多应用程序在投入生产后开始演进,随着用户的加入,问题也会随之出现。确定操作平台的最低要求是确保平台保持可操作的关键,评估是否有监控和警报机制是一个好的起点。|

创建关于想要引入的适应度函数的架构决策记录(ADR)是一个不错的开始。虽然可能无法立即实施表中列出的所有内容,但识别那些难以逆转的决策很重要。通过使用 ADR 和开放讨论集体做出的决策,将有助于构建具有长久生命力的架构。

7. 系统模块化分解

为了避免代码变成“意大利面条”或“泥球”,可以将软件应用程序分解为模块。在代码库中设计模块化组件有助于根据功能内聚性定义清晰的边界和逻辑分组。模块旨在形成定义良好的边界,隐藏实现细节。在不同的编程语言中,模块的定义方式可能不同,例如在 Java 中,可以选择方法、类、包和模块等不同的构造。

以会议系统为例,可以引入模块来表示控制器、服务和数据访问对象(DAO)模式。每个控制器暴露 RESTful 端点,由托管应用程序的 Web 服务器公开;服务模块包含控制器背后的业务逻辑,并向控制器暴露清晰的接口;DAO 模块包含服务背后的数据访问对象,并向服务暴露清晰的接口。模块分层很常见,实现模块之间明确的单向依赖是模块化的良好应用。

模块化设计具有多个优点,例如每个模块可以独立进行测试,开发人员可以在模块内进行推理和测试。在一个项目中,开发人员将与数据库交互的 DAO 模式作为应用程序的一个模块,通过接口向其他模块暴露功能,使模块之间的交互清晰。后来,将业务逻辑拆分为三个使用 DAO 的独立模块并转换为独立服务,实现了系统的独立演进。

使用 C4 图来表达软件是一种轻量级的组件级方法,有助于定义系统中组件之间的关系。组件图可以帮助审查关系并定义模块化结构。定义应用程序内的模块是一个良好的设计步骤,但实现模块化的方法有很多种,可以利用语言级别的支持来强制执行模块,并与团队商定最适合技术栈的方法。

8. 创建 API 作为扩展“接缝”

“接缝”的概念最早由 Michael Feather 在 2004 年提出。接缝是功能拼接在一起的点,可以看作是一个被考虑的主题与另一个主题交互的点。通常通过依赖注入等技术实现,注入协作者并针对接口执行,以实现可替换性。可替换性的考虑很重要,它允许有效地扩展系统。

在软件架构设计中,内聚、耦合、信息隐藏、架构选型、系统演进管理、模块化设计以及 API 接缝设计等方面相互关联,共同影响着软件系统的质量和可维护性。通过深入理解这些概念并合理应用相关策略,可以构建出更加健壮、灵活和高效的软件系统。

软件架构设计:内聚、耦合与系统演进策略

9. 模块化设计的深入探讨

在前面提到了将软件系统模块化的重要性以及一些基本的模块化设计方法,接下来进一步深入探讨模块化设计的更多细节。

首先,在确定模块边界时,要充分考虑功能的内聚性和耦合性。一个好的模块应该具有高度的内聚性,即模块内的各个元素紧密相关,共同完成一个明确的功能。同时,模块之间应该保持松耦合,这样在修改一个模块时,不会对其他模块产生过大的影响。

另外,模块的粒度也是一个需要权衡的问题。如果模块粒度太小,会导致系统中模块数量过多,增加管理和维护的复杂度;如果模块粒度太大,可能会导致模块内的功能过于复杂,降低内聚性。例如,在一个电商系统中,将商品管理、订单管理、用户管理等分别作为不同的模块是比较合理的,但如果将商品的基本信息管理和商品的库存管理拆分成过于细小的模块,可能会使系统变得复杂。

在实现模块时,可以采用分层架构的思想。例如,在会议系统的例子中,控制器层负责接收外部请求,服务层负责处理业务逻辑,数据访问层负责与数据库交互。这种分层架构可以使模块之间的职责更加清晰,提高系统的可维护性和可扩展性。

下面是一个简单的 mermaid 流程图,展示了分层架构中模块之间的交互:

graph LR
    A[控制器层] --> B[服务层]
    B --> C[数据访问层]
    C --> B
    B --> A
10. API 设计的最佳实践

API 作为系统之间交互的接口,其设计的好坏直接影响到系统的可维护性和可扩展性。以下是一些 API 设计的最佳实践:

  • 遵循 RESTful 原则 :RESTful API 具有简洁、易于理解和扩展的特点。使用标准的 HTTP 方法(如 GET、POST、PUT、DELETE)来表示不同的操作,使用统一的资源标识符(URI)来表示资源。例如, /users 可以表示用户资源, GET /users 可以用于获取所有用户信息, POST /users 可以用于创建新用户。
  • 使用版本控制 :随着系统的不断演进,API 可能需要进行更新。为了避免对现有用户造成影响,应该使用版本控制。可以在 URI 中包含版本号,如 /v1/users /v2/users
  • 提供清晰的文档 :API 文档是用户使用 API 的重要参考,应该提供详细的接口说明、请求参数、响应格式等信息。可以使用工具如 Swagger 来生成 API 文档。
  • 错误处理和状态码 :在 API 设计中,要合理处理错误情况,并返回合适的 HTTP 状态码。例如,400 表示请求参数错误,404 表示资源未找到,500 表示服务器内部错误。
11. 系统演进的风险管理

在系统演进过程中,会面临各种风险,需要进行有效的管理。以下是一些常见的风险及应对措施:

风险类型 描述 应对措施
技术过时风险 随着技术的不断发展,现有的技术栈可能会过时。 定期评估技术栈,适时进行技术升级;关注行业技术趋势,提前进行技术储备。
兼容性风险 系统演进过程中,新的功能或架构可能与现有系统不兼容。 在进行重大变更之前,进行充分的兼容性测试;采用渐进式的演进方式,逐步引入新的功能。
性能风险 系统功能的增加可能会导致性能下降。 在设计和开发过程中,进行性能测试和优化;采用缓存、负载均衡等技术来提高系统性能。
人员风险 团队成员的变动可能会影响系统演进的进度和质量。 建立完善的知识传承机制,确保团队成员之间的知识共享;进行人员培训,提高团队整体技术水平。
12. 持续集成与持续部署(CI/CD)

持续集成与持续部署是现代软件开发中的重要实践,可以提高开发效率和系统的稳定性。

  • 持续集成(CI) :开发人员将代码频繁地集成到共享代码库中,并通过自动化的构建和测试流程来验证代码的正确性。这样可以及时发现代码冲突和错误,减少集成问题。
  • 持续部署(CD) :在代码通过 CI 流程后,自动将其部署到生产环境中。可以使用工具如 Jenkins、GitLab CI/CD 等来实现 CI/CD 流程。

下面是一个简单的 CI/CD 流程的 mermaid 流程图:

graph LR
    A[开发人员提交代码] --> B[代码库]
    B --> C[自动化构建]
    C --> D[自动化测试]
    D --> E{测试是否通过}
    E -- 是 --> F[自动部署到生产环境]
    E -- 否 --> G[反馈给开发人员]
13. 总结

软件架构设计是一个复杂而又关键的过程,涉及到内聚、耦合、信息隐藏、架构选型、系统演进管理、模块化设计以及 API 设计等多个方面。在实际工作中,需要根据具体的业务需求和系统特点,综合考虑各种因素,选择合适的架构和设计方法。

通过合理运用内聚和耦合的原则,设计高度内聚、松耦合的系统,可以提高系统的可维护性和可扩展性。利用信息隐藏原则,可以保护系统的其他部分不受底层实现变化的影响。在架构选型方面,要根据系统的规模、复杂度和业务需求,选择合适的架构类型,如单体架构、SOA、微服务架构等。

在系统演进过程中,要明确目标,使用适应度函数进行评估和监控,同时注意风险管理。模块化设计可以将系统分解为多个独立的模块,提高系统的可管理性和可测试性。API 设计要遵循最佳实践,为系统之间的交互提供清晰、稳定的接口。最后,通过持续集成和持续部署,可以提高开发效率和系统的稳定性。

总之,软件架构设计是一个不断探索和实践的过程,需要不断学习和积累经验,才能构建出高质量、高性能的软件系统。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值