《架构整洁之道》读书笔记(中)

软件架构

第15章  什么是软件架构

软件架构工作的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。

尽可能长时间地保持尽可能多的选项。

软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构让系统便于理解、易于修改、方便维护,并能轻松部署。终极目标最大化程序员的生产力,同时最小化系统的总运营成本。

开发

系统架构的作用就是要方便其开发团队对它的开发 ;不同的团队结构应采用不同的架构设计。

组件对应团队的架构不 太可能是系统在部署、运行及维护方面的最优方案。研发团队只受开发进度驱动,架构设计最终一定倾向于这个方向。

部署

系统部署成本越高,可用性就越低。实现一键式的轻松部署应该是我们设计软件架构的一个目标。

系统的早期开发中通常很少考虑部署策略方面的事情,这常常导致一些易于开发、难于部署的系统架构。

运行

软件架构对系统运行的影响远不及它对开发、部署和维护的影响。任何运行问题都可以通过增加硬件的方式来解决,以避免软件架构的重新设计;同时硬件也远比人力更便宜。

设计良好的软件架构应能明确反映系统在运行时的需求,即设计良好的系统应该可以使开发人员对于系统的运行过程一目了然,架构应起到揭示系统运行过程的作用。用例、功能、系统行为……

维护

维护所需的成本最高。永不停歇的新功能需求,层出不穷的系统缺陷将会占去绝大部分的人力。

系统维护的主要成本集中在“探秘”和“风险”。探秘(spelunking)成本来自对现有软件系统的挖掘,目的是确定新增功能或被修复问题的最佳位置和最佳方式;风险(risk)是指进行上述修改时,总是有可能衍生出新的问题,这种可能性就是风险成本。

保持可选项

软件的灵活性取决于系统整体状况、组件的布置及组件之间的连接方式。

软件维持”软"性的方法就是尽可能长时间地保留尽可能多的可选项。应保留哪些选项?无关紧要的的设计细节。
所有的软件系统都可以降解为策略与细节这两种主要元素。策略体现的是软件中所有的业务规则与操作过程,是系统真正的价值所在。细节是那些让操作系统的人、其他系统以及程序员们与策略进行交互,但又不影响策略本身的行为。包括I/O设备、数据库、WEB系统、服务器、框架、交互协议等。

架构师的目标是创建一种系统形态,以策略为最基本元素,并让细节与策略脱离关系,以允许在具体决策过程中推迟或延迟与细节相关的内容。

一个优秀的系统架构师应该致力于最大化可选项数量。

小结

优秀的架构师会小心地将软件的高层策略与其底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不需要关心底层细节。优秀架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好。

第16章  独立性

良好的软件架构必须支持以下几点:

系统的用例与正常运行

系统的维护

系统的开发

系统的部署

用例

系统架构必须非常直观地支持应用可能涉及的所有用例。

一个设计良好的架构在行为上对系统最重要的作用就是明确和显式地反映系统设计意图的行为,使其在架构层面可见。

运行

架构在支持系统运行方面扮演着更实际的角色。

大吞吐量、低延迟,分布式和微服务的要求。

开发

康威定律:任何一个组织在设计系统时,往往都会复制出一个与该组织内沟通结构相同的系统。

一个由多个不同目标的团队协作开发的系统必须具有相应的软件架构。

部署

一个设计良好的软件架构可以让系统在构建完成之后立刻就能部署。

保留可选项

一个设计良好的架构应该充分地权衡以上所述的所有关注点:用例、运行、开发、部署。

通过遵守一些原则可以帮助我们正确地将系统划分为隔离良好的组件,以便尽可能长时间地为我们的未来保留尽可能多的可选项。

一个设计良好的架构应该通过保留可选项的方式,让系统在任何情况下都能方便地做出必要的变更。

按层解耦

架构师通过采用单一职责原则(SRP)和共同 闭包原则(CCP),以及既定的设计意图来隔离那些变更原因不同的部分,集成变更原因相同的部分。

哪些部分的变更原因不同呢?

用户界面和业务逻辑

业务逻辑可以与应用密切相关,也可以是更具有普适性的

数据库、查询语言、表结构,这些都是系统的技术细节

系统可以被解耦成若干个水平分层——UI界面、应用独有的业务逻辑、领域普适的业务逻辑、数据库等。

用例的解耦

用例本身的变更原因不同,按照用例来切分系统;这些用例也是前述系统水平分层的一个个垂直切片。

如果按照变更原因的不同对系统进行解耦,就可以持续地向系统内添加新用例,而不会影响旧用例;如果同时对支持这些用例的UI和数据库进行了分组,那么每个用例使用的就是不同面向的UI和数据库,因此添加新用例就更不可能影响旧有用例了。

解耦的模式

解耦的动作对架构设计的第二个目标——系统运行——有重要意义……

对于基于服务来构建的架构,通常称之为面向服务的架构(service-oriented architecture)。

并没鼓吹SOA是一种最佳的软件架构,或者微服务是未来的潮流;但有时候我们必须把组件切割到服务这个应用层次。

