栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > 架构设计

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

架构设计 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

架构整洁之道
罗伯特·C·马丁

无论是微观世界的代码,还是宏观层面的架构,无论是三种编程范式还是微服务架构,它们都在解决一个问题——分离控制和逻辑。所谓控制就是对程序流转的与业务逻辑无关的代码或系统的控制(如多线程、异步、服务发现、部署、弹性伸缩等),所谓逻辑则是实实在在的业务逻辑,是解决用户问题的逻辑。控制和逻辑构成了整体的软件复杂度,有效地分离控制和逻辑会让你的系统得到最大的简化。

◆ 1 架构

所谓架构就是“用最小的人力成本来满足构建和维护系统需求”的设计行为;软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。

软件架构是系统设计过程中的重要设计决定的集合,可以通过变更成本来衡量每个设计决定的重要程度。

一个好的架构,不仅要在某一特定时刻满足软件用户、开发者和所有者的需求,更要在一段时间内持续满足他们的后续需求。采用好的软件架构可以大大节省软件项目构建与维护的人力成本。让每次变更都短小简单,易于实施,并且避免缺陷,用最小的成本,最大程度地满足功能性和灵活性的要求。

设计与架构究竟是什么?底层设计细节和高层架构信息是不可分割的。它们组合在一起,共同定义了整个软件系统,缺一不可。所谓的底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线。

一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果该成本很低,并且在系统的整个生命周期内一直都能维持这样的低成本,那么这个系统的设计就是优良的。如果该系统的每次发布都会提升下一次变更的成本,那么这个设计就是不好的。就这么简单。

遗留系统?当然,某些软件研发工程师可能会认为挽救一个系统的唯一办法是抛弃现有系统,设计一个全新的系统来替代。但是这里仍然没有逃离过度自信。试问:如果是工程师的过度自信导致了目前的一团乱麻,那么,我们有什么理由认为让他们从头开始,结果就会更好呢?过度自信只会使得重构设计陷入和原项目一样的困局中。

软件系统的第一个价值维度:系统行为,是紧急的,但是并不总是特别重要。
软件系统的第二个价值维度:系统架构,是重要的,但是并不总是特别紧急。

◆ 2 编程范式

三个编程范式,它们分别是结构化编程(structured programming)、面向对象编程(object-oriented programming)以及函数式编程(functional programming)。
结构化编程对程序控制权的直接转移进行了限制和规范。
面向对象编程对程序控制权的间接转移进行了限制和规范。
函数式编程对程序中的赋值进行了限制和规范。

结构化编程

可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。

顺序结构:Dijkstra展示了顺序结构的正确性可以通过枚举法证明,其过程与其他一般的数学推导过程是一样的:针对序列中的每个输入,跟踪其对应的输出值的变化就可以了。

分支结构:Dijkstra利用枚举法又证明了分支结构的可推导性。因为我们只要能用枚举法证明分支结构中每条路径的正确性,自然就可以推导出分支结构本身的正确性。

循环结构:循环结构的证明过程则有些不同,为了证明一段循环程序的正确性,Dijkstra需要采用数学归纳法。具体来说就是,首先要用枚举法证明循环1次的正确性。接下来再证明如果循环N次是正确的,那么循环N+1次也同样也是正确的。最后还要用枚举法证明循环结构的起始与结束条件的正确性。

面向对象编程

譬如封装(encapsulation)、继承(inheritance)、多态(polymorphism)。其隐含意思就是说面向对象编程是这三项的有机组合,或者任何一种支持面向对象的编程语言必须支持这三个特性。

系统行为决定了控制流,而控制流则决定了源代码依赖关系。

请注意模块ML1和接口I在源代码上的依赖关系(或者叫继承关系),该关系的方向和控制流正好是相反的,我们称之为依赖反转。这种反转对软件架构设计的影响是非常大的。

面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。

函数式编程

所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。

◆ 3 设计原则

SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序

