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

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

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

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

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

图片来源:https://images.app.goo.gl/dRgpzwPR9w8u5u4t7
优点
  1. 支持领域驱动设计(DDD,Domain-Driven Design)的思想,重点关注域、用户和用例。
  2. 减少域(稳定且更改较少)和实现细节(变更频繁,如表示层、数据库)之间的耦合。
缺点
  1. 初始成本更高,因为必须将更多的时间/思考/讨论用于划分领域与应用层所需的单独模型。
  2. 因为需要更多思考,所以开发人员不喜欢,他们坚持旧的以数据库为中心的三层架构。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

微服务架构

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

图片来源: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

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术总监,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
微信公众号:DeepNoMind

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容