一个良好的架构要为将来多留一些选项,这里的解耦模式也是可选项之一。

开发独立性

架构设计的第三个目标是支持系统的开发。

系统按照其水平分层和用例进行了恰当的解耦,这个系统的架构就可以支持多团队开发,不管团队组织形式是分功能开发、分组件开发、分层开发,还是按什么变量分工都可以。

部署的独立性

这种按用例和水平分层的解耦也给系统的部署带来极大的灵活性。

如果 解耦做得好,甚至可以在系统运行中热切换(hot-swap)。

重复

看起来重复的代码,如果走的是不同的演进路径,也就是说它们有着不同的变更速率和变更缘由,那么这两段代码就不是真正的重复。

一定要小心避免对任何重复都要立即消除的应激反应模式总,要确保这些消除动作只针对真正意义的重复。

再谈解耦模式

按水平分层和用例解耦一个系统有多种方式。

源代码层次上解耦、二进制层次上解耦(部署),也在可以在执行单元层次上解耦(服务)

源码层次:可以控制源代码模块之间的依赖关系,以实现一个模块的变更不会导致其他模块也需要变更或重新编译。通常是单体结构。

部署层次:可以控制部署单元(jar文件、DLL、共享库等)之间的依赖关系,以实现一个模块的变更不会导致其他模块的重新构建和部署。

服务层次:可以组件间的依赖关系降低到数据结构级别,仅通过网络数据包来通信。

服务层次解耦问题在于成本很高:系统资源成本高昂,而且研发成本更高。

作者倾向于将系统解耦推行到某种一旦有需要就可以随时转变为服务的程度即可,让真个程序尽量长时间地保持单体结构,一遍给未来留下可选项。

一个设计良好的架构应该能允许一个系统从单体结构开始,以单一文件的形式部署,然后逐步成长为一组相互独立的可部署单元,甚至是独立的服务或者微服务。最后还能随着情况的变化,允许系统逐渐退回单体结构。并在此过程中,能保护系统的大部分源码不受变更影响。

小结

一个系统所适用的解耦模式可能会随着时间而变化,优秀的架构师应该能预见这一点,并做出相应的对策。

第17章  划分边界

架构师追求的目标是最大限度地降低构建和维护一个系统所需的人力资源。最消耗人力资源的是什么?系统中存在的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合;是那些与系统的业务需求无关的决策,包括要采用的框架、数据库、Web服务器、工具库、依赖注入等。

通过划清边界,可以推迟和延后一些细节性的决策,最终会为我们节省大量的时间、避免大量的问题。

边界线应该划在那些不相关的事情中间。

一个非常重要的原则,I/O(GUI,一种I/O)是无关紧要的。

插件式架构

软件开发技术发展的历史就是一个如何想法设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。系统的核心业务逻辑必须和其他组件隔离,保持独立,其他组件要么可以去掉的,要么有多种实现的。

《架构整洁之道》读书笔记(中)_第1张图片
图片发自App

插件式架构的好处

将系统设计为插件式架构,就等于构建起一面变更无法逾越的防火墙。只要GUI是以插件形式插入系统的业务逻辑中的,那么GUI发生的变更不会影响到系统的业务逻辑。

边界线应该沿着系统的变更轴来画;位于边界线两侧的组件应该以不同原因、不同速率变化着。

小结

为了在软件架构中画边界线,需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,另一部分是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改,让非核心组件依赖于系统的核心业务逻辑组件。

这是一种对依赖反转原则(DIP)和稳定抽象原则(SAP)的具体应用,依赖箭头应该由底层具体实现细节指向高层抽象方向。

第18章  边界剖析

系统架构是由一系列组件以及它们之间的边界共同定义的。

跨边界的定义

跨边界调用指的是边界线一侧的函数调用另一侧的函数,并同时传递数据的行为。构造合理的跨边界调用需要对源码中的依赖关系进行合理管控。

划分边界,就是在模块之间建立针对这种变更的防火墙。

令人生畏的单体结构

单体系统,源码层次的解耦模式,这类系统的架构边界在部署过程中并不可见,但这些边界的划分对系统各组件的独立开发也是非常有意义的。

这类架构一般都需要利用某种形式的多态来管理体内部的依赖关系。面向对象编程提供了较为安全的动态多态实现(静态形式的多态(泛型或模板)也是单体结构系统的一种有效的依赖关系管理模式;然而泛型这样的解耦模式不能像动态多态那样避免重新编译与部署),避免了使用函数指针这种危险模式进行组件解耦。

最简单的跨边界调用形式,是由底层客户端来调用高层服务函数,这种依赖关系在运行时和编译时保持指向一致,都是从底层组件指向高层组件。

《架构整洁之道》读书笔记(中)_第2张图片
图片发自App

但当高层组件中的客户端需要调用底层组件中的服务时,就需要运用动态形式的多态来反转依赖关系;这种情况下,系统在运行时的依赖关系与编译时的依赖关系就是相反的。

《架构整洁之道》读书笔记(中)_第3张图片
图片发自App

