序
- 程序员的三个层次
(1) 普通程序员
编写代码,能够让程序跑起来的人。
(2) 工程师
有“洁癖”、有工匠精神、有修养的程序员。他们把编程当成一种设计,一种工业设计。
(3) 架构师
解决系统性问题、经验足、不怕难的工程师。 - 软件架构的规则其实就是排列组合代码块的规则。软件架构的规则是相同的,与应用和系统无关。
第 1 部分 概述
第1章 设计与架构究竟是什么
- 架构即是设计。
- 软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。
- 现在的软件研发工程师都有点过于自信,但是他们真正偷懒的地方在于——持续低估那些好的、良好设计的、整洁的代码的重要性。
第2章 两个价值维度
- 每个软件系统,我们都可以通过行为和架构两个维度来体现它的实际价值。
- 行为价值
软件系统的功能。 - 架构价值
软件系统容易被修改。
第 2 部分 从基础构件开始:编程范式
第3章 编程范式总览
- 一共只有三个编程范式
(1) 结构化编程(structured programming
)
(2) 面向对象编程(object-oriented programming
)
(3) 函数式编程(functional programming
) - 结构化编程(
structured programming
)
结构化编程对程序控制权的直接转移进行了限制和规范。
限制
goto
语句。
- 面向对象编程(
object-oriented programming
)
面向对象编程对程序控制权的间接转移进行了限制和规范。
限制函数指针。
- 函数式编程(
functional programming
)
函数式编程对程序中的赋值进行了限制和规范。
限制赋值语句。
- 每个编程范式的目的都是设置限制。这些范式主要是为了告诉我们不能做什么,而不是可以做什么。
第4章 结构化编程
- 人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。
-
goto
是有害的
随着编程语言的演进,goto
语句的重要性越来越小,最终甚至消失了。如今大部分的现代编程语言中都已经没有了goto
语句。就算那些还支持goto
关键词的编程语言也通常限制了goto
的目标不能超出当前函数范围。 - 测试
测试只能展示Bug
的存在,并不能证明不存在Bug
。
一段程序可以由一个测试来证明其错误性,但是却不能被证明是正确的。测试的作用是让我们得出某段程序已经足够实现当前目标这一结论。 - 软件开发虽然看起来是在操作很多数学结构,其实不是一个数学研究过程。恰恰相反,软件开发更像是一门科学研究学科,我们通过无法证伪来证明软件的正确性。
- 结构化编程范式促使我们先将一段程序递归降解为一系列可证明的小函数,然后再编写相关的测试来视图证明这些函数的错误。如果这些测试无法证伪这些函数,那么我们就可以认为这些函数是足够正确的,进而推导整个程序是正确的。
- 结构化编程范式中最有价值的地方就是,它赋予我们创造可证伪程序单元的能力。
第5章 面向对象编程
- 什么是面向对象?
数据和函数的组合。——不够贴切。
一种对真实世界进行建模的方式。——避重就轻。
封装、继承、多态。——神秘术语。
第6章 函数式编程
- 函数式编程所依赖的原理, 在很多方面其实是早于编程本身出现的。
- 函数式编程语言中的变量是不可变的。
所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。 - 不可变性是否实际可行?
如果我们能忽略存储器与处理器在速度上的限制,那么答案是肯定的。否则的话,不可变性只有在一定情况下是可行的。
第 3 部分 设计原则
-
SOLID
原则
SRP
:单一职责原则。
一个软件模块只有一个需要被改变的理由。
OCP
:开放封闭原则。
如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。
LSP
:里氏替换原则。
如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
ISP
:接口隔离原则。
软件设计师应该在设计中避免不必要的依赖。
DIP
:依赖反转原则。
高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。
第7章 SRP:单一职责原则
- 任何一个软件模块都应该有且仅有一个被修改的原因。
单一职责原则的含义并不是每个模块应该只做一件事。
- 任何一个软件模块都应该只对某一类行为者负责。
在大部分情况下,软件模块就是指一个源代码文件。
第8章 OCP:开放封闭原则
- 设计良好的计算机软件应该易于扩展,同时抗拒修改。
- 软件架构师根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。
- 软件系统不应该依赖其不直接使用的组件。
第9章 LSP:里氏替换原则
- 正方形/长方形问题是一个著名的违反
LSP
的设计案例。用户需要增加区分两者的检测逻辑(例如增加if
语句),用户的行为依赖它所使用的类,这两个类就不能互相替换。 -
LSP
可以且应该被应用于软件架构层面,因为一旦违背了可替换性,该系统架构就不得不为此添加大量复杂的应对机制。
第10章 ISP:接口隔离原则
- 有多个用户需要操作
OPS
类。现在,我们假设这里的User1
只需要使用op1
,User2
只需要使用op2
,User3
只需要使用op3
。
违反ISP
的UML
类图:
符合ISP
的UML
类图:
- 任何层次的软件设计如果依赖于不需要的东西,都会是有害的。从源码层次来说,这样的依赖关系会导致不必要的重新编译和重新部署,对更高层次的软件架构设计来说,问题也是类似的。
- 任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。
第11章 DIP:依赖反转原则
- 依赖反转原则(
DIP
)主要想告诉我们的是,如果想要设计一个灵活的系统,在源代码层次的依赖关系就应该多引用抽象类型,而非具体实现。 - 每次修改抽象接口的时候,一定也会去修改对应的具体实现。但反过来,当我们修改具体实现时,却很少需要去修改相应的抽象接口。所以,我们可以认为接口比实现更稳定。
- 该设计原则可以归结为以下几条具体的编码守则:
① 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
对象的创建过程应该受到严格限制,我们通常会选择用抽象工厂(abstract factory
)这一设计模式。
② 不要在具体实现类上创建衍生类。
继承关系是所有一切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使用应该格外小心。
③ 不要覆盖包含具体实现的函数。
创建一个抽象函数,然后再为该函数提供多种具体实现。
④ 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。
这基本上是DIP
原则的另外一个表达方式。
第 4 部分 组件构建原则
第12章 组件
- 组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。设计良好的组件都应该永远保持可被独立部署的特性。
第13章 组件聚合
- 哪些类可以被组合成一个组件呢?
三个构建组件相关的基本原则:
REP
:复用/发布等同原则。
CCP
:共同闭包原则。
CRP
:共同复用原则。 - 复用/发布原则
软件复用的最小粒度等同于其发布的最小粒度。 - 共同闭包原则
将由于相同原因而修改,并且需要同时修改的东西放在一起。将由于不同原因而修改,并且不同时修改的东西分开。对大部分应用程序来说,而维护性的重要性要远远高于可复用性。 - 共同复用原则
不要强迫一个组件的用户依赖他们不需要的东西。
将经常共同复用的类和模块放在同一个组件中。不是紧密相连的类不应该被放在同一个组件里。
我们希望组件中的所有类是不能拆分的,即不应该出现别人只需要依赖它的某几个类而不需要其他类的情况。 - 三个原则之间存在着竞争关系,
REP
和CCP
原则是粘合性原则,他们会让组件变得更大,而CRP
原则是排除性原则,他会尽量让组件变小。架构师的任务就是要在这三个原则中间进行取舍。
第14章 组件耦合
- 无依赖环原则
组件依赖关系图中不应该出现环。
循环依赖的组件之间事实上被合并成了个一个更大的组件。 - 打破循环依赖(打破
A
依赖B
)
(1) 应用依赖反转原则
创建C
,使A
持有C
,B
继承C
。
(2) 创建一个新组件
创建C
,并让A
和B
都依赖C
。
将A
和B
中互相依赖的类全部放入C
。 - 自上而下的设计
组件结构图是不可能自上而下被设计出来的。他必须随着系统的变化而扩张,而不可能在系统构建的最初就被完美设计出来。
最初我们对项目中的共同闭包一无所知,也不可能知道哪些组件可以复用。因此组件依赖关系是必须要随着项目的逻辑关系一起扩张和演进的。 - 稳定性指标
Fan-in
:入向依赖,指代了组件外部类依赖于组件内部类的数量。
Fan-out
:出向依赖,指代了组件内部类依赖于组件外部类的数量。
I
不稳定性:Fan-out
/Fan-in
+Fan-out
。
I
指标的范围时[0,1]
,I = 0
最稳定,I = 1
最不稳定。
该指标是通过统计和组件内部类有依赖的组件外部类的数量来计算的。 - 稳定依赖原则(
SDP
)
依赖关系必须要指向更稳定的方向。
任何一个我们预期会经常变更的组件都不应该被一个难于修改的组件所依赖。
稳定依赖原则(SDP
)要求组件结构依赖图中各组件的I
指标必须要按其依赖关系方向递减。 - 稳定抽象原则
一个组件的抽象化程度应该与其稳定性保持一致。一方面,该原则要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩展性。另一方面,该原则也要求一个不稳定的组件应该包含具体的实现代码,这样它的不稳定性就可以通过具体的代码被轻易修改。
即:依赖关系应该指向更抽象的方向。
第 5 部分 软件架构
第15章 什么是软件架构
- “架构”这个词给人的直观感受就充满了权利和神秘感,因此谈论架构总让人有一种进行责任重大的决策或者深度技术分析的感觉。
- 首先,软件架构自身需要是程序员,并且必须一直坚持做一线程序员,绝对不要听从那些说应该让软件架构师从代码中解放出来以专心解决高阶问题的伪建议。
- 软件架构师应该是能力最强的一群程序员,他们通常会在自身承接编程任务的同时,逐渐引导整个团队向一个能够最大化生产力的系统设计方向前进。
- 软件架构师必须不停地承接编程任务。如果不亲自承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
- 软件系统的架构质量是由他的构建者所决定,软件架构这项工作的实质就是规划如何将系统切分为组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。
- 软件架构设计的主要目标是支撑软件系统的全生命周期,设计良好的架构可以让系统便于理解、易于修改、方便维护,并且能轻松部署。软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
- 如果想设计一个便于推进各项工作的系统,其策略就是要在设计中尽可能长时间地保留尽可能多的可选项。一个优秀的软件架构师应该致力于最大化可选项的数量。
- 优秀的架构师会小心的将软件的高层策略与其底层实现隔离开,让高层策略与实现细节脱钩,使其策略部分完全不关心底层细节,当然也不会对这些细节有任何形式的依赖。另外,优秀的架构师所设计的策略应该允许系统尽可能地推迟与实现细节相关的决策,越晚做决策越好。
第16章 独立性
- 一个设计良好的软件架构必须支持以下几点。
系统的用例与正常运行。
系统的维护。
系统的开发。
系统的部署。 - 重复
代码重复有时候是假的,或者说只是表面上的重复。如果有两段看起来重复的代码,他们走的是不同的演进路径,也就是说它们有着不同的变更速率和变更缘由,那么这两段代码就不是真正的重复代码。
第17章 划分边界
- 软件架构设计本身就是一门划分边界的艺术。边界的作用是将软件分割成各种元素,以便约束边界两侧之间的依赖关系。
- 架构师所追求的目标是最大限度地降低构建和维护一个系统所需的人力资源。一个系统最消耗人力资源的是系统中的耦合——尤其是那些过早做出的、不成熟的决策所导致的耦合。
- 通过划清边界,我们可以推迟和延后一些细节性的决策。边界线应该画在那些不相关的事情中间。
GUI
和业务逻辑无关,数据库和GUI
无关,数据库和业务逻辑无关,这些两者之间都应该有一条边界线。 - 为了在软件架构中画边界线,我们需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改,让这些非核心组件依赖于系统的核心业务逻辑组件。
即:依赖箭头应该由底层具体实现细节指向高层抽象的方向。
GUI
和业务逻辑中间应该有一条线。widgetView
指向widgetModel
,widgetModel
不能指向widgetView
。
第18章 边界剖析
- 一个系统的架构是由一系列软件组件以及他们之间的边界共同定义的。而这些边界有着不同的存在形式。
第19章 策略与层次
- 所有软件系统都是一组策略语句的集合。计算机程序不过就是一组仔细描述如何将输入转化为输出的策略语句的集合。
- 软件架构设计的工作重心就是,将这些策略彼此分离,然后将它们按照变更的方式进行重新分组。其中变更原因、时间和层次相同的策略应该被分到同一个组件中。反之,变更原因、时间和层次不同的策略则应该分属于不同的组件。
- 架构设计的工作常常需要将组件重排组合成为一个有向无环图。图中的每一个节点代表的是一个拥有相同层次策略的组件,每一条单向链接都代表了一种组件之间的依赖关系,它们将不同级别的组件链接起来。
- 在一个设计良好的架构中,依赖关系的方向通常取决于它们所关联的组件的层次。一般来说,低层组件被设计为依赖于高层组件。
从另一个角度来说,低层组件应该成为高层组件的插件。
第20章 业务逻辑
- 我们可以将自己的应用程序划分为业务逻辑和插件两部分,前者是应用程序的核心。
- 业务实体包含了一系列用于操作关键数据的业务逻辑。业务实体与数据库、用户界面、第三方框架等部分无关。业务实体这个概念中应该只有业务逻辑,没有别的。
- 业务逻辑是整个软件系统的皇冠明珠。业务逻辑应该保持纯净、不要掺杂用户界面或者所使用的数据库相关的东西。
- 在理想情况下,代表业务逻辑的代码应该是整个系统的核心,其他低层概念的实现应该以插件形式接入系统中。业务逻辑应该是系统中最独立、复用性最高的代码。
第21章 尖叫的软件架构
- 一个良好的架构设计应该围绕着用例来展开,这样的架构设计可以在脱离框架、工具以及使用环境的情况下完整地描述用例。这就好像一个住宅建筑设计的首要目标应该是满足住宅的使用需求,而不是确保一定要用砖来构建这个房子。
- 良好的架构设计应该尽可能地允许用户推迟和延后决定采用什么框架、数据库、
Web
服务以及其他与环境相关的工具。架构师应该花费很多精力来确保该架构的设计在满足用例需要的情况下,尽可能地允许用户能自由地选择建筑材料(砖头、石料或者木材)。 - 框架是工具而不是生活信条
框架作者往往对自己写出的框架有着极深的信念,他们所写出来的使用手册一般都是从如何成为该框架的虔诚信徒的角度来描绘如何使用这个框架的。
这不应该成为你的观点。我们要带着怀疑的态度审视每一个框架,避免让框架主导我们的架构设计。 - 可测试的架构设计
我们在运行测试的时候不应该运行Web
服务,也不应该需要连接数据库。我们测试的应该只是一个简单的业务实体对象,没有任何与框架、数据库相关的依赖关系。
第22章 整洁架构
- 不同架构在细节上各有不同,但总体上是非常相似的。他们都具有同一个设计目标:按照不同关注点对软件进行切割。也就是说,这些架构都会将软件切割成不同的层,至少有一层是只包含该软件的业务逻辑的,而用户接口、系统接口则属于其他层。
(1) 独立于框架。
(2) 可被测试。
(3) 独立于UI
。
(4) 独立于数据库。
(5) 独立于任何外部机构。
第23章 展示器和谦卑对象
- 谦卑对象模式最初的设计目的是帮助单元测试的编写者区分容易测试的行为与难以测试的行为,并将它们隔离。
第24章 不完全边界
- 构建完整的架构边界是一件很耗费成本的事。需要为系统设计双向的多态边界接口,用于输入和输出的数据结构,以及所有相关的依赖关系管理,以便将系统分割成可独立编译与部署的组件。这里会涉及大量的前期工作,以及大量的后期维护工作。
- 很多情况下,设计架构边界的成本太高了,但为了应对将来可能的需要,需要预留一个边界,这时就需要引入不完全边界的概念。
第25章 层次与边界
- 人们通常习惯于将系统分成三个组件:
UI
、业务逻辑和数据库。对于稍复杂一些的系统,组件远不止三个。 - 架构边界可以存在于任何地方。作为架构师,我们必须要小心审视究竟在什么地方才需要设计架构边界。另外,我们还必须弄清楚这些边界将会带来多大的成本。
- 软件架构师必须仔细权衡成本,决定哪里需要设计架构边界,以及这些地方需要的是完整的边界,还是不完整的边界,还是可以忽略的边界。
第26章 Main组件
-
Main
组件是系统中最细节化的部分——也就是底层的策略,它是整个系统的初始点。在整个系统中,除了操作系统不会再有其他组件依赖它了。Main
组件的任务是创建所有的工厂类、策略类以及其他的全局设施,并最终将系统的控制权转交给最高抽象层的代码来处理。 -
Main
组件是整个系统中细节信息最多的组件。
第27章 服务:宏观与微观
- 架构设计的任务就是找到高层策略与低层细节之间的架构边界,同时保证这些边界遵守依赖关系规则。
- 虽然服务化可能有助于提升系统的可扩展性和可研发性,但服务本身却代表整个系统的架构设计。系统的架构是由系统内部的架构边界,以及边界之间的依赖关系定义的,与系统中各组件之间的调用和通信方式无关。