一般情况下,我们为软件构建中层结构的主要目标如下:
使软件可容忍被改动。
使软件更容易被理解。
构建可在多个软件系统中复用的组件。

SRP:单一职责原则。
OCP:开闭原则。
ISP:接口隔离原则。
LSP:里氏替换原则
DIP:依赖反转原则。

SRP:单一职责原则

任何一个软件模块都应该有且仅有一个被修改的原因。
任何一个软件模块都应该只对某一类行为者负责。
使用Facade设计模式:Facade(外观)模式为子系统中的各类(或结构与方法)提供一个简明一致的界面,隐藏子系统的复杂性,使子系统更加容易使用

OCP:开闭原则

一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。

软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。

OCP是我们进行系统架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围

LSP:里氏替换原则

1988年,Barbara Liskov在描述如何定义子类型时写下了这样一段话:
这里需要的是一种可替换性:如果对于每个类型是S的对象o1都存在一个类型为T的对象o2,能使操作T类型的程序P在用o2替换o1时行为保持不变,我们就可以将S称为T的子类型。[4]

正方形/长方形问题是一个著名(或者说臭名远扬)的违反LSP的设计案例(该问题的结构如图9.2所示)。

LSP可以且应该被应用于软件架构层面,因为一旦违背了可替换性,该系统架构就不得不为此增添大量复杂的应对机制。

ISP:接口隔离原则

在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。从源代码层次来说,这样的依赖关系会导致不必要的重新编译和重新部署,对更高层次的软件架构设计来说,问题也是类似的。

任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。

DIP:依赖反转原则

如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。

源代码依赖方向永远是控制流方向的反转——这就是DIP被称为依赖反转原则的原因。

该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。

4 组件构建原则

如果说SOLID原则是用于指导我们如何将砖块砌成墙与房间的,那么组件构建原则就是用来指导我们如何将这些房间组合成房子的。

组件

组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。

组件聚合与耦合

REP:复用/发布等同原则。
CCP:共同闭包原则。
CRP:共同复用原则。

软件复用的最小粒度应等同于其发布的最小粒度。

我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。

不要强迫一个组件的用户依赖他们不需要的东西。
不要依赖不需要用到的东西。
组件依赖关系图中不应该出现环。
依赖关系必须要指向更稳定的方向。

一个项目在组件结构设计上的重心是根据该项目的开发时间和成熟度不断变动的,我们对组件结构的安排主要与项目开发的进度和它被使用的方式有关,与项目本身功能的关系其实很小。

组件结构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化和扩张,而不可能在系统构建的最初就被完美设计出来。

Fan-in:入向依赖,这个指标指代了组件外部类依赖于组件内部类的数量。
Fan-out:出向依赖,这个指标指代了组件内部类依赖于组件外部类的数量。
I:不稳定性,I=Fan-out/(Fan-in+Fan-out)。该指标的范围是[0,1],I=0意味着组件是最稳定的,I=1意味着组件是最不稳定的。

稳定依赖原则(SDP)的要求是让每个组件的I指标都必须大于其所依赖组件的I指标。也就是说,组件结构依赖图中各组件的I指标必须要按其依赖关系方向递减

一个组件的抽象化程度应该与其稳定性保持一致。

◆ 5 软件架构

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

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

如果软件架构师早先就考虑到这些部署问题,可能就会有意地减少微服务的数量,采用进程内部组件与外部服务混合的架构,以及更加集成式的连接管理方式。

软件有行为价值与架构价值两种价值。这其中的第二种价值又比第一种更重要,因为它正是软件之所以“软”的原因。

给出可选的方案:一个优秀的软件架构师应该致力于最大化可选项数量。

优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好。

独立性

系统的用例与正常运行。
系统的维护。
系统的开发。
系统的部署。

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

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

我们不喜欢重复的代码,当代码真的出现重复时,我们经常会感到作为一个专业人士,自己是有责任减少或消除这种重复的。但是:我们一定要小心避免陷入对任何重复都要立即消除的应激反应模式中。一定要确保这些消除动作只针对那些真正意义上的重复。

