软件架构之原则、风格和实践

简介: 软件架构之原则、风格和实践

好的架构可以更容易的支持业务演进,更容易修改,容错性更好。而糟糕的架构就像一团浆糊,一点小小的改动可能都会对原有功能造成破坏。本文从很高层的角度介绍了什么是糟糕的架构,什么好的架构,以及构建好的架构的一些常用参考模型。原文:Software Architecture — Principles, Practices & Styles[1]


为解决特定的问题设计正确的架构更像是一门艺术而不是科学,其实践在很大程度上依赖于对问题的陈述、环境以及我们对问题的理解。对于任何架构来说,最重要的是在面对业务和规模需求变化时的适应性。下面是我关于将不同架构风格、原则和方法如何结合在一起,以形成一个可以演进的架构的经验。


什么是糟糕的架构?如何识别出糟糕的架构?


为了提高开发速度,开发人员经常会偷懒写一堆乱七八糟的代码,也就是我们通常所说的意大利面条式代码。这些代码经常会隐藏很多 bug,时不时造成功能瘫痪,而重新构建代码的成本甚至比修复现有代码的成本更低。这些代码包含下述特征:


  1. 不必要的复杂性(Unnecessarily Complex)——具有讽刺意味的是,编写复杂的代码很容易,任何人都能做到,但编写简单的代码却很困难。
  2. 刚性/脆弱性(Rigid/Brittle)——因为代码复杂,所以不容易理解,因此维护很困难,即使是很小的代码更改也很容易出错。
  3. 不可测试性(Untestable)——代码耦合紧密,通常不会遵循单一责任原则,难以测试。
  4. 不可维护性(Unmaintainable)——测试覆盖率较低的脆弱代码演变成维护的噩梦。


什么是好的架构?有什么特征?


  1. 简单(Simple)——容易理解。
  2. 模块化/分层/清晰(Modularity/Layering/Clarity)——这点很重要,对某个层进行修改不会影响到其他层,层与层之间的耦合最小。
  3. 灵活/可扩展(Flexible/Extendable)——可以很容易适应新的演进需求。
  4. 可测试/可维护(Testable/Maintainable)——易于测试,方便添加自动化测试,鼓励 TDD 文化,因此更容易维护。


为什么要重视架构、原则和实践?


降低成本(Cost reduction)——虽然最初的开发速度可能会降低,但最终,构建和维护的总成本会降低。


构建最重要的东西(Build what is essential)——我们需要构建最重要和必要的部分。只在必要的时候构建必要的东西,这一点很重要。这种方法通过只构建必要的内容,从而减少代码维护的开销,有助于清理混乱。


优化(Optimization)——优化以获得更好的可维护性,对于开发者和用户来说,优化应该提前完成。


性能优化(Performance Optimization)——在规划和设计可以为性能而演进的系统时,请记住,针对性能的代码级优化应该推迟到 LRT(Last Responsible Time,最后一刻)。


最后负责时间(Last Responsible Time)——LRT 是从精益原则中借鉴的概念,在精益原则中,决策/变更被推迟到某个时间点,超过这个时间点,不做决策的成本将比做决策的成本更高。当需求不够紧迫/重要的时候,设计决策应该推迟到 LRT,这样我们就有足够的知识来做出合理的设计决策。


适应性/进化(Adaptability/Evolution)——当软件不断适应业务和规模的新需求时,总是遵循进化模式。


我们可以使用什么工具、方法和技术?


  1. 精益原则(Lean principles)——构建正确的东西,构建必要的内容。
  2. 敏捷方法(Agile methodology)——建立正确的方法,以敏捷、适应性强、快速响应不断变化的市场需求的方式构建软件。
  3. 测试驱动开发实践和自动化测试(Test-driven development practice & Automated Tests)——测试驱动代码实现,确保可测试的软件设计,支持测试左移,“尽早测试,经常测试”可以帮助实现可维护的代码,消除对无意中破坏现有功能的恐惧。


遵循什么架构风格?