即使是单体部署、静态链接的系统,这种自律式的组件划分仍然可以极大地帮助项目的开发、测试与部署,不同团队可以独立开发不同组件,不会互相干扰。高层组件与底层细节可以良好隔离,独立演进。

单体系统组件交互通常都是普通的函数调用,迅速而廉价,这就意味着跨源码层次解耦边界的通信很频繁;单体结构的部署 需要编译所有源码,并静态链接,所以系统中的组件一般以源码形式交付。

部署层次组件

系统架构常见的物理边界形式:动态链接库。DLL、Java的jar文件、Ruby Gem及UNIX共享库等;部署时不需要重新编译,是以二进制形式或其他等价的可部署形式交付,采用的部署层次上的解耦模式。

除此差异外,按部署层次解耦的组件与单体结构几乎一样,所有的函数仍然处于同一进程、同一个地址空间中;管理组件划分依赖关系的策略也基本和单体一致。与单体类似,按部署层次解耦的组件间的跨边界调用也只是普通的函数调用,成本较低。动态链接或运行时加载会有一个一次性的调用成本,但跨边界通信调用仍然很频繁。

线程

单体结构和按部署层次划分的组件都可以采用线程模型;线程不属于架构边界,也不属于部署单元,仅仅是管理并调度程序执行的方式。一个线程可以被包含在单一组件中,可以横跨多个组件。

本地进程

系统架构还有一个更明显的物理边界形式,本地进程。

本地进程有各自不同的地址空间,可以用某种独立的内存区域来实现共享,最常见的会用socket来彼此通信,或者操作系统提供的方式,共享邮件或消息队列。

本地进程可以是静态链接的单体结构,也可以是动态链接组件组成的程序。

高层进程的源码不应该包含低层进程的名字、物理内存地址或是注册表键名。此系统架构的设计目标是让低层进程成为高层进程的一个插件。

本地进程间的跨边界调用需要用到系统调用、数据的编码和解码,以及进程间的上下文切换,成本相对更高一些,所以需谨慎地控制通信的次数。

服务

系统架构中最强的边界形式就是服务。

服务不依赖于具体的运行位置,会始终假设它们之间的通信全部通过网络进行。

服务间的跨边界调用相对于函数调用来说,速度是非常慢的,其往返时间从几十毫秒到几秒不等。因此划分架构边界时,一定要尽可能地控制通信次数;通信业必须能够适应高延时的情况。

服务层次上使用与本地进程相同的规则,也就是让较低层次服务成为较高层次服务的“插件”,因此要确保高层服务的源码中没有任何包含与低层服务相关的物理信息(如URI)。

小结

除单体结构外,大部分系统会同时采用多种边界划分策略。按照服务层次划分边界的系统,也可能会在某一部分采用本地进程的边界划分模式。服务经常不过是一系列相互作用的本地进程的外在形式;服务/本地进程,几乎都是一个或多个源码组件组成的单体结构,或一组动态链接的可部署组件。

一个系统中通常会同时包含高通信量、低延迟的本地架构边界和低通信量、高延迟的服务边界。

第19章  策略与层次

软件架构设计的工作重点之一就是讲业务策略彼此分离,然后将它们按照变更的方式进行重新分组。其中变更原因、时间和层次相同的策略应该被分到同一个组件中;反之,变更原因、时间和层次不同的策略应该分属于不同的组件。

设计良好的架构中,依赖关系的方向中通常取决于他们所关联的组件层次;底层组件被设计为依赖高层组件。这里的依赖关系时源码层次上的、编译期的依赖关系。

层次(Level)

“层次”严格按照“输入与输出之间的距离”来定义的。策略距离系统的输入/输出越远,所属的层次就越高;直接管理输入/输出的策略在系统中的层次是最低的。

通过将策略隔离,并让源码中的依赖方向与数据流向脱钩,都统一调整为指向高层策略;从另一个角度来说,底层组件应该成为高层组件的插件。

小结

本章针对策略的讨论涉及单一职责原则(SRP)、开闭原则(OCP)、共同闭包 原则(CCP)、依赖反转原则(DIP)、稳定依赖原则(SDP)以及稳定抽象原则(SAP)。

第20章  业务逻辑

业务逻辑就是程序中真正用于赚钱或省钱的业务逻辑过程。

关键业务逻辑和关键业务数据是紧密相关的,很适合被放在同一个对象处理,这种对象称为“业务实体”。

业务实体

业务实体包含了一系列用于操作关键数据的业务逻辑;这些实体对象要么直接包含关键业务数据,要么可以很容易地访问这些数据。业务实体的接口层则是由那些实现关键业务逻辑、操作关键业务数据的函数组成的。

业务实体这个概念中应该只有业务逻辑,没有别的,如数据库、用户界面、第三方框架等。

业务实体这个概念只要求我们将关键业务数据和关键业务逻辑绑定在一个独立的软件模块内。

用例

用例中包含了对如何调用业务实体中的关键业务逻辑的定义;简言之,用例控制着业务实体之间的交互方式。