划分边界

软件架构设计本身就是一门划分边界的艺术。边界的作用是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。

那么我们就需要了解一个系统最消耗人力资源的是什么?答案是系统中存在的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合。

在一个设计良好的系统架构中,这些细节性的决策都应该是辅助性的,可以被推迟的。一个设计良好的系统架构不应该依赖于这些细节,而应该尽可能地推迟这些细节性的决策,并致力于将这种推迟所产生的影响降到最低。

系统的核心业务逻辑必须和其他组件隔离,保持独立,而这些其他组件要么是可以去掉的,要么是有多种实现的;这其实就是单一职责原则(SRP)的具体实现,SRP的作用就是告诉我们应该在哪里画边界线。

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

一个系统的架构是由一系列软件组件以及它们之间的边界共同定义的。而这些边界有着多种不同的存在形式。在本章中,我们看看其中最常见的一些形式。
跨边界调用

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

服务经常不过就是一系列互相作用的本地进程的某种外在形式。无论是服务还是本地进程,它们几乎肯定都是由一个或多个源码组件组成的单体结构,或者一组动态链接的可部署组件。

策略与层次

本质上,所有的软件系统都是一组策略语句的集合。

一条策略距离系统的输入/输出越远,它所属的层次就越高。而直接管理输入/输出的策略在系统中的层次是最低的。

离输入/输出最远的策略——高层策略——一般变更没有那么频繁。即使发生变更,其原因也比低层策略所在的组件更重大。反之,低层策略则很有可能会频繁地进行一些小变更。

业务逻辑

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

关键业务逻辑和关键业务数据是紧密相关的,所以它们很适合被放在同一个对象中处理。我们将这种对象称为“业务实体(Entity)”。

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

业务实体属于高层概念,而用例属于低层概念

因为用例描述的是一个特定的应用情景,这样一来,用例必然会更靠近系统的输入和输出。而业务实体是一个可以适用于多个应用情景的一般化概念,相对地离系统的输入和输出更远。所以,用例依赖于业务实体,而业务实体并不依赖于用例。

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

整洁架构

它们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。

按照这些架构设计出来的系统,通常都具有以下特点。
◆ 独立于框架:
应用程序框架是实现细节:框架作者想让我们与框架订终身——这相当于我们要对他们的框架做一个巨大而长期的承诺,而在任何情况下框架作者都不会对我们做出同样的承诺。这种婚姻是单向的。我们要承担所有的风险,而框架作者则没有任何风险。
◆ 可被测试:这些系统的业务逻辑可以脱离UI、数据库
◆ 独立于UI
Web是实现细节:GUI只是一个实现细节。而Web则是GUI的一种,所以也是一个实现细节。作为一名软件架构师,我们需要将这类细节与核心业务逻辑隔离开来。
◆独立于数据库
◆独立于任何外部机构:
◆源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。
◆一个系统的架构应该着重于展示系统本身的设计,而并非该系统所使用的框架。

业务实体
这一层中封装的是整个系统的关键业务逻辑,一个业务实体既可以是一个带有方法的对象,也可以是一组数据结构和函数的集合。

用例
软件的用例层中通常包含的是特定应用场景下的业务逻辑,这里面封装并实现了整个系统的所有用例。
软件的系统架构应该为该系统的用例提供支持。这就像住宅和图书馆的建筑计划满篇都在非常明显地凸显这些建筑的用例一样,软件系统的架构设计图也应该非常明确地凸显该应用程序会有哪些用例。
一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。

层次越往内,其抽象和策略的层次越高,同时软件的抽象程度就越高,其包含的高层策略就越多。最内层的圆中包含的是最通用、最高层的策略,最外层的圆包含的是最具体的实现细节。

展示器和谦卑对象