一般来说,没有一种方法是万能的。对于某个问题的设计决策取决于环境,每个设计都是权衡。下面是一些最常用的架构风格,只有需要做出设计决策的人才知道什么组合是最适合的。


  1. 以领域为中心的架构(Domain Centric Architecture)
  2. 以应用为中心的架构(Application Centric Architecture)
  3. Screaming Architecture
  4. 微服务架构(Microservices architecture)
  5. 事件驱动架构(Event-Driven Architecture — EDA)
  6. 命令查询职责分离(Command Query Responsibility Segregation — CQRS)


以领域为中心的架构(Domain Centric Architecture)


领域是模型的中心,其他一切都围绕领域构建的,应用层、表示层、持久层、通知服务、web 服务等等,也就是说,领域是基本的,其他一切都只是一个可替换的实现细节。


这里的域表示系统用户的心智模型,是架构中最稳定的部分,很少发生变化。接下来是嵌入用例的应用程序层,这些用例定义了其他一切。



image.png

图片来源:https://images.app.goo.gl/cW5QmNMxn912DM4D6


有两种领域模型:六边形模型和洋葱模型。本质上,每一个外层都依赖于内层,而内层不知道外层。


image.png

图片来源:https://images.app.goo.gl/dRgpzwPR9w8u5u4t7


优点


  1. 支持领域驱动设计(DDD,Domain-Driven Design)的思想,重点关注域、用户和用例。
  2. 减少域(稳定且更改较少)和实现细节(变更频繁,如表示层、数据库)之间的耦合。


缺点


  1. 初始成本更高,因为必须将更多的时间/思考/讨论用于划分领域与应用层所需的单独模型。
  2. 因为需要更多思考,所以开发人员不喜欢,他们坚持旧的以数据库为中心的三层架构。


以应用为中心的架构(Application Centric Architecture)


一旦定义了域边界,接下来就是应用层。通过应用下面的 SOLID 原则,应用层将更加健壮。


image.png

图片来源:https://images.app.goo.gl/UwBEyStMHaVVJt7u5


抽象(做什么?)——应用程序应该以一种能够通过抽象容纳业务逻辑的方式构建,专注于想做的事情。


解耦(怎么做?)——架构已经完成,实现细节使用依赖注入(DI,dependency injection)插入。依赖注入不仅适用于使用各种设计模式注入业务逻辑,而且尤其适用于注入基础设施元素,如数据库、缓存、通知服务器、外部 web 服务等。


接口/契约(交互)——这种方式自动构建了分层体系结构,每个外部元素都有清晰的接口。职责分离与每一层拥有单一职责相结合可以减少耦合,反过来有助于构建易于测试的代码,这些代码也可以使用 mock 进行单元测试。


同样,应用层不应依赖于任何其他实现细节,只了解它所依赖的领域层。


代码的功能性组织(Screaming Architecture)


体系架构应该突出系统的意图——Uncle Bob


建筑设计图很好的阐明了这一点,每个房间的用途都一目了然。


image.png

图片来源:https://images.app.goo.gl/3am8cnt6BrzFzh3JA


对于后端层——我们可以通过目录结构根据功能进行代码的模块化,具有功能内聚性的代码放在一起。每个模块都可以有一个聚合根作为模块的单一入口,因此只要查看聚合根,我们就能够写出模块的所有用例,从而简化模块的功能意图。


image.png

图片来源:https://levelup.gitconnected.com/let-me-hear-you-screaming-architecture-3adcc02f2ca3


对于表示层——可能仍然需要遵循模型/视图/控制器的旧分类方法。表示层应该保持轻量级,没有业务逻辑。这有两个好处:首先,消除重复的逻辑。其次,这样的组织可以帮助初级 UI 开发人员专注于丰富 UI。


微服务架构


过去,我们用统一的领域模型来表示销售上下文和支持上下文中的客户或产品。例如,支持联系人和销售客户被建模为单个 Customer 模型。随着解决方案空间变得越来越大,拥有越来越多的域,我们添加了更多的参数、属性和验证规则,有些规则只适用于一个域,而不适用于另一个域,从而导致统一代码的不必要的复杂性开销。


image.png

图片来源:https://images.app.goo.gl/HHfv3ojn17B5L1dU7


有界上下文(Bounded Context)——识别特定上下文范围,在该范围内,领域模型的特定术语是有效且有意义的。


在新模型中,不必在支持域的“Customer”中定义“Contact”,而是在支持域中使用正确的术语“Contact”。当与 Sales 域对话时,可以使用定义良好的接口将“Contact”转换为“Customer”对象。这样可以提高内聚性,减少跨不同领域的耦合。