用例并不描述系统与用户之间的接口,只描述该应用在某些特定场景下的业务逻辑,这些业务逻辑所规范的是用户与业务实体之间的交互方式,它与数据流入/流出系统的方式无关。

有些业务逻辑通过定义或限制自动化系统的运行方式来实现赚钱或省钱的业务,这些业务逻辑不能靠人工来执行。

低层的业务用例需要了解高层的业务实体。所以,用例依赖于业务实体,而业务实体不依赖于用例。

请求与响应模型

用例通常不应该知道数据展现给用户或者其他组件的方式。

用例类接收的输入应该是一个简单的请求性数据结构,而返回输出的应是一个简单的响应性数据结构。这些数据结构不应该存在任何依赖关系。

独立性非常关键,如果这里的请求和响应模型不是完全独立的,用到模型的用例就会依赖这些模型带来的各种依赖关系。

业务实体与请求/响应模型存在的意义非常、非常不一样的,会以不同的原因、不同的速率发生变更。它们以任何方式整合在一起都是对共同闭包原则(CCP)和单一职责原则(SRP)的违反,会导致代码中出现很多分支判断和中间数据。

小结

业务逻辑是一个系统存在的意义,属于核心功能,是系统用来赚钱或省钱的那部分代码,是整个系统中的皇冠明珠。

业务逻辑应该保持纯净,不要掺杂用户界面或者所用数据库相关东西。理想情况下,代表业务逻辑爱的代码应该是整个系统的核心,其它底层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。

第21章  尖叫的软件架构

应用程序的架构设计、顶层结构目录、及顶层软件包中的源代码应该反映设计系统功能,系统用例和设计目标

架构设计的主题

软件的系统架构应该系统的用例提供支持,架构设计图应该非常明确凸显应用程序会有哪些用例。

框架只是一个可用的工具和手段,而不是一个架构所规范的内容。如果架构基于框架来设计,就不能基于我们的用例来设计了。

架构设计的核心目标

良好的架构设计应该围绕着用例来展开,这样的架构可以在脱离框架、工具及使用环境的情况下完整地描述用例。

良好的架构设计应尽可能允许用户推迟或延后决定采用什么框架、数据库Web服务及其他与环境相关的工具。

良好的架构应该只关注用例,并能将它们与其他周边因素隔离。

Web

Web只是一种交付手段——一种IO设备。应用程序采用Web方式来交付只是一个实现细节,这不应该主导整个项目的结构设计。一个系统应该尽量保持与交付方式之间的无关性。

框架是工具而不是生活信条

要带着怀疑的态度审视每一个框架。要懂得权衡如何使用一个框架,如何保护自己;物无论如何,需要仔细考虑如何能保持对系统用例的关注,避免让框架主导我们的架构设计。

可测试的架构设计

如果系统架构的所有设计都是围绕着用例来展开的,并且在使用框架的问题上保持谨慎态度,那么就应该可以在不依赖任何框架的情况下针对这些用例进行单元测试。我们测试的应只是一个简单的业务实体对象,没有任何与框架、数据库相关的依赖关系。

总而言之,应该通过用例对象来调度业务实体对象,确保所有的测试都不需要依赖框架。

小结

系统的架构应该着重于展示系统本身的设计,而并非系统所使用的框架。

第22章  整洁架构

常见系统架构,六边形架构(端口与适配器架构)、DCI架构、BCE架构。共同设计目标:按照不同关注点对软件进行切割。这些架构都会将软件切割成不同的层,至少有一个层是只包含软件的业务逻辑的,用户接口、系统接口属于其他层。

按照上述架构设计的系统有如下特点:

独立于框架:系统架构不依赖于某个功能丰富的框架中的某个函数,框架被当作工具使用,不需要让让系统来适应框架。

可被测试:系统的业务逻辑可以脱离UI、数据库、Web服务及其他外部元素来测试。

独立于UI:系统的UI变更起来很容易,不需要修改其他的系统部分。

独立于数据库:业务逻辑与数据库之间已完成解耦,数据库可轻易更换。

独立于任何外部结构:系统业务逻辑不需要知道任何其他外部接口的存在。

《架构整洁之道》读书笔记(中)_第4张图片
图片发自App

依赖关系规则

图中的同心圆分别代表了软件系统中不同的层次,越靠近中心,其所在软件层次就越高。基本上,外层园代表的是机制,内层园代表的是策略。贯穿整个架构设计的规则——依赖关系规则:

源码中的依赖关系必须只指向同心圆的内层,即y由低层机制指向高层策略。

总言之,不应该让外层园中发生的任何变更影响到内层园的代码。

业务实体

业务实体这一层封装的是整个系统的关键业务逻辑,既可以是带有方法的对象 ,也可以是一组数据结构和函数的集合。属于系统中最不容易受外界影响而变动的部分。

用例

用例层包含的是特定应用场景下的业务逻辑,这里面封装并实现了整个系统的所有用例。用例引导了数据在业务实体之间的流入/流出,并指挥着其中业务实体利用关键业务逻辑来实现用例的设计目标。

既不希望这一层发生的变更影响业务实体,也不希望这一层受外部因素(譬如数据库、UI、常见框架)的影响。