谦卑对象模式最初的设计目的是帮助单元测试的编写者区分容易测试的行为与难以测试的行为,并将它们隔离。就是将这两类行为拆分成两组模块或类。其中一组模块被称为谦卑(Humble)组,包含了系统中所有难以测试的行为,而这些行为已经被简化到不能再简化了。另一组模块则包含了所有不属于谦卑对象的行为。

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

展示器则是可测试的对象。展示器的工作是负责从应用程序中接收数据,然后按视图的需要将这些数据格式化,以便视图将其呈现在屏幕上

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

层次与边界

预防性设计在敏捷社区里是饱受诟病的,因为它显然违背了YAGNI原则(“You Aren’t Going to Need It”,意即“不要预测未来的需要”)

构建不完全边界的一种方式就是在将系统分割成一系列可以独立编译、独立部署的组件之后,再把它们构建成一个组件。

你会看到一个临时占位的,将来可被替换成完整架构边界的更简单的结构。这个结构采用了传统的策略模式(strategy pattern)
采用门户模式(facade pattern)。这里的边界将只能由Facade类来定义,这个类的背后是一份包含了所有服务函数的列表,它会负责将Client的调用传递给对Client不可见的服务函数

不应该将未来的需求抽象化。这就是YAGNI原则:“You aren’t going to need it”,臆想中的需求事实上往往是不存在的。这是一句饱含智慧的建议,因为过度的工程设计往往比工程设计不足还要糟糕。

现实就是这样。作为软件架构师,我们必须有一点未卜先知的能力。有时候要依靠猜测——当然还要用点脑子。软件架构师必须仔细权衡成本,决定哪里需要设计架构边界,以及这些地方需要的是完整的边界,还是不完全的边界,还是可以忽略的边界。

服务:宏观与微观

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

所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。服务之间的确在变量层面做到了彼此隔离。然而,它们之间还是可能会因为处理器内的共享资源,或者通过网络共享资源而彼此耦合的。另外,任何形式的共享数据行为都会导致强耦合。

首先,无数历史事实证明,大型系统一样可以采用单体模式,或者组件模式来构建,不一定非得服务化。因此服务化并不是构建大型系统的唯一选择。
其次,上文说到的解耦合谬论已经说明拆分服务并不意味着这些服务可以彼此独立开发、部署和运维。如果这些服务之间以数据形式或者行为形式相耦合,那么它们的开发、部署和运维也必须彼此协调来进行。

基于组件的服务,服务并不一定必须是小型的单体程序。服务也可以按照SOLID原则来设计,按照组件结构来部署,这样就可以做到在添加/删除组件时不影响服务中的其他组件。

系统的架构是由系统内部的架构边界,以及边界之间的依赖关系所定义的,与系统中各组件之间的调用和通信方式无关。

测试边界

测试也是一种系统组件;
可测试性设计;
修改一个通用的系统组件可能会导致成百上千个测试出现问题,我们通常将这类问题称为脆弱的测试问题(fragiletestsproblem)。
测试专用API:这种具有超级权限的测试专用API如果被部署到我们的产品系统中,可能会是非常危险的。如果要避免这种情况发生,应该将测试专用API及其对应的具体实现放置在一个单独的、可独立部署的组件中。

拾遗

分层架构:
传统的水平分层架构。在这个架构里,我们将代码从技术角度进行分类。这通常被称为“按层封装;
另外一种组织代码的形式是“按功能封装”,即垂直切分,根据相关的功能、业务概念或者聚合根(领域驱动设计原则中的术语)来切分;

通过采用“端口和适配器”“六边形架构”“边界、控制器、实体”等,我们可以创造出一个业务领域代码与具体实现细节(数据库、框架等)隔离的架构。我们可以区分出代码中的内部代码(领域,Domain)与外部代码(基础设施,Infrastructure)。内部区域包含了所有的领域概念,而外部区域则包含了与外界交互的部分(例如UI、数据库、第三方集成等)

一个规范——一个架构设计原则——内容是“Web控制器永远不应该直接访问数据层”

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/912946.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号