微服务定义(Microservices defined)——将单个系统划分为子系统,子系统作为服务承担单一职责,通过明确定义的接口相互通信。每个服务可以自主部署,有自己独立的数据库和支持服务。每个微服务都是独立的,可以选择最适合自己的技术栈、工具和实践。每个服务可以独立缩放。负责每个服务的团队规模相对较小,一个团队可能负责一个或多个微服务。每个团队只需要了解他们负责的微服务的领域知识,而不需要知道整个系统的所有内容。


缺点

  1. 初始成本较高。
  2. 必须有 DevOps 自动化和自动化部署。
  3. 这种分布式计算架构在处理延迟、负载均衡、日志记录、监控、处理最终一致性等方面需要付出额外的时间和成本。

事件驱动架构(Event-Driven Architecture — EDA)


微服务可以通过 REST 调用的请求/响应机制(如 JSON)相互通信,或者使用带有消息代理的事件驱动架构。现代体系架构更喜欢 EDA,这样服务的响应更快、延迟更少,可以提供更健壮、可容错、有保障的服务,并允许更好的可伸缩性。


在 EDA 中有三个参与者,即创建触发事件的生产者、以健壮的方式携带消息的消息代理和可以订阅特定/所有事件的消费者,这些构成了“反应式编程(Reactive Programing)”。这种模式对来自数据流的事件(触发器)做出反应,从而获得更快的响应时间和更低的延迟。


对于需要支持跨微服务事务最终一致性,具有 ACID 属性的微服务,可以使用 SAGA 模式,其支持显式回滚机制来处理错误回滚。不过这种设计会变得更复杂,应该谨慎使用。


EDA 还定义了事件源(Event Sourcing),作为将数据存储在 DB 中的新机制。在这里,DB 对象永远不会被更新。相反,要获取对象的当前状态,需要按照事件到达的顺序进行处理。在事件源中,通过以固定的时间间隔创建当前状态的快照来实现性能优化。

CQRS 模式-命令查询职责分离


微服务和 EDA 的出现也催生了 CQRS 模式,其中“命令”用于修改底层对象的状态,“查询”不修改对象,只是返回请求的对象子集。

这有什么用?以下是一些示例。
  1. 可以在不影响写的情况下提高读的可伸缩性。例如,通过在 MongoDB 中添加更多的辅助节点来满足读取需求,可以选择性地扩展读取能力。
  2. “命令”必须将更新请求发送到数据库,可以选择使用缓存来提供更快的读取。
  3. 有时对象可能属于另一个微服务,而每次查询另一个微服务的开销可能很大,因此可以使用缓存来满足查询需求。数据虽然有重复,但只要维护好数据,并且变化不是很快,就能够在很大程度上减少延迟。这样同时也提高了可用性,即使在其他微服务不可用的情况下,我们的微服务也可以继续正常工作。例如:在订单服务中缓存产品目录。


以上是一些常用的高层设计选择和实践,这些可以和其他一些底层设计(不同设计模式、原则、工具等的组合)一起使用的。所有这些都以一种有意义的方式组合在一起,从而定义一个敏捷的、适应性强的、可扩展的、可维护的、可测试的,当然最重要的是简单的解决方案。



References:

[1] https://sarada-sastri.medium.com/software-architecture-principles-practices-styles-a0263aa11530