接口适配器

软件的接口适配器层中通常是一组数据转换器:负责将数据从对用例和业务实体而言最方便操作的格式,转化成外部系统(如数据库、Web)最方便操作的格式;也会负责将数据从对业务实体与用例而言最方便操作的格式,转化为对所采用的持久性框架(如数据库)最方便的格式。这层也会负责将来自外部服务的数据转换成系统内用例与业务实体所需的格式。

框架与驱动程序

上图中最外层的模型层一般是由工具、数据库Web框架等组成,这一层通常只需要编写与内层沟通的粘合性代码。

框架与驱动程序层中包含了所有的实现细节,Web、数据库。

只有4层吗

上图的同心园只是为了说明架构的结构,真正的架构可能会超过4层。并没有规则约定系统架构有且只有四层,但更多层,依赖关系原则不变……

跨越边界

上图右下侧,示范在架构中跨边界的情况,具体就是控制器、展示器与下一层的用例之间的通信过程。控制流方向,从控制器开始、穿过用例,最后执行展示器代码;源码中的依赖方向都是从内指向用例的。这里采用依赖反转原则(DIP)来解决这种相反性。

利用动态多态技术,将源码中的依赖关系与控制流方向进行反转;不管控制流原本方向如何,都可以让它遵守架构的依赖关系原则。采用这种方式跨越系统中所有的架构边界。

哪些数据会跨越边界

通常,会跨越边界的数据在数据结构上都是简单的。跨边界传输的对象应该有一个独立、简单的数据结构;传递的数据结构中也不应该存在违法依赖规则的依赖关系。

跨边界传输一定要采用内层最方便使用的形式。

小结

通过将系统划分层次,并确保这些层次遵守依赖关系规则,可以构建出一个天生可测试的系统。而且当系统外层的数据库、Web框架过时的时候,还可以很轻松地替换它们。

第23章  展示器和谦卑对象

谦卑对象模式

谦卑对象模式最初设计目的是帮助单元测试编写者区分容易测试的行为与难以测试的行为,并将它们隔离。设计思路就是讲这两类行为拆分成两组模块或类,谦卑组(Humble)——包含系统中所有难以测试的行为,另一组——包含所有不属于谦卑对象的行为。

“谦卑”,拟人化的表述,指难以测试对象清晰认识到自己的局限性,只发挥自己的桥梁和通信作用,并不从中干预信息的传输。

展示器与视图

视图部分属于难以测试的谦卑对象。这种对象的代码应该尽量简单,只负责把数据填充到GUI,而不应该对数据进行任何处理。

展示器则是可测试对象。展示器负责从应用程序中接收数据,按视图要求对数据格式化,以便呈现在屏幕上。

应用程序所能控制的、要在屏幕上显示的一切东西,都应该在视图模型中以字符串、布尔值或枚举值的形式存在。视图部分除了加载视图模型所需要带的值,不应该再做任何其他事情。

测试与架构

强大的可测试性是架构世界是否优秀的显著衡量标准之一。将系统行为分割成可测试和不可测试两部分两部分的过程常常就定义了系统的架构边界。展示器与视图之间的边界只是多种架构边界中的一种。

数据库网关

用例交互器与数据库中间的组件,称之为数据库网关。数据库网关本身是一个多态接口,包含了应用程序在数据库上所要执行的创建、读取、更新、删除等所有操作。

交互器不属于谦卑对象,封装的是特定应用场景下的业务逻辑。

数据映射器

ORM“数据映射器”,只是将数据从关系型数据库加载到对应数据结构中。ORM属于系统架构中的数据库层,在数据库和数据库网关接口之间构建了一种谦卑对象的边界。

服务监听器

在应用程序与其他服务(或本身提供的服务)交互,在服务的边界处,也有谦卑对象存在。

数据结构进行跨服务边界的传输。

小结

在系统的边界处运用谦卑对象模式,可以大幅提高整个系统的可测试性:跨边界的通信需要用到某种简单的数据结构,而边界自然而然会将系统分割成难以测试部分与容易测试部分。

第24章  不完全边界

构建完整的架构边界是一件很耗费成本的事。预测性设计在敏捷社区饱受诟病,因为它显然违背了YAGNI原则(“You Are‘t Going to Need It”,即“不要预测未来的需要”)

引入不完全边界(partial boundary)的概念。

省掉最后一步

构建不完全边界的一种方式,在将系统分割成一系列可以独立编译、独立部署的组件之后,再把它们构建成一个组件。这种不完全边界的所需要的代码量和设计的工作量,和设计完整边界时是完全一样的,只是省了多组件管理部分的工作,等于省了版本号管理和发布管理方面的工作。

单向边界


《架构整洁之道》读书笔记(中)_第5张图片
图片发自App


门户模式


《架构整洁之道》读书笔记(中)_第6张图片
图片发自App


小结

介绍了三种不完全地实现架构边界的简单方法。

每种方式都有相应的成本和收益。每种方式有所适用的场景,可以被用来充当最终完整架构边界的临时替代品;若最终这些边界被证明没必要存在,可以被自然降解。

