架构设计的核心

  • 分离控制,和业务逻辑, 分离依赖

学习到的经验

  • 代码实现设计使用开闭, 单一职责.
  • 业务逻辑不应该依赖, 外部具体细节,需要控制反转.
  • 梳理 UML 图, 合理的描述整个项目代码结构.

如果当前项目重新开始?

  • 禁止数据结构体的透传(从持久层透传到呈现层)
  • 采用依赖注入,利用编译器,比例go的internal 文件夹,控制不合理的依赖
  • 业务逻辑引用 usecase,采用业务场景分层,放弃 MVC 平铺式写法
  • 禁用通过 SQL 完成业务逻辑, 数据层与业务逻辑解耦,禁止直接复用对象
  • 梳理好项目架构设计图文档,避免新人打乱代码层级(或通过服务设计强制分割依赖

文摘

然而,其中一些程序员发现,只让代码跑起来是不够的,因为这个世界是不断变化的,他们发现自己需要花更多的时间来维护代码:增加新的需求,扩展原有的流程,修改已有的功能,优化性能……一个人完全维护不过来,还需要更多的人,于是代码还需要在不同人之间轮转;他们发现代码除了需要跑起来,还需要易读、易扩展、易维护,甚至可以直接重用。于是,这些人使用各种各样的手段和技术不断提高代码的易读性、可扩展性、可维护性和重用性。我们把这些有“洁癖”、有工匠精精、有修养的程序员叫作工程师,工程师不仅仅是在编写代码,他们会用工程的方法来编写代码,以便让编程开发更为高效和快速。他们把编程当成一种设计,一种工业设计,把代码模块化,让这些模块可以更容易地交互拼装和组织,让代码排列整齐——阅读和维护这些代码就像看阅兵式一样舒心畅快。

但是有一些资深的工程师开始站出来挑战这些问题,有的基于业务分析给出平衡的方案,有的开始尝试设计更高级的技术,有的开始设计更灵活的系统,有的则开始简化和轻量化整个系统……这些高智商、经验足、不怕难的工程师们引领着整个行业前行。他们就是架构师!

2023/09/27发表想法 分离控制,和逻辑

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

◆ 推荐序二 久远的教诲,古老的智慧

我提了一个很“笨”的办法:把所有“共享变量”都抽到Redis中进行读写,消灭本地副本,然后把稳定版本程序多部署几份,这样就可以多启动几个实例,将这些实例标记为AB两组。同时,在前面搭建代理服务,用于分流请求——核心功能请求分配到A组(程序基本不更新),外围功能请求分配到B组(程序按业务需求更新)。这样做看起来有点多此一举——AB两组都只有部分代码提供服务,而且要通过Redis共享状态,但是却实现了无论B组的程序如何更新,都不会影响A组所承载的核心服务的目的。

,我看到接口的设计非常随意,接口不是基于行为而是基于特定场景的实现,没有做适当的抽象,也没有为未来预留空间,直接导致契约僵硬死板。每新增一种终端呈现形式,整个内容生产流程就要大动干戈,这样的例子并不罕

◆ 第1章 设计与架构究竟是什么

总的来说,架构图里实际上包含了所有的底层设计细节,这些细节信息共同支撑了顶层的架构设计,底层设计信息和顶层架构设计共同组成了整个房屋的架构文档。软件设计也是如此。底层设计细节和高层架构信息是不可分割的。它们组合在一起,共同定义了整个软件系统,缺一不可。所谓的底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线。

2023/10/09发表想法 降低发布变更成本

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

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

要想跑得快,先要跑得稳。

◆ 第2章 两个价值维度

业务部门与研发人员经常犯的共同错误就是将第三优先级的事情提到第一优先级去做。换句话说,他们没有把真正紧急并且重要的功能和紧急但是不重要的功能分开。这个错误导致了重要的事被忽略了,重要的系统架构问题让位给了不重要的系统行为功能。

◆ 第3章 编程范式总览

结构化编程对程序控制权的直接转移进行了限制和规范。

这两个程序员注意到在ALGOL语言中,函数调用堆栈(call stack frame)可以被挪到堆内存区域里,这样函数定义的本地变量就可以在函数返回之后继续存在。这个函数就成为了一个类(class)的构造函数,而它所定义的本地变量就是类的成员变量,构造函数定义的嵌套函数就成为了成员方法(method)。这样一来,我们就可以利用多态(polymorphism)来限制用户对函数指针的使用。

面向对象编程对程序控制权的间接转移进行了限制和规范。

函数式编程对程序中的赋值进行了限制和规范。

◆ 第4章 结构化编程

Dijkstra提出的解决方案是采用数学推导方法。他的想法是借鉴数学中的公理(Postulate)、定理(Theorem)、推论(Corollary)和引理(Lemma),形成一种欧几里得结构。Dijkstra认为程序员可以像数学家一样对自己的程序进行推理证明。换句话说,程序员可以用代码将一些已证明可用的结构串联起来,只要自行证明这些额外代码是正确的,就可以推导出整个程序的正确性。

Dijkstra在研究过程中发现了一个问题:goto语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元,这会导致无法采用分解法来将大型问题进一步拆分成更小的、可证明的部分。

既然结构化编程范式可将模块递归降解拆分为可推导的单元,这就意味着模块也可以按功能进行降解拆分。这样一来,我们就可以将一个大型问题拆分为一系列高级函数的组合,而这些高级函数各自又可以继续被拆分为一系列低级函数,如此无限递归。更重要的是,每个被拆分出来的函数也都可以用结构化编程范式来书写

这就是科学理论和科学定律的特点:它们可以被证伪,但是没有办法被证明。

Dijkstra曾经说过“测试只能展示Bug的存在,并不能证明不存在Bug”,换句话说,一段程序可以由一个测试来证明其错误性,但是却不能被证明是正确的。测试的作用是让我们得出某段程序已经足够实现当前目标这一结论。

结构化编程范式中最有价值的地方就是,它赋予了我们创造可证伪程序单元的能力。

◆ 第5章 面向对象编程

我们很难说强封装是面向对象编程的必要条件

归根结底,多态其实不过就是函数指针的一种应用。自从20世纪40年代末期冯·诺依曼架构诞生那天起,程序员们就一直在使用函数指针模拟多态了。也就是说,面向对象编程在多态方面没有提出任何新概念。

因为自20世纪50年代末期以来,我们学到了一个重要经验:程序应该与设备无关。这个经验从何而来呢?因为一度所有程序都是设备相关的,但是后来我们发现自己其实真正需要的是在不同的设备上实现同样的功能。

2023/10/17发表想法 依赖与控制流是同方向的

我们可以想象一下在安全和便利的多态支持出现之前,软件是什么样子的。下面有一个典型的调用树的例子,main函数调用了一些高层函数,这些高层函数又调用了一些中层函数,这些中层函数又继续调用了一些底层函数。在这里,源代码层面的依赖不可避免地要跟随程序的控制流(详见图5.1)。[插图]图5.1:源代码依赖与控制流的区别

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

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

软件架构师可以完全控制采用了面向对象这种编程方式的系统中所有的源代码依赖关系,而不再受到系统控制流的限制。不管哪个模块调用或者被调用,软件架构师都可以随意更改源代码依赖关系。

2023/10/17发表想法 用户界面和数据库以接口的形式,注入业务逻辑模块

这种能力有什么用呢?在下面的例子中,我们可以用它来让数据库模块和用户界面模块都依赖于业务逻辑模块(见图5.3),而非相反。[插图]图5.3:数据库和用户界面都依赖于业务逻辑这意味着我们让用户界面和数据库都成为业务逻辑的插件。也就是说,业务逻辑模块的源代码不需要引入用户界面和数据库这两个模块。

于是,业务逻辑组件就可以独立于用户界面和数据库来进行部署了,我们对用户界面或者数据库的修改将不会对业务逻辑产生任何影响,这些组件都可以被分别、独立地部署。

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

◆ 第6章 函数式编程

为什么不可变性是软件架构设计需要考虑的重点呢?为什么软件架构师要操心变量的可变性呢?答案显而易见:所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。

◆ 第3部分 设计原则

:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。

◆ 第7章 SRP:单一职责原则

2023/10/20发表想法 对不同行为者负责的逻辑代码应该分开,就算中间可能存在重复代码

这个类的三个函数分别对应的是三类非常不同的行为者,违反了SRP设计原则。calculatePay()函数是由财务部门制定的,他们负责向CFO汇报。reportHours()函数是由人力资源部门制定并使用的,他们负责向COO汇报。save()函数是由DBA制定的,他们负责向CTO汇报。

读者也许会反对上面这些解决方案,因为看上去这里的每个类中都只有一个函数。事实上并非如此,因为无论是计算工资、生成报表还是保存数据都是一个很复杂的过程,每个类都可能包含了许多私有函数。总而言之,上面的每一个类都分别容纳了一组作用于相同作用域的函数,而在该作用域之外,它们各自的私有函数是互相不可见的。

◆ 第8章 OCP:开闭原则

一个好的软件架构设计师会努力将旧代码的修改需求量降至最小,甚至为0。

让我们再来复述一下这里的设计原则:如果A组件不想被B组件上发生的修改所影响,那么就应该让B组件依赖于A组件。

2023/10/20发表想法 增加,不同的具体实现类,来达到开闭的效果。

其中,Interactor组件是整个系统中最符合OCP的。发生在Database、Controller、Presenter甚至View上的修改都不会影响到Interactor。

2023/10/20发表想法 同依赖倒置,非核心业务,依赖业务逻辑,业务不受其他模块影响

为什么Interactor会被放在这么重要的位置上呢?因为它是该程序的业务逻辑所在之处,Interactor中包含了其最高层次的应用策略。其他组件都只是负责处理周边的辅助逻辑,只有Interactor才是核心组件

◆ 第9章 LSP:里氏替换原则

2023/10/20发表想法 不依赖于具体实现

因为Billing应用程序的行为并不依赖于其使用的任何一个衍生类。也就是说,这两个衍生类的对象都是可以用来替换License类对象的。

2023/10/20发表想法 正方形的特殊属性,倒置业务理解偏差

而Square类的高和宽则必须一同修改。由于User类始终认为自己在操作Rectangle类,因此会带来一些混淆

认为LSP只不过是指导如何使用继承关系的一种方法,然而随着时间的推移,LSP逐渐演变成了一种更广泛的、指导接口与其实现方式的设计原则。

因为一旦违背了可替换性,该系统架构就不得不为此增添大量复杂的应对机制。

◆ 第10章 ISP:接口隔离原则

2023/10/21发表想法 通过接口隔离对其他方案的依赖

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

2023/10/21发表想法 依赖对象某一个方法,该对象的其他方法就是多余的。

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

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

◆ 第11章 DIP:依赖反转原则

,在Java这类静态类型的编程语言中,在使用use、import、include这些语句时应该只引用那些包含接口、抽象类或者其他抽象类型声明的源文件,不应该引用任何具体实现。

我们每次修改抽象接口的时候,一定也会去修改对应的具体实现。但反过来,当我们修改具体实现时,却很少需要去修改相应的抽象接口。所以我们可以认为接口比实现更稳定。的确,优秀的软件设计师和架构师会花费很大精力来设计接口,以减少未来对其进行改动。毕竟争取在不修改接口的情况下为软件增加新的功能是软件设计的基础常识。

继承关系是所有一切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使用应该格外小心

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

绝大部分系统中都至少存在一个具体实现组件——我们一般称之为main组件,因为它们通常是main函数[插图]所在之处。在图11.1中,main函数应该负责创建ServiceFactoryImpl实例,并将其赋值给类型为ServiceFactory的全局变量,以便让Application类通过这个全局变量来进行相关调用。

◆ 第13章 组件聚合

从软件设计和架构设计的角度来看,REP原则就是指组件中的类与模块必须是彼此紧密相关的。也就是说,一个组件不能由一组毫无关联的类和模块组成,它们之间应该有一个共同的主题或者大方向

2023/10/23发表想法 当重复代码,与单一原则冲突时,优先单一原则。 服用代码的目的也是提高可维护性

可维护性的重要性要远远高于可复用性

将由于相同原因而修改,并且需要同时修改的东西放在一起。将由于不同原因而修改,并且不同时修改的东西分开。

2023/10/23发表想法 CRP 哪些类应分开, Go 模块的设计, 避免无需使用的依赖引起冲突。

因此,当我们决定要依赖某个组件时,最好是实际需要依赖该组件中的每个类。换句话说,我们希望组件中的所有类是不能拆分的,即不应该出现别人只需要依赖它的某几个类而不需要其他类的情况

CRP原则实际上是ISP原则的一个普适版。ISP原则是建议我们不要依赖带有不需要的函数的类,而CRP原则则是建议我们不要依赖带有不需要的类的组件。

◆ 第14章 组件耦合

影响组件结构的不仅有技术水平和公司内部政治斗争这两个因素

我们一定都有过这样的经历:当你花了一整天的时间,好不容易搞定了一段代码,第二天上班时却发现这段代码莫名其妙地又不能工作了。这通常是因为有人在你走后修改了你所依赖的某个组件。我给这种情况起了个名字——“一觉醒来综合征”

2023/10/23发表想法 golang 包的不能循环引用设计, 单向依赖结构

更重要的是,不管我们从该图中的哪个节点开始,都不能沿着这些代表了依赖关系的边最终走回到起始点。也就是说,这种结构中不存在环,我们称这种结构为有向无环图(Directed Acyclic Graph,简写为DAG)。

[插图]图14.1:典型的组件结构图

当我们需要发布整个系统时,可以让整个过程从下至上来进行。具体来说就是,首先对Entities组件进行编译、测试、发布。随后是Database和Interactors这两个组件。再紧随其后的是Presenters、View、Controllers,以及Authorizer四个组件。最后是Main组件。这样一来,整个流程会非常清晰,也很容易。只要我们了解系统各部分之间的依赖关系,构建整套系统就会变得很容易。

事实上,组件依赖结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图。

2023/10/24发表想法 沉淀,拥抱变化

通过遵守共同闭包原则(CCP),我们可以创造出对某些变更敏感,对其他变更不敏感的组件。这其中的一些组件在设计上就已经是考虑了易变性,预期它们会经常发生变更的。任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖,否则这个多变的组件也将会变得非常难以被修改。

稳定指的是“很难移动”。所以稳定性应该与变更所需的工作量有关。例如,硬币是不稳定的,因为只需要很小的动作就可以推倒它,而桌子则是非常稳定的,因为将它掀翻需要很大的动作。

2023/10/24发表想法 抽象组件,接口存在的意义就是为了隔离依赖,如果是一个非常不稳定的接口,那么就需要思考接口存在的意义了

因为这些抽象组件通常会非常稳定,可以被那些相对不稳定的组件依赖。

如果一个组件想要成为稳定组件,那么它就应该由接口和抽象类组成,以便将来做扩展。如此,这些既稳定又便于扩展的组件可以被组合成既灵活又不会受到过度限制的架构。

Nc:组件中类的数量。Na:组件中抽象类和接口的数量。A:抽象程度,A=Na÷Nc。A指标的取值范围是从0到1,值为0代表组件中没有任何抽象类,值为1就意味着组件中只有抽象类。

因为这些组件通常是无限抽象的,但是没有被其他组件依赖,这样的组件往往无法使用

坐落于主序列线上的组件不会为了追求稳定性而被设计得“太过抽象”,也不会为了避免抽象化而被设计得“太过不稳定”。这样的组件既不会特别难以被修改,又可以实现足够的功能

大型系统中的组件不可能做到完全抽象,也不可能做到完全稳定。所以我们只要追求让这些组件位于主序列线上,或者贴近这条线即可。

D指标[插图]:距离D=|A+I-1|,该指标的取值范围是[0,1]。值为0意味着组件是直接位于主序列线上的,值为1则意味着组件在距离主序列最远的位置。

但是有些组件处于平均值的标准差(Z=1)以外。这些组件值得被重点分析,它们要么过于抽象但依赖不足,要么过于具体而被依赖得太多。

◆ 第5部分 软件架构

首先,软件架构师自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议

2023/10/25发表想法 再重复修改以前代码的时候,才能感受到设计的缺陷

也许软件架构师生产的代码量不是最多的,但是他们必须不停地承接编程任务。如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向

软件架构设计最高优先级的目标就是保持系统正常工作。

如果研发团队只受开发进度来驱动的话,他们的架构设计最终一定会倾向于这个方向。

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

开发人员可能会决定采用某种“微服务架构”。这种架构的组件边界清晰,接口稳定,非常利于开发。但当我们实际部署这种系统时,就会发现其微服务的数量已经大到令人望而生畏,而配置这些微服务之间的连接以及启动时间都会成为系统出错的主要来源。如果软件架构师早先就考虑到这些部署问题,可能就会有意地减少微服务的数量,采用进程内部组件与外部服务混合的架构,以及更加集成式的连接管理方式。

那就是设计良好的系统架构应该可以使开发人员对系统的运行过程一目了然。架构应该起到揭示系统运行过程的作用

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

我想读者应该明白我的意思了。如果在开发高层策略时有意地让自己摆脱具体细节的纠缠,我们就可以将与具体实现相关的细节决策推迟或延后,因为越到项目的后期,我们就拥有越多的信息来做出合理的决策

而系统运行人员可以将操作系统的抽象设备与具体的读卡器、磁带读取器以及其他类似的设备进行对接。

◆ 第16章 独立性

如果该系统的架构能够在其组件之间做一些适当的隔离,同时不强制规定组件之间的交互方式,该系统就可以随时根据不断变化的运行需求来转换成各种运行时的线程、进程或服务模型。

2023/10/25发表想法 系统的交互方式,同时反应了团队的沟通方式,代码结构设计,也映射的团队成员的工作沟通方式

任何一个组织在设计系统时,往往都会复制出一个与该组织内沟通结构相同的系统。一个由多个不同目标的团队协作开发的系统必须具有相应的软件架构。这样,这些团队才可以各自独立地完成工作,不会彼此干扰

任何一个组织在设计系统时,往往都会复制出一个与该组织内沟通结构相同的系统。一个由多个不同目标的团队协作开发的系统必须具有相应的软件架构。这样,这些团队才可以各自独立地完成工作,不会彼此干扰

2023/10/25发表想法 例如, API 接口也是理所当然的切割

添加新订单的用例与删除订单的用例在发生变更的原因上几乎肯定是不同的,而且发生变更的速率也不同。因此,我们按照用例来切分系统是非常自然的选择。

添加新订单的用例与删除订单的用例在发生变更的原因上几乎肯定是不同的,而且发生变更的速率也不同。因此,我们按照用例来切分系统是非常自然的选择。

如果不同面向之间的用例得到了良好的隔离,那么需要高吞吐量的用例就和需要低吞吐量的用例互相自然分开了

们的解耦动作还应该注意选择恰当的模式。譬如,为了在不同的服务器上运行,被隔离的组件不能依赖于某个处理器上的同一个地址空间,它们必须是独立的服务,然后通过某种网络来进行通信。许多架构师将上面这种组件称为“服务”或“微服务”,至于是前者还是后者,往往取决于某些非常模糊的代码行数阈值。对于这种基于服务来构建的架构,架构师们通常称之为面向服务的架构(service-oriented architecture)。

我只是认为有时候我们必须把组件切割到服务这个应用层次。

2023/10/25发表想法 区分是否重复,还是应该从代码的责任逻辑上看,对什么负责,什么会引起其变化。不一样的责任应该进行隔离,允许重复

其中有些是真正的重复,在这种情况下,每个实例上发生的每项变更都必须同时应用到其所有的副本上。重复的情况中也有一些是假的,或者说这种重复只是表面性的。如果有两段看起来重复的代码,它们走的是不同的演进路径,也就是说它们有着不同的变更速率和变更缘由,那么这两段代码就不是真正的重复。等我们几年后再回过头来看,可能就会发现这两段代码是非常不一样的了。

2023/10/25发表想法 没有隔离,数据库字段透传到前端,前端跟据当前程序业务逻辑,只选取其中部分字段。导致后端数据字段会直接透传影响到前端,查看接口返回时,前端使用的字段和逻辑也没有那么明确,需要前端人员介入确认,高度耦合有可能引发 bug。

我们可能也会为了避免再创建一个看起来相同的视图模型并在两者之间复制元素,而选择直接将数据库记录传递给UI层。我们也一定要小心,这里几乎肯定只是一种表面性的重复。而且,另外创建一个视图模型并不会花费太多力气,这可以帮助我们保持系统水平分层之间的隔离。

我们可能也会为了避免再创建一个看起来相同的视图模型并在两者之间复制元素,而选择直接将数据库记录传递给UI层。我们也一定要小心,这里几乎肯定只是一种表面性的重复。而且,另外创建一个视图模型并不会花费太多力气,这可以帮助我们保持系统水平分层之间的隔离。

服务层次解耦的另一个问题是不仅系统资源成本高昂,而且研发成本更高。处理服务边界不仅非常耗费内存、处理器资源,而且更耗费人力。虽然内存和处理器越来越便宜,但是人力成本可一直都很高

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

2023/10/25发表想法 良好代码架构,能够应该快速地支出不同的部署架构切换(从单体到微服务,再从微服务到单体)。说明代码层级的隔离需要做得足够好。

一个设计良好的架构应该能允许一个系统从单体结构开始,以单一文件的形式部署,然后逐渐成长为一组相互独立的可部署单元,甚至是独立的服务或者微服务。最后还能随着情况的变化,允许系统逐渐回退到单体结构。

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

◆ 第17章 划分边界

架构师们所追求的目标是最大限度地降低构建和维护一个系统所需的人力资源。那么我们就需要了解一个系统最消耗人力资源的是什么?答案是系统中存在的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合

◆ 第19章 策略与层次

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

架构设计的工作常常需要将组件重排组合成为一个有向无环图。图中的每一个节点代表的是一个拥有相同层次策略的组件,每一条单向链接都代表了一种组件之间的依赖关系,它们将不同级别的组件链接起来

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

2023/11/02发表想法 参考 golang io.ReadWriter 接口设计。 通过 接口隔离依赖。 依赖关系与数据流向脱钩, 考虑更高层次的变更

我们希望源码中的依赖关系与其数据流向脱钩,而与组件所在的层次挂钩。但我们很容易将这个加密程序写成下面这样,这就构成了一个不正确的架构:function encrypt(){while(true)writeChar(translate(readChar()));}上面这个程序架构设计的错误在于,它让高层组件中的函数encrypt()依赖于低层组件中的函数readChar()与writeChar()。

◆ 第20章 业务逻辑

业务逻辑就是程序中那些真正用于赚钱或省钱的业务逻辑与过程。更严格地讲,无论这些业务逻辑是在计算机上实现的,还是人工执行的,它们在省钱/赚钱上的作用都是一样的。

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

当我们创建这样一个类时,其实就是在将软件中具体实现了该关键业务的部分聚合在一起,将其与自动化系统中我们所构建的其他部分隔离区分。这个类独自代表了整个业务逻辑,它与数据库、用户界面、第三方框架等内容无关。该类可以在任何一个系统中提供与其业务逻辑相关的服务,它不会去管这个系统是如何呈现给用户的,数据是如何存储的,或者是以何种方式运行的。总而言之,业务实体这个概念中应该只有业务逻辑,没有别的

有些读者可能会担心我在这里把业务实体解释成一个类。不是这样的,业务实体不一定非要用面向对象编程语言的类来实现。业务实体这个概念只要求我们将关键业务数据和关键业务逻辑绑定在一个独立的软件模块内。

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

2023/11/02发表想法 含泪点头

可能有些读者会选择直接在数据结构中使用对业务实体对象的引用。毕竟,业务实体与请求/响应模型之间有很多相同的数据。但请一定不要这样做!这两个对象存在的意义是非常、非常不一样的。随着时间的推移,这两个对象会以不同的原因、不同的速率发生变更。所以将它们以任何方式整合在一起都是对共同闭包原则(CCP)和单一职责原则(SRP)的违反。这样做的后果,往往会导致代码中出现很多分支判断语句和中间数据。

◆ 第21章 尖叫的软件架构

一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。这就好像一个住宅建筑设计的首要目标应该是满足住宅的使用需求,而不是确保一定要用砖来构建这个房子。架构师应该花费很多精力来确保该架构的设计在满足用例需要的情况下,尽可能地允许用户能自由地选择建筑材料(砖头、石料或者木材)。

我们一定要带着怀疑的态度审视每一个框架。是的,采用框架可能会很有帮助,但采用它们的成本呢?我们一定要懂得权衡如何使用一个框架,如何保护自己

◆ 第22章 整洁架构

按照不同关注点对软件进行切割

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

外层圆中使用的数据格式也不应该被内层圆中的代码所使用,尤其是当数据格式是由外层圆的框架所生成时。总之,我们不应该让外层圆中发生的任何变更影响到内层圆的代码

只要它能被系统中的其他不同应用复用就可以

2023/11/02发表想法 定义如何丰富/获取数据的数据接口, 再由外层对应实现,完成数据的流入

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

2023/11/02发表想法 控制填补 usecase 请求参数, 再将返回数据转化为响应

而模型部分则应该由控制器传递给用例,再由用例传回展示器和视图。

假设某些用例代码需要调用展示器,这里一定不能直接调用,因为这样做会违反依赖关系原则:内层圆中的代码不能引用其外层的声明。我们需要让业务逻辑代码调用一个内层接口(图22.1中的“用例输出端”),并让展示器来负责实现这个接口

这里最重要的是这个跨边界传输的对象应该有一个独立、简单的数据结构。总之,不要投机取巧地直接传递业务实体或数据库记录对象。同时,这些传递的数据结构中也不应该存在违反依赖规则的依赖关系。

很多数据库框架会返回一个便于查询的结果对象,我们称之为“行结构体”。这个结构体不应该跨边界向架构的内层传递。因为这等于让内层的代码引用外层代码,违反依赖规则

◆ 第23章 展示器和谦卑对象

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

展示器则是可测试的对象。展示器的工作是负责从应用程序中接收数据,然后按视图的需要将这些数据格式化,以便视图将其呈现在屏幕上。例如,如果应用程序需要在屏幕上展示一个日期,那么它传递给展示器的应该是一个Date对象。然后展示器会将该对象格式化成所需的字符串形式,并将其填充到视图模型中

不过,交互器尽管不属于谦卑对象,却是可测试的,因为数据库网关通常可以被替换成对应的测试桩和测试替身类

所以通过在系统的边界处运用谦卑对象模式,我们可以大幅地提高整个系统的可测试性

◆ 第24章 不完全边界

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

◆ 第26章 Main组件

2023/11/10发表想法 Main 负责初始化 系统

Main组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。Main组件中的依赖关系通常应该由依赖注入框架来注入。在该框架将依赖关系注入到Main组件之后

Main组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。Main组件中的依赖关系通常应该由依赖注入框架来注入。在该框架将依赖关系注入到Main组件之后

当我们将Main组件视为一种插件时,用架构边界将它与系统其他部分隔离开这件事,在系统的配置上是不是就变得更容易了呢?

◆ 第27章 服务:宏观与微观

2023/11/10发表想法 高层策略和底层细节之间的架构边界

,架构设计的任务就是找到高层策略与低层细节之间的架构边界,同时保证这些边界遵守依赖关系规则。所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。

,架构设计的任务就是找到高层策略与低层细节之间的架构边界,同时保证这些边界遵守依赖关系规则。所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。

系统架构都是由那些跨越架构边界的关键函数调用来定义的,并且整个架构必须遵守依赖关系规则

任何形式的共享数据行为都会导致强耦合。

例如,如果给服务之间传递的数据记录中增加了一个新字段,那么每个需要操作这个字段的服务都必须要做出相应的变更,服务之间必须对这条数据的解读达成一致。因此其实这些服务全部是强耦合于这条数据结构的,因此它们是间接彼此耦合的。

这就是所谓的横跨型变更(cross-cutting concern)问题,它是所有的软件系统都要面对的问题,无论服务化还是非服务化的

而言之,服务边界并不能代表系统的架构边界,服务内部的组件边界才是。

2023/11/10发表想法 正如采用了微服务的框架,不一定是微服务

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

◆ 第28章 测试边界

因为其中总是充满了各种细节信息,非常具体,所以它始终都是向内依赖于被测试部分的代码的。事实上,我们可以将测试组件视为系统架构中最外圈的程序。它们始终是向内依赖的,而且系统中没有其他组件依赖于它们。

◆ 第6部分 实现细节

从系统架构的角度来看,工具通常是无关紧要的——因为这只是一个底层的实现细节,一种达成目标的手段。一个优秀的架构师是不会让实现细节污染整个系统架构的。

需要了解数据表结构的代码应该被局限在系统架构的最外圈、最低层的工具函数中。

◆ 第32章 应用程序框架是实现细节

请仔细想想这一关系,当我们决定采用一个框架时,就需要完整地阅读框架作者提供的文档。在这个文档中,框架作者和框架其他用户对我们提出进行应用整合的一些建议。一般来说,这些建议就是在要求我们围绕着该框架来设计自己的系统架构。譬如,框架作者会建议我们基于框架中的基类来创建一些派生类,并在业务对象中引入一些框架的工具。框架作者还会不停地催促我们将应用与框架结合得越紧密越好。对框架作者来说,应用程序与自己的框架耦合是没有风险的。毕竟作为作者,他们对框架有绝对的控制权,强耦合是应该的。

框架可能会要求我们将代码引入到业务对象中——甚至是业务实体中。框架可能会想要我们将框架耦合在最内圈代码中。而我们一旦引入,就再也不会离开该框架了,这就像戴上结婚戒指一样,从此一生不离不弃了

2023/11/14发表想法 保持警醒

我们可以使用框架——但要时刻警惕,别被它拖住。我们应该将框架作为架构最外圈的一个实现细节来使用,不要让它们进入内圈。

我们可以使用框架——但要时刻警惕,别被它拖住。我们应该将框架作为架构最外圈的一个实现细节来使用,不要让它们进入内圈。

◆ 第34章 拾遗

2023/11/14发表想法 无法展现具体的业务领域, DDD

这里还存在另外一个问题是,分层架构无法展现具体的业务领域信息。把两个不同业务领域的、但是都采用了分层架构的代码进行对比,你会发现它们的相似程度极高:都有Web层、服务层和数据仓库层。这是分层架构的另外一个问题,后文会具体讲述。

2023/11/14发表想法 真实事件,现在的 MVC 架构经常有这种代码

假设新员工加入了团队,你给新人安排了一个订单相关的业务用例的实现任务。由于这个人刚刚入职,他想好好表现,尽快完成这项功能。粗略看过代码之后,新人发现了OrdersController这个类,于是他将新的订单相关的Web代码都塞了进去。但是这段代码需要从数据库查找一些订单数据。这时候这个新人灵机一动:“代码已经有了一个OrdersRepository接口,只需要将它用依赖注入框架引入控制器就行,我真机智!”几分钟之后,功能已经正常了,但是UML结构图变成了图34.5这样。

虽然新的业务用例可以正常工作,但是它可能不是按照合理方式实现的

我遇见的很多团队仅仅通过采用“自律”或者“代码评审”方式来执行,“我相信我的程序员”。有这种自信当然很好,但是我们都知道当预算缩减、工期临近的时候会发生什么事情

总的来说,这种方式将“业务逻辑”与“持久化代码”合并在一起,称为“组件”

这一章的中心思想就是,如果不考虑具体实现细节,再好的设计也无法长久。必须要将设计映射到对应的代码结构上,考虑如何组织代码树,以及在编译期和运行期采用哪种解耦合的模式