目录
相关文章
|
5天前
|
设计模式 监控 持续交付
深入理解微服务架构:从理论到实践
【6月更文挑战第22天】微服务架构作为现代软件开发的基石,其理念和实践已经深入人心。本文将通过一个实际案例,探讨如何将微服务架构的理论应用到实践中去,包括设计原则、技术选型、以及实施过程中可能遇到的挑战和相应的解决策略。我们将看到,尽管微服务带来了许多优势,但在实际应用中也不可避免地会遇到一系列问题,需要开发者具备深厚的技术功底和丰富的实践经验才能妥善应对。
|
3天前
|
人工智能 Cloud Native Java
从云原生视角看 AI 原生应用架构的实践
本文核心观点: • 基于大模型的 AI 原生应用将越来越多,容器和微服务为代表的云原生技术将加速渗透传统业务。 • API 是 AI 原生应用的一等公民,并引入了更多流量,催生企业新的生命力和想象空间。 • AI 原生应用对网关的需求超越了传统的路由和负载均衡功能,承载了更大的 AI 工程化使命。 • AI Infra 的一致性架构至关重要,API 网关、消息队列、可观测是 AI Infra 的重要组成。
|
4天前
|
Kubernetes 监控 Cloud Native
云原生架构下的微服务治理实践
【6月更文挑战第23天】在云计算的浪潮中,云原生架构以其弹性、可扩展性和高效性成为企业数字化转型的重要推手。本文将深入探讨如何利用云原生技术实现微服务的治理与优化,确保系统的稳定性和高可用性。我们将从微服务的基本概念出发,通过具体案例分析,揭示云原生环境下微服务治理的关键策略,并分享实践经验,旨在为读者提供一套完整的微服务治理解决方案。
|
1天前
|
运维 Cloud Native 安全
云原生架构的演进与实践
【6月更文挑战第25天】本文将深入探讨云原生技术从概念提出到实际应用的发展过程,分析其核心价值和面临的挑战。文章将通过具体案例,展示云原生如何促进企业IT架构的现代化转型,并讨论在实施过程中的最佳实践和注意事项,旨在为读者提供一份云原生技术落地的实用指南。
12 2
|
1天前
|
消息中间件 负载均衡 持续交付
探索后端开发:微服务架构的演进与实践
【6月更文挑战第25天】本文深入探讨了微服务架构的概念、发展以及在现代后端开发中的应用。我们将通过一个虚构案例,展示如何将传统的单体应用重构为基于微服务的架构,并讨论在此过程中遇到的挑战和解决方案。文章旨在为读者提供从理论到实践的全面指导,帮助理解微服务架构的优势及其在企业级系统中的应用。
|
3天前
|
运维 负载均衡 Cloud Native
云原生架构下的微服务治理实践
【6月更文挑战第24天】在云原生的浪潮下,微服务治理成为确保系统弹性、可维护性和可观测性的关键。本文通过深入分析微服务治理的核心要素与挑战,结合前沿技术和工具,提出一套实用的微服务治理策略,旨在帮助开发者和架构师构建更加稳定、高效且易于管理的分布式系统。
|
4天前
|
监控 API 数据安全/隐私保护
构建高效后端服务:微服务架构的实践与挑战
【6月更文挑战第23天】在现代软件开发中,微服务架构已成为设计高性能、可扩展后端系统的首选模式。本文将深入探讨微服务的设计原则、实践方法及其面临的技术挑战,旨在为开发者提供一个全面的微服务实施指南。
18 3
|
2天前
|
Kubernetes Cloud Native API
云原生架构的演进与实践
在数字化转型的浪潮中,云原生技术以其灵活性、可扩展性和弹性成为企业IT战略的核心。本文将深入探讨云原生架构的关键组件、设计原则以及如何在实践中有效应用这些概念以支持现代业务需求。通过分析容器化、微服务、持续集成/持续部署(CI/CD)和声明式API等技术的应用,本文旨在为读者提供一套全面的云原生实施指南,助力企业构建更加灵活、高效的IT基础设施。
10 0
|
4天前
|
监控 Cloud Native API
云原生架构下的微服务治理实践
【6月更文挑战第23天】在数字化转型的浪潮中,云原生技术以其灵活性、可扩展性和弹性成为企业IT架构升级的首选。本文将深入探讨云原生架构下微服务治理的关键策略与实践,包括服务发现、配置管理、流量控制等核心组件的应用,旨在为读者提供一套完整的微服务治理解决方案,以支撑业务的快速迭代和高可用性需求。
|
6天前
|
监控 Cloud Native 安全
云原生架构下的微服务治理实践
本文旨在深入探讨在云原生环境下,如何有效实施微服务治理。通过分析微服务架构的核心价值与挑战,结合具体的云平台工具和最佳实践,文章详细阐述了服务发现、配置管理、弹性设计等关键治理策略。此外,文章还提供了关于如何在保障系统可观测性的同时,确保安全性和合规性的实用建议。读者将获得一套完整的微服务治理框架,以及在云原生旅程中应对复杂问题的能力提升。

热门文章

最新文章