架构师的职责之一就是预判未来哪里有可能会需要设置架构边界,并决定是以完全形式还是不完全形式来实现它们。

第25章  层次与边界

对于简单系统来说,可分为UI、业务逻辑、数据库三个组件,但复杂系统就远不止三个。

基于文本的冒险游戏:Hurt the Wumpus

保留这种基于文本的UI,将UI与游戏业务逻辑之间的耦合解开,以便游戏可以使用不同语言。也就是,游戏的业务逻辑与UI之间应该用一种与自然语言无关的API来通信,由UI负责将API传递尽量的信息转换成合适的自然语言。

如图,多个UI组件复用同一套游戏业务逻辑,而游戏的业务逻辑组件不知道,也不必知道UI在使用哪一种自然语言。

《架构整洁之道》读书笔记(中)_第7张图片
图片发自App

游戏的业务逻辑不依赖于不同种类的数据存储,而是相反;所以设计要合理遵守依赖关系原则。如图

《架构整洁之道》读书笔记(中)_第8张图片
图片发自App

可否采用整洁架构

虚线框代表抽象组件,其定义的API通常由其上下层组件来实现。

《架构整洁之道》读书笔记(中)_第9张图片
图片发自App

所有场景中,由Boundary接口所定义的API都是由其使用者的上一层组件负责维护的。

去掉所有的具体实现类,只保留API组件,进一步简化设计图。左侧数据流关注与用户通信,右侧数据流关注数据持久化;数据流在顶部的GameRules汇聚,GameRules组件是所有数据的最终处理者。

《架构整洁之道》读书笔记(中)_第10张图片
图片发自App

数据流的分割

游戏的低层策略会负责向高层策略传递事件,而高层组件则管理玩家状态,最终该策略将会决定玩家在游戏中的输赢。

《架构整洁之道》读书笔记(中)_第11张图片
图片发自App

面向海量玩家的新版Hurt the Wumpus游戏。

MoveMangement组合有玩家的本地计算机处理,PlayerMangement组件由服务端来处理。PlayerMangement组件会为所有连接上它的MoveMangement组件提供一个微服务的API。MoveMangement与PlayerMangement之间存在一个完整的系统架构边界。

《架构整洁之道》读书笔记(中)_第12张图片
图片发自App

小结

架构师必须仔细权衡成本,决定哪里需要设计架构边界,是完整边界,还是不完全边界,还是可忽略边界。

不是一次性的决定。架构师必须持续观察系统的演进,时刻注意哪里可能需要设计边界,并仔细观察这些地方会由于不存在边界而出现哪些问题;出现问题时,还要权衡一下实现这个边界的成本,并与不实现这个边界的成本对比——经常反复作这种对比。找到设置边界的优势超过其成本的拐点,这就是实现该边界的最佳时机。

第26章  Main组件

所有系统中,至少有一个组件来负责创建、协调、监督其他组件的运转。称其为Main组件。

最细节化的部分

Main组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。

Main组件的依赖关系通常应该由依赖注入框架来注入。

小结

Main组件可以被视为应用程序的插件——负责设置起始状态、配置信息、加载外部资源,最后将控制权转交给应用程序的其他高层组件。由于Main组件能以插件形式存在于系统中,可以为一个系统设计多个Main组件,让它们各自对应于不同的配置。

以架构边界把Main组件与系统的其他部分隔离开。

第27章  服务:宏观与微观

面向服务及微服务的架构近年来非常流行,原因如下:

服务之间似乎是强隔离的,但实际并不完全是这样。

服务被认为是支持独立开发和部署的,实际并不完全是这样。

面向服务的架构

架构设计的任务就是找到高层策略与底层细节之间的架构边界,并保证这些边界遵守依赖关系规则。

服务这种形式不过是一种跨进程/平台边界的“函数调用”,比函数调用方式成本稍高,是一种分割应用程序的行为,与系统架构无关。服务本身并不能完全代表系统架构。

系统架构都是由跨越架构边界的关键函数调用(包括服务的“函数调用”)来定义的,并且整个架构必须遵守依赖关系规则。

服务所带来的好处

以下批驳了服务所谓的“好处”,挑战了目前流行的对服务架构的崇拜情节。

解耦合的谬论

服务之间仅仅是在变量层面做到了彼此隔离。服务彼此之间会因处理器内共享资源,或通过网络共享资源而耦合;任何形式的共享数据行为都会导致强耦合。

服务因彼此之间共享数据而耦合,强耦合于彼此间共享的数据结构。

服务的接口与普通函数接口相比,并没有比后者更正式、更严谨,也没有更好。

独立开发部署的谬论

服务并不是构建大型系统的唯一选择。大型系统一样可以采用单体模式,或组件模式来构建,不一定非得服务化。

前面所述的解耦合谬论已说明拆分服务并不意味着这些服务可以彼此独立开发、部署和运维。如果服务之间以数据形式或行为形式相耦合,它们的开发、部署和运维也必须彼此协调来进行。

运送猫咪的难题

出租车调度系统

