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

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

好的架构可以更容易的支持业务演进,更容易修改,容错性更好。而糟糕的架构就像一团浆糊,一点小小的改动可能都会对原有功能造成破坏。本文从很高层的角度介绍了什么是糟糕的架构,什么好的架构,以及构建好的架构的一些常用参考模型。原文: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


目录
相关文章
|
3天前
|
Cloud Native 安全 API
云原生架构下的微服务治理策略与实践####
—透过云原生的棱镜,探索微服务架构下的挑战与应对之道 本文旨在探讨云原生环境下,微服务架构所面临的关键挑战及有效的治理策略。随着云计算技术的深入发展,越来越多的企业选择采用云原生架构来构建和部署其应用程序,以期获得更高的灵活性、可扩展性和效率。然而,微服务架构的复杂性也带来了服务发现、负载均衡、故障恢复等一系列治理难题。本文将深入分析这些问题,并提出一套基于云原生技术栈的微服务治理框架,包括服务网格的应用、API网关的集成、以及动态配置管理等关键方面,旨在为企业实现高效、稳定的微服务架构提供参考路径。 ####
21 5
|
6天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
4天前
|
负载均衡 监控 Cloud Native
云原生架构下的微服务治理策略与实践####
在数字化转型浪潮中,企业纷纷拥抱云计算,而云原生架构作为其核心技术支撑,正引领着一场深刻的技术变革。本文聚焦于云原生环境下微服务架构的治理策略与实践,探讨如何通过精细化的服务管理、动态的流量调度、高效的故障恢复机制以及持续的监控优化,构建弹性、可靠且易于维护的分布式系统。我们将深入剖析微服务治理的核心要素,结合具体案例,揭示其在提升系统稳定性、扩展性和敏捷性方面的关键作用,为读者提供一套切实可行的云原生微服务治理指南。 ####
|
4天前
|
消息中间件 缓存 Cloud Native
云原生架构下的性能优化实践与挑战####
随着企业数字化转型的加速,云原生架构以其高度解耦、弹性伸缩和快速迭代的特性,成为现代软件开发的首选模式。本文深入探讨了云原生环境下性能优化的关键策略与面临的主要挑战,通过案例分析,揭示了如何有效利用容器化、微服务、动态调度等技术手段提升应用性能,同时指出了在复杂云环境中确保系统稳定性和高效性的难题,为开发者和架构师提供了实战指南。 ####
18 3
|
5天前
|
运维 Kubernetes Cloud Native
深入理解云原生架构:从理论到实践
【10月更文挑战第38天】本文将引导读者深入探索云原生技术的核心概念,以及如何将这些概念应用于实际的软件开发和运维中。我们将从云原生的基本定义出发,逐步展开其背后的设计哲学、关键技术组件,并以一个具体的代码示例来演示云原生应用的构建过程。无论你是云原生技术的初学者,还是希望深化理解的开发者,这篇文章都将为你提供有价值的见解和实操指南。
|
4天前
|
Kubernetes Cloud Native 持续交付
云原生技术在现代应用架构中的实践与思考
【10月更文挑战第38天】随着云计算的不断成熟和演进,云原生(Cloud-Native)已成为推动企业数字化转型的重要力量。本文从云原生的基本概念出发,深入探讨了其在现代应用架构中的实际应用,并结合代码示例,展示了云原生技术如何优化资源管理、提升系统弹性和加速开发流程。通过分析云原生的优势与面临的挑战,本文旨在为读者提供一份云原生转型的指南和启示。
19 3
|
4天前
|
运维 Kubernetes Cloud Native
云原生技术在现代应用架构中的实践与挑战####
本文深入探讨了云原生技术的核心概念、关键技术组件及其在实际项目中的应用案例,分析了企业在向云原生转型过程中面临的主要挑战及应对策略。不同于传统摘要的概述性质,本摘要强调通过具体实例揭示云原生技术如何促进应用的灵活性、可扩展性和高效运维,同时指出实践中需注意的技术债务、安全合规等问题,为读者提供一幅云原生技术实践的全景视图。 ####
|
8天前
|
监控 API 持续交付
后端开发中的微服务架构实践与挑战####
本文深入探讨了微服务架构在后端开发中的应用,分析了其优势、面临的挑战以及最佳实践策略。不同于传统的单体应用,微服务通过细粒度的服务划分促进了系统的可维护性、可扩展性和敏捷性。文章首先概述了微服务的核心概念及其与传统架构的区别,随后详细阐述了构建微服务时需考虑的关键技术要素,如服务发现、API网关、容器化部署及持续集成/持续部署(CI/CD)流程。此外,还讨论了微服务实施过程中常见的问题,如服务间通信复杂度增加、数据一致性保障等,并提供了相应的解决方案和优化建议。总之,本文旨在为开发者提供一份关于如何在现代后端系统中有效采用和优化微服务架构的实用指南。 ####
|
10天前
|
消息中间件 设计模式 运维
后端开发中的微服务架构实践与挑战####
本文深入探讨了微服务架构在现代后端开发中的应用,通过实际案例分析,揭示了其在提升系统灵活性、可扩展性及促进技术创新方面的显著优势。同时,文章也未回避微服务实施过程中面临的挑战,如服务间通信复杂性、数据一致性保障及部署运维难度增加等问题,并基于实践经验提出了一系列应对策略,为开发者在构建高效、稳定的微服务平台时提供有价值的参考。 ####
|
10天前
|
Cloud Native API 云计算
云原生架构的深度探索与实践####
本文深入探讨了云原生架构的核心概念、技术特点及其在现代软件开发中的应用实践。通过分析云原生架构如何促进企业数字化转型,提升业务敏捷性与可扩展性,本文旨在为读者提供一个全面而深入的理解框架。我们将从云原生的定义出发,逐步深入到其关键技术组件、最佳实践案例及面临的挑战与解决方案,为开发者和企业决策者提供宝贵的参考与启示。 ####