《架构整洁之道》读书笔记(中)_第13张图片
图片发自App

增加猫咪送达需求后,系统所有的服务都需要做变更,并且服务间还要彼此协调好。这些服务事实上全都是强耦合的,并不能真正做到独立开发、部署和维护。

横跨型变更(cross-cutting concern)问题,是所有软件系统都要面对的问题,无论服务化还是非服务化。如上图所示这种按功能划分服务的架构方式,在跨系统的功能变更时是最脆弱的。

对象化是救星

通过组件化的系统架构,如何解决这个难题?

通过对SOLID设计原则的仔细考虑,一开始就设计出一系列多态化的类,以应对将来的新功能的扩展需要。

服务化设计中的大部分逻辑被包含在对象模型的基类中。针对特定行程的逻辑被抽离到一个单独的Rides组件,运送猫咪的新功能被放入到Kittens组件。这两个组件覆盖了原始组件中的抽象基类,这种测试方法被称为模板方法或策略模式。

如此,运送猫咪的功能与系统其他部分实现了解耦,可以独立开发和部署。

《架构整洁之道》读书笔记(中)_第14张图片
图片发自App

基于组件的服务

服务也可以按照SOLID原则来设计,按照组件结构来部署,就可以做到添加/删除组件时不影响服务中的其他组件。这种增加新功能的过程符合开闭原则(OCP)。

架构中的服务和以前一样,但每个服务中都增加了内部组件结构,以便使用衍生类来添加新功能,衍生类都有各自所生存的组件。

《架构整洁之道》读书笔记(中)_第15张图片
图片发自App

横跨型变更

系统的架构边界事实上并不落在服务之间,而是穿透所有服务,在服务内部以组件边界的形式存在

为处理横跨型变更问题,必须在服务内部采用遵守依赖关系原则的组件设计方式。服务边界并不能代表系统的架构边界,服务内部的组件边界才是。

小结

服务化可能有助于提升系统的可扩展性和可研发性,但服务本身不能代表整个系统的架构设计。系统的架构是由系统内部的架构边界,及边界之间的依赖关系所定义的,与系统各组件之间的调用和通信方式无关。

服务可能是独立组件,以系统架构边界的形式隔开;服务也可能是几个组件组成,组件以架构边界的形式互相隔离。

第28章  测试边界

测试也是一种系统组件

测试是系统中最独立的组件。也要遵守依赖关系原则,可视为系统架构中最外圈的组件,始终向内依赖于被测试部分的代码;即使系统本身不需要独立部署,其测试组件也总是可以独立部署,常被部署于测试环境中。

测试组件是为了支持开发过程,而不是运行过程。测试组件在许多方面都反映了系统中组件所应遵循的设计模型。

可测试性设计

测试如果没有被集成到系统设计中,往往是非常脆弱的,会使得系统变得死板,难以修改。

测试代码与系统是强耦合的,就得随着系统变更而变更。修改一个通用组件可能导致成百上千个测试出现问题,这类问题称为脆弱的测试问题(fragile tests problem)。

脆弱的测试还往往让系统变得非常死板:当开发者意识到一些简单修改会导致大量测试出错时,他们自然就会抵制修改。

设计原则第一条——不要依赖于多变的东西。在系统设计与测试设计时,应让业务逻辑不通过GUI与可以被测试。

测试专用API

方法之一——专门为验证业务逻辑的测试创建一个API;这个API应成为用户界面所用到的交互器和接口适配器的超集。

设置测试API是为了让测试代码从应用程序中分离出来。换言之,这种解耦动作不是为了分离测试部分和UI部分,而是将测试代码的结构和应用程序其他部分代码的结构分开。

结构性耦合

结构性耦合是测试代码所具有的耦合关系中最强大、最阴险的一种形式。……

测试专用API的作用就是将应用程序与测试代码解耦。产品代码和测试代码可以在不影响对方的情况下进行重构和演进。这种演进过程的隔离非常重要,随着时间推移测试代码趋向于具体和详细,产品代码趋向于抽象和通用。结构性的强耦合会让这种必须的演进无法进行或形成强烈干扰。

安全性

测试专用API经常被授予超级权限,允许忽视安全限制……,强制将系统设置到某种可测试状态中。为避免安全性问题,应将测试专用API及其对应实现放置到单独可独立部署的组件中,确保只部署在测试环境。

小结

测试不能独立于系统之外,它们是系统的重要组成部分。需要精心设计这些测试,才能让它们发挥验证系统稳定性和预防问题复发的作用。没有按照系统组成部分来设计的测试代码,往往是非常脆弱且难以维护的。

第29章  整洁的嵌入式架构

软件质量本身并不会随着时间推移而磨损,而硬件不断演进,未妥善管理的硬件依赖和固件依赖是软件的头号杀手。

固件并不一定是是指存储在ROM中的代码;固件也不是依据其存储位置来定义的,而是由其代码的依赖关系,及其随着硬件的演进在变更难度上的变化来定义的。在架构嵌入式代码时要时刻记得:硬件是不断演进。

应该少写点固件,多写点软件。代码中嵌入了SOL或引入了对某个平台的依赖,Android工程师没有将业务逻辑与Andriod API分离,也是在写固件代码。

产品的功能代码(软件)与硬件支持代码(固件)边界模糊的几乎不存在;通信系统,没有区分TDM技术代码和拨打电话这样的业务逻辑。整个产品从头至尾都与具体技术、具体硬件息息相关,无法分割,导致整个产品成为事实上的固件。

从使用意义上将软件、硬件、固件区分开,不要再写固件代码,通过好的架构设计让嵌入式代码拥有更长的有效生命周期。

“程序适用测试”测试

Kent Back描述的软件构建过程三个阶段:

1、“先让代码工作起来”——如果代码不能工作,就不能产生价值

2、“然后再试图将它变好”——通过对代码进行重构,让我们自己和其他人更好地理解代码,并能按照需求不断地修改代码。

3、“最后再试着让它运行得更快”——按照性能提升的“需求”来重构代码

大部分“野生”的嵌入式代码都只关注“让它先工作起来”,偶尔还有关注“让它更快”。大部分嵌入式或非嵌入式应用仅仅停留在“可用”这个目标,很少为长久使用而进行正确设计。

对程序员来说,让程序工作这件事只能被称为“程序适用测试(app-titudetest)”。

目标硬件瓶颈

嵌入式系统程序员需要处理非嵌入式系统不需要关心的事情——如,有限的地址空间、实时性限制、运行截止时间、有限的I/O能力、非常规的用户接口、感应器,其他与物理世界的世界链接。

目标硬件瓶颈(target-hardware bottleneck)是嵌入式系统特有的问题。采用清晰的架构来设计嵌入式的代码结构,可以避免只能在目标系统平台上测试代码的难题。

整洁的嵌入式架构就是可测试的嵌入式架构

具体如何将架构设计原则应用在嵌入式软件和固件上,以避免陷入目标硬件瓶颈。

分层

将系统分为三层:硬件层、固件层和软件层。小心处理各层之间的依赖。

硬件是实现细节

软件与固件之间的分割线没有代码与硬件之间的分割线那么清晰。

软件与固件之间的边界成为硬件抽象层(HAL)。HAL的API应该按照它上层软件的需要来量身定做,HAL的作用是为软件部分提供服务,以便隐藏具体的实现细节;不要向HAL的用户暴露硬件细节。

设计合理的HAL可以为脱离硬件平台的测试提供相应的支撑。

处理器是实现细节

处理器相关的工具链、访问寄存器、I/O端口、时钟信息、I/O位、中断控制器以及其他处理器函数,这些代码都必须成为固件,与硬件实现绑定。同时,固件也必须把这类代码隔离成处理器抽象层(PAL),如此,使用PAL的固件就可以在目标平台之外被测试。

操作系统是实现细节

光有HAL是不够的。为了延长代码的生命周期,必须将操作系统也定义为实现细节,让代码避免与操作系统层产生依赖。

软件通过操作系统访问运行环境服务。操作系统是将软件和固件隔离的那一层。

整洁的嵌入式架构会引入操作系统抽象层(OSAL),将软件与操作系统分隔开。实现这个抽象层,就像给函数改个名字,或者需要将几个函数封装在一起。

《架构整洁之道》读书笔记(中)_第16张图片
图片发自App

OSAL可以让应用共享一种公用结构;可以帮助高价值的应用程序在目标平台、目标操作系统之外进行测试。由整洁的嵌入式架构所构建出来的软件是可以在目标操作系统之外被测试的;设计良好的OSAL会为这种目标环境外的测试提供支撑点。

面向接口编程与可替代性

分层架构的理念是基于接口编程的理念来设计的;模块之间能以接口形式交互时,就可以把一个服务替换成另一个服务。

目前普适规则之一就是用头文件充当接口定义。要小心控制头文件中的内容,尽量确保头文件只包括函数声明、必须的结构体和常量;不在定义接口的头文件中包含只有具体实现代码才需要的数据结构、常量与类型定义(typedef)。必须控制好实现细节的可见性,因为实现细节肯定会变化的。关注实现细节的代码越少,它们所需的变更就越少。

由整洁的嵌入式架构所构建的系统应该在每一个分层中都是可测试的,因为模块之间采用接口通信,每个接口都为平台之外的测试提供了替换点。

DRY条件性编译命令

另一个经常被忽视的可替代性规则的实际案例是嵌入式C/C++程序对不同平台和操作系统的处理方式。这些程序常会用条件性编译命令来根据不同平台启用或禁用某一段代码。这种代码的重复违背了“不要重复自己(DRY)”原则。

使用HAL来解决。硬件类型只是HAL中的一个实现细节;如果使用HAL所提供的一系列接口,就可以用链接器,或者某种运行时加载器来将软件与硬件相结合。

小结

嵌入式编程人员应该学习和运用非嵌入式系统的编程经验。



《架构整洁之道》读书笔记(上)

《架构整洁之道》读书笔记(下)

你可能感兴趣的:(《架构整洁之道》读书笔记(中))