架构整洁之道 (Clean Architecture) 读书笔记
前言
本文是个人结合自己的思考、摘录文中菁华做的读书笔记。笔记有助于快速掌握书中精华、复习小抄,如有没明白之处可对照原文。
书中结合大量的场景、实例介绍架构设计思想,读懂之后大大收益。通篇读下来记住三个关键字:解耦、抽象、实现。也对面经八股文中的 面向切面编程、控制反转 有了更进一步的理解,前者是组件之间的解耦,后者是通过抽象设计来实现架构的可扩展性。
《代码整洁之道》是提升了写代码的功力,《架构整洁之道》则是提升了构建系统的内功,就好像武学中有形的招式与无形的内力。而真正的高手既要过招也要比拼内力。
左耳朵耗子 R.I.P
程序员可以分为三个层次:普通程序员、工程师、架构师。
- 普通程序员:编写代码,让程序跑起来的人。
- 工程师:考虑易读性、易扩展性、易维护性、性能优化等。
- 架构师:解决问题时,结合现有经验、考虑不同的业界解决方案,结合实际场景做出决策/tradeoff。比如基于业务场景给出解决方案、基于更高级的技术给出解决方案、设计更加灵活的系统等等。比 工程师 站在更高的视角考虑问题。
《代码整洁之道》教你写出易读、可扩展、可维护、可重用的代码,《代码整洁之道:程序员的职业素养》教你怎样变成一个有修养的程序员,而《架构整洁之道》基本上是在描述软件设计的一些理论知识。
《架构整洁之道》大体分成三个部分:编程范式(结构化编程、面向对象编程和函数式编程),设计原则(主要是SOLID),以及软件架构(其中讲了很多高屋建翎的内容)
架构或者设计,核心要解决的问题是:分离 控制与逻辑。
- 逻辑:指代业务逻辑,解决用户的问题
- 控制:与业务无关的部分,比如多线程、异步、服务发现、部署、弹性伸缩等切面。
yurii-says/余晟
架构设计是一门复杂的学问,要综合考虑各种因素,做出权衡:
- 编码、质量、部署、发布、运维、排障、升级 等等
概述
为什么要考虑软件的架构与设计?好的架构能够有如下优势
- 节省项目构建、维护的人力成本
- 让需求易于实现,只需对项目做很小的变更
- 避免缺陷
也即,用最小的成本,最大程度满足功能性和灵活性的需求。
第一章、设计与架构究竟是什么
架构是从更高、更抽象的视角描述软件、设计是从更底层的视角描述软件,二者不可分割。后文中出现的架构包括了架构与设计两层意思。
软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。
第二章、两个价值的维度
软件系统的两个价值维度:
- 系统行为的正确性:当前系统能否按照预期运行。
- 系统架构的灵活性:需求迭代时,系统更新的难易程度。
两者都很重要,但前者更加紧急,因为系统在每时每刻都要保证行为的正确性;后者虽然不紧急,但很容易被忽略,而忽略的后果很严重,忽略后系统将变得越来越难以维护,终有一天变得无法再修改,只能重构来实现新功能需求。
系统行为的正确性不需要更多强调,但是系统架构的灵活性则更需要牢记,在设计系统之初、在系统演进时都要思考。
从基础构建开始:编程范式
编程范式指的是程序采用什么样的代码结构,与具体的编程语言关系相对较小。
第三章、编程范式总览
三类编程范式及其意义:
- 结构化编程:对程序控制权的直接转移进行了限制和规范,通俗讲就是限制了
goto
语句 - 面向对象编程(OOP):对程序控制权的间接转移进行了限制和规范, 通俗讲就是限制了函数指针,程序就是类及其之间的函数调用
- 函数式编程:对程序中的赋值进行了限制和规范,通俗讲就是限制了赋值语句
与架构三大关注点的关系:
- 功能性:多态可以跨越架构边界,对应 OOP
- 组件独立性:各个模块/函数内部实现则遵循结构化编程范式
- 数据管理:数据存放与访问权限,对应函数式编程
每个范式都约束了某种编写代码的方式,明确了什么不应该做。
第四章、结构化编程
goto
语句导致功能/模块无法被递归拆分成更小的部分,所以需要限制。
强调将大型系统按照功能拆分成各个模块和组件。并且每个模块/组件可以被方便的测试(单元测试 or 集成测试)。
第五章、面向对象编程
封装:数据和操作数据的函数封装在一个类中,要求高内聚、低耦合。
继承:可以在某个作用域内对外部定义的某一组变量与函数进行覆盖。
多态,实质是一种函数指针的应用:
- 例如,在C++中,类中的虚函数地址都被记录在 vtable 的数据结构中。对虚函数的每次调用都要先查询这个表;接口的实现类的构造函数负责将该类的虚函数地址加载到对象的 vtable 中。
如果没有面向对象技术,直接使用函数指针实现多态充满了危险,因为对指针的正确调用需要人为遵守约定,否则会有 bug 或出错。因此,面向对象编程语言为我们消除了人工遵守约定的必要,也就等于消除了这方面的危险性。采用面向对象编程语言让多态实现变得非常简单,让一个传统C程序员可以去做以前不敢想的事情。
通过多态,面向对象编程其实是对程序间接控制权的转移进行了约束。
通过多态,能够实现插件式架构,可以在调用方不做变更情况下,替换底层的实现。
- 例如,Unix 系统将 IO 设备设计成插件形式,即不同的驱动设备程序只要实现指定的接口,就能被系统调用。
依赖反转,是一种设计思想,这里可以粗暴的理解为面向接口编程。上层函数只能调用底层的类的接口,而接口一般都是稳定不变的,但是接口的实现可以有很多变化。
- 在不用依赖反转时,依赖的方向是 上层函数依赖其调用的函数。
- 在使用依赖反转时,依赖的方向变成,底层函数的实现需要依赖事先约定好的接口,具体来说就是函数签名不能变。
依赖反转的好处是
- 让组件具有独立部署能力:当某个组件的源代码需要修改时,仅仅需要重新部署该组件,不需要更改其他组件
- 让系统具有独立开发能力:如果系统中的所有组件都可以独立部署,那它们就可以由不同的团队并行开发,这就是所谓的独立开发能力
举个例子:利用依赖反转的设计,让数据库模块和用户界面模块都依赖于业务逻辑模块(而非相反),让用户界面和数据库都成为业务逻辑的插件。这样一来,业务逻辑、用户界面以及数据库就可以被编译成三个独立的组件或者部署单元。
面向对象编程对架构的意义:面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力,这种能力让软件架构师可以构建出某种插件式架构,让高层策略性组件与底层实现性组件相分离,底层组件可以被编译成插件,实现独立于高层组件的开发和部署。
第六章、函数式编程
函数式编程中一个核心的概念是 —— 不可变性。比如 Scala 编程语言中的 val
关键字指定不可变变量。
不可变性与架构的关系:所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。
根据可变性,对系统进行隔离:
- 一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。
- 软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。
不可变性在架构上的应用 —— 事件溯源。举个例子
- 银行维护账户余额时,不保存具体账户余额,仅仅保存事务日志,那么当有人想查询账户余额时,我们就将全部交易记录取出,并且每次都得从最开始到当下进行累计。这样就不需要变量了。类似日志重放。
- 并不需要这个设计永远可行,而且可能在整个程序的生命周期内,我们有足够的存储和处理能力来满足它。
设计原则
这一部分内容主要讲如何利用 SOLID 原则将数据和函数组织成类,以及将这些类链接成程序。这里的程序可以指大型软件系统,也可以指单个组件。 下面简单、直观的介绍 SOLID 原则。
- SRP: 单一职责原则
- 每个软件模块都有且只有一个被改变的理由。
- OCP: 开闭原则
- 如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。
- LSP: 里氏替换原则
- 如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,一遍让这些组件可以相互替换。
- ISP: 接口隔离原则
- 软件设计避免不必要的依赖
- DIP: 依赖反转原则
- 高层策略性代码不应该依赖底层实现细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。
第七章、SRP 单一职责原则
每一个软件模块都应该只对一类行为者负责。
文章举了一个非常有意思的例子,一个 Employee
类里面提供了 calculatePay()
和 reportHours()
两个函数,前者是为了给财务部门计算薪酬、后者是为了给人力部门汇报工时。
由于在一个类里面,所以这两个函数复用了 regularHours()
函数来计算工时。
如果财务部门需要修改工时计算方法,而负责修改的程序员由于没有注意到 regularHours()
函数还被 reportHours()
调用了,所以直接修改了 regularHours()
函数。此时人力部门看到结果也是经过修改过后的数据!!很容易导致问题!!
因此,需要将 calculatePay()
和 reportHours()
两个功能实现拆分到不同的类当中去,并使用各自私有的计算工时的函数。这样,有两个类分别对应财务和人力的计算逻辑,它们只对一个行为者负责。
第八章、OCP 开闭原则
设计良好的计算机软件应该易于扩展,同时抗拒修改。换句话说,一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。
通俗的说,设计要考虑软件的扩展性。
实现的方式(个人理解)就是,利用依赖反转原则,将高层抽象定义成接口,应用程序面向接口编程,而屏蔽底层实现细节,同时接口也可以实现多态。接口尽量保证稳定。
第九章、LSP 里氏替换原则
面向接口编程,应用程序通过调用各个接口完成处理逻辑,实现接口的不同衍生类可以相互替换。
还是在讨论扩展性问题,继承就是将上层抽象和底层实现隔离,在接口不变动的情况下,具体实现类之间可以相互替换而不影响系统整体功能。
第十章、ISP 接口隔离原则
任何层次的设计,都不应该依赖它并不需要的东西。
举个例子,有如下的 OPS 类有三个函数,分别被 User1、User2、User3 类中的函数直接调用。这种直接依赖导致任何对 OPS 的 op1 函数的修改也会导致 User2、User3 被重新编译。
在 User 和 OPS 中间添加一个接口层,让底层实现 OPS 依赖接口(依赖反转),User 类调用接口。这样就能保证每个类不依赖最小化。
public interface U1Ops {public void op1();}
public interface U2Ops {public void op2();}
public interface U3Ops {public void op3();}
public class OPS implements U1Ops, U2Ops, U3Ops {
@Override
public void op1() {}
@Override
public void op2() {}
@Override
public void op3() {}
}
第十一章、DIP 依赖反转原则
依赖反转原则(DIP)主要想告诉我们的是,如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。
也就是说,在Java这类静态类型的编程语言中,在使用use、import、include这些语句时应该只引用那些包含接口、抽象类或者其他抽象类型声明的源文件,不应该引用任何具体实现。这里是针对那些经常会变动的实现而言的,并不包括像 String 这类稳定实现的类。
通过依赖稳定的抽象层,来实现依赖反转。有一些可以按需遵循的最佳实践
- 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类
- 不要在具体实现类上创建衍生类
- 不要覆盖(override)包含具体实现的函数
以下图为例,这条曲线将整个系统划分为两部分组件:抽象接口与其具体实现。抽象接口组件中包含了应用的所有高阶业务规则,而具体实现组件中则包括了所有这些业务规则所需要做的具体操作及其相关的细节信息。图中曲线代表架构边界。
组件构建原则
SOLID 原则用来指导如何实现各个组价,组件构建原则用来指导如何构建系统。
第十二章、组件
组件是软件的部署单元,是整个软件系统在部署过程中可以独立完成部署的最小实体。例如,对于Java来说,它的组件是jar文件/maven模块。
为了介绍程序组件部署历史,介绍了下
- 重定位技术:随着应用程序代码的增长,无法在固定位置加载依赖库代码,否则会导致应用程序代码被切断。通过重定位技术,使得依赖库代码可以被加载到内存的不同位置。可参考Relocation 介绍
- 链接器(linker):程序、依赖库被分别编译、加载到内存后,将程序中调用的函数链接到依赖中对应的地址。
第十三章、组件聚合
那么那些类应该被组合成一个组件呢?本章将介绍3个基本原则:
- REP(Reuse/Release Equivalence Principle): 复用/发布等同原则
- 软件复用的最小粒度等同于其发布的最小粒度,需要有自己的发布版号(另外还要有通知、发布文档)
- CCP(Common Closure Principle): 共同闭包原则
- 对应组件层面的 SRP 原则,将那些会同时修改、并且为相同目的而修改的类放到同一个组件中,而将不会同时修改、不会为了相同目的而修改的类放到不同组件中。这样变更都会集中在某个组件中,只要重新发布、部署单个组件即可。
- CRP(Common Reuse Principle): 共同复用原则
- 不要强迫一个组件的用户依赖他们不需要的东西,不紧密相连的类不该放在同一组件中。
- 是 ISP 原则的普适版,都强调不要依赖不需要用到的东西。
REP和CCP原则是黏合性原则,它们会让组件变得更大,而CRP原则是排除性原则,它会尽量让组件变小。软件架构师需要在这三个原则中取舍。 如下图所示,如果忽略某个方框中的原则,将会其对边上的问题。
┌─────────────┐ ┌─────────────┐
│ │ too many redundent release │ │
│REP principle│────────────────────────────────────│CCP principle│
│为复用性而组合 │ │ 为维护性而组合 │
└─────────────┘ └─────────────┘
\ /
\ 如何在这片区域内定位 /
过多组件变更 /
\ 难以复用
\ /
\ /
\ ┌─────────────┐ /
│ │
│CRP principle│
│为减少发布而拆分│
└─────────────┘
随着项目的发展,项目在上图三角形中的位置不断改变:在前期主要牺牲复用性(右侧),成熟后被依赖会变多,项目定位开始左移。主要与开发进度和被使用方式有关。
第十四章、组件耦合
组件依赖关系图中不应该出现环
- 打破循环依赖的方法:依赖反转,打破环的某一边,让该边连接的两个组件都依赖于一个新的接口,接口放在依赖组件的那一侧。
示例
如下图,Interactor
、Authorizer
、Entities
形成了循环依赖。 Database
组件原本只依赖 Entities
组件,现在却必须也要与 Authorizer
组件兼容。比如 Database
需要升级 Entities
组件到 Entities-1.10 版本,但是它依赖了 Authorizer
组件的 Authorizer-1.12 版本,如果低于这个版本会报错;此时,Database
组件相当于间接依赖了 Authorizer
。
┌────────────┐ ┌─────────────┐
│ Interactor │◀────────────────────│ Authorizer │
└────────────┘ └─────────────┘
│ ▲
│ │
▼ │
┌─────────────┐ │
│ Entities │───────────────────────────┘
└─────────────┘
▲
│
│
┌─────────────┐
│ Database │
└─────────────┘
此时的解决方案有两类:
- 技术上:将 Entities 组件打成一个 uber-jar,除了把自己的二进制代码打包到当前组件中,还将部分依赖组件的二进制代码打包到当前组件中。同时为了避免使用当前组件的程序错误使用当前组件中的依赖组件而产生冲突,还会搭配 shade 技术,a.k.a 在当前的组件里面重命名依赖的组件的包名。
- 设计上:有两种方法
- 应用依赖反转原则,在 Entities 组件中定义一个接口,Entities 组件中使用该接口,Authorizer 组件中实现该接口。
- 重新定义一个组件,将有相互依赖的内容放到新组件中
/**
* 依赖反转
* ┌──────────────┐ ┌──────────────┐
* │ Entities │ │ Authorizer │
* │ │ │ │
* │ ┏━━━━┓ │ │ ┏━━━━━━━━━━┓│
* │ ┃User┃──┼───────────────────┼─▶┃Permission┃│
* └──────┻━━━━┻──┘ └──┻━━━━━━━━━━┻┘
*
*
* ┌─────────────────┐ ┌──────────────┐
* │ Entities │ │ Authorizer │
* │┏━━━━┓ │ │ │
* │┃User┃ │ │ ┏━━━━━━━━━━┓│
* │┗━━━━┛ │ ┌──────┼─▶┃Permission┃│
* │ │ ┏━━━━━━━━━━┫ │ └──┻━━━━━━━━━━┻┘
* │ │ ┃Permission┃ │
* │ └─▶┃Interface ┃─────────┘
* └──────┻━━━━━━━━━━┛
*/
/**
* 重新定义组件
* ┌──────────────┐ ┌──────────────┐
* │ Interactor │◀──────────────│ Authorizer │
* └──────────────┘ └──────────────┘
* │ │
* │ │
* ▼ ▼
* ┌──────────────┐ ┌──────────────┐
* │ Entities │─────────────▶ │ Permission │
* └──────────────┘ └──────────────┘
* ▲
* │
* │
* ┌──────────────┐
* │ Database │
* └──────────────┘
*/
稳定依赖原则(SDP):依赖关系必须指向更稳定的方向。通俗说,一个需要稳定的组件不应该依赖经常会变动的组件,否则就没办法保证稳定。
一个设计越是抽象,那么它应该是越不稳定的,因为可以有多种实现方式。然而却又有如下原则。
稳定抽象原则(SAP):一个组件的抽象化程度应该与稳定性保持一致。
SDP + SAP => 组件需要稳定,但也不能一成不变,所以一个组件应该是部分抽象、部分稳定的。抽象的部分可以通过开闭原则来保证扩展性,稳定的部分最好不要轻易变动。
此外,本章节还介绍了一些依赖关系管理的指标。当然,只是用来衡量的工具,不代表真理。
软件架构
第十五章、什么是软件架构
- 架构师必须是一线程序员,只有对糟糕设计有深切体会的架构师才知道合理架构对最大化生产力的重要性。
- 架构工作概括:系统如何切分成组件、组件之间如何排列、组件之间如何通信。
- 架构目的:
- 支撑系统生命周期
- 为了让系统便于理解、修改、维护、部署
- 最大化生产力、最小化运营成本。
- 软件系统可以结构为 抽象 + 实现 两类元素
- 抽象对应业务规则,实现对应具体细节。在架构设计过程中
- 抽象 应该完全不依赖与 实现,并且尽可能推迟确定实现细节。这样可以为系统选择不同的实现组件。比如抽象出系统所用 KV 接口,底层可以选择 RocksDB、LevelDB、Redis 等等各种具体的实现组件。
- 一个优秀的软件架构师应该致力于最大化可选项(Option)数据。
第十六章、独立性
表面性的重复:处理数据库的函数和某个 UI 函数一样,此处只是表面性的重复,不应该复用。
利用各种解耦方法,让系统的各个部分保留一定独立性
- 源码层次独立:控制源码模块间依赖,实现单个模块变更不导致其他模块重新编译
- 部署层次独立:控制部署单元(jar、共享库等)之间的依赖,实现一个模块变更不会导致其他模块的重新构建和部署
- 服务层次的独立性:将组件间的依赖关系降低到数据结构级别,仅通过网络数据包进行通信。
不同的解耦方法适用于不同的场景,随着项目的发展,最优的方法也会随之改变。比如由单机项目变成分布式的,则最佳解耦方法可能由源码层次独立变成部署层次独立,甚至是服务层次独立。
第十七章、划分边界
划分边界的好处:通过划清边界,可以推迟和延后一些细节性的决策,可以节省大量的时间、避免大量的问题。这就是一个设计良好的架构所应该带来的助益。比如,在设计一个 web 服务时,不用在一开始就考虑数据库选型,因为最终可能仅仅只需要静态网页,根本用不到数据。
如何划分边界:在不相关的组件之间划分边界,比如 GUI 、Database 与 业务逻辑之间就可以划分边界。 利用 SRP 原则来考虑如何划分边界。
插件式架构:通过定义好抽象接口,可以选用不同的底层实现。
为了在软件架构中画边界线,我们需要先将系统分割成组件,其中一部分是系统的核心业务逻辑组件,而另一部分则是与核心业务逻辑无关但负责提供必要功能的插件。然后通过对源代码的修改(此处修改指的是添加 Wrapper 做适配,而并非真的修改数据库等组件源代码),让这些非核心组件依赖于系统的核心业务逻辑组件。
第十八章、边界剖析
需要管控跨边界的依赖
- 源码层次:通过跨边界的函数调用,实现了依赖。因为当一个模块变更之后,依赖的那个模块可能也需要重新编译。
- 部署层次:也是通过函数调用实现依赖,依赖一个动态链接库。
- 服务层次:边界形式最强,通过网络调用,延迟很大,因此需要控制次数。
第十九章、策略与层次
- 策略即抽象。
- 离 I/O 越远,抽象的层次越高。
- 层次低组件的依赖层高的组件,可以通过定义接口实现。
第二十章、业务逻辑
应用程序分为业务逻辑和插件两部分。
- 业务逻辑:是系统核心逻辑,产生价值的逻辑
- 业务实体:包含了业务逻辑的关键数据
- 用例:系统在某个特定场景下的运行逻辑
- 插件:为了完成业务逻辑而提供的服务功能
第二十一章、尖叫的软件架构
- 架构应该面向要解决的问题(用例),而不是具体的框架、工具。
- 可测试的架构设计:测试时不应该运行 web 服务、不应该链接数据库。
- 优秀的架构设计,应该能让人第一眼看出来这个系统是为了解决什么问题的。
- 了解一个系统也应该从测试用例开始看起,而非陷于各个模块的细节。
第二十二章、整洁架构
-
各种架构方式的共同点:
- 相同的设计目标:按照不同关注点对软件进行切割
- 独立于框架:可以使用框架,但不与某个框架深度绑定
- 可被测试:业务逻辑可以脱离 UI、数据库、Web服务等其他外部元素
- 独立于 UI:UI 变更不会影响其他部分。
- 独立于数据库:可以轻易使用不同的数据库来替换
- 独立于外部机构:不依赖于其他外部接口
-
依赖关系规则
- 同心圆分别代表了软件系统中的不同层次,通常越靠近中心,其所在的软件层次就越高。基本上,外层圆代表的是机制,内层圆代表的是策略。
- 源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。
- 跨越边界的数据在数据结构上都是很简单的
- 为了避免内层使用外层的数据结构,当我们进行跨边界传输时,一定要采用内层最方便使用的形式。
/**
* 整洁架构
* ┌──────────────────────────────────────┐
* │ ┌────────────────────────────────┐ │
* │ │ │ │
* │ │ ┌──────────────────────┐ │ │
* │ │ │ ┌────────────┐ │ │ │
* │ ──┼─▶ ──┼─▶ ────┼▶Core Logic │ │ │ │
* │ │ │ └────────────┘ │ │ │
* │ │ │ │ │ │
* │ │ │ Use Case │ │ │
* │ │ └──────────────────────┘ │ │
* │ │ Interface Adapter │ │
* │ └────────────────────────────────┘ │
* │ │
* │ Framework/device driver/etc. │
* └──────────────────────────────────────┘
*/
第二十三章、展示器和谦卑对象
谦卑对象模式
- 是为了将系统中难以测试的部分与易于测试的部分分割成不同模块。难以测试的模块被称为 谦卑组,通常在组件边界处,比如数据库、UI 等。又因为跨越边界传输的数据一般都是简单/通用的数据类型,所以可以通过插桩/mock的形式来实现。
- 目的是为了提高系统的可测试性。
第二十四章、不完全边界
不完全边界
- 概念:在构建系统时如果严格按照边界划分,分成不同组件、独立部署,那么成本是很高的。但为了应对将来可能的需要,通常会预留一个边界。
- 做法:
- 将系统分割成一系列独立编译、部署的组件后,再把它们构建成一个组件。
- 门户模式(facade pattern)中的 Facade 类决定为 Client 类选择具体的一种实现类。此时, Client 会依赖所有的实现类,但也很容易改造符合架构边界的实现方式(利用依赖反转原则,添加接口)。
第二十五章、层次与边界
- 本节结合一个例子来说明架构的边界可以存在任何地方。
- 架构师需要审视在什么地方设计架构边界、成本有多大;如果前期忽略,后期增加架构边界是否容易。
- 随着系统演进,架构师需要不断审视在何处增加架构边界 —— 增加与否所要承担的成本。
第二十六章、Main 组件
- 系统只要有一个 Main 组件,负责创建、协调、监督其他组件的运行。
- Main 组件控制全局,依赖所有组件,因此抽象层次最低。
- 是入口,包含最多的细节。
- 负责加载所有必要信息,将控制权交给系统高层抽象的组件。
- 可以看成是启动的插件,可以有多个,每个对应不同的配置。
第二十七章、服务:宏观与微观
- 面向服务的架构(SOA,此处着重指微服务)并不能做到完全解耦,服务之间传递的数据发生变化时,服务的另一侧仍然需要变更。因此,服务边界 ≠ 架构边界。
- 可以使用模板方法/策略模式来实现解耦。
第二十八章、测试边界
- 强调测试重要性,测试也是系统的一个组件,如 Maven 中 dependency 中的 Scope 有 test 选项。
- 测试组件是供开发系统使用的。
- 设计测试时也要考虑鲁棒性,否则对系统简单的修改都会导致大量测试报错,难以维护。
第二十九章、整洁的嵌入式架构
略
实现细节
第三十章、数据库只是实现细节
- 设计软件架构时,更多的应该关心业务逻辑,数据库技术只是实现细节,可以在底层实现上优化,不应当影响上层抽象。
- 系统架构设计还会受市场影响,比如用户就是希望你的软件中的数据库全部用 TiDB,因为他觉得 TiDB 比 MySQL 更牛逼,即使他只听宣传这么说。
第三十一章、Web 是实现细节
- Web 技术并没有改变任何东西,只是将计算在客户端(浏览器)、服务端之间搬运:开始计算都在服务器上,后来 ajax 将运算搬到浏览器上,后来 Nodejs 又将计算搬回服务端。
- UI 是实现细节,应当与业务逻辑划分边界。保证 UI 的演进不影响核心业务逻辑。
第三十二章、应用程序框架式实现细节
尽可能不与第三方框架绑定,但是可以使用语言自带的类库,如 C++/Java 标准类库。
第三十三章、案例分析
略
第三十四章、拾遗
如果不考虑具体实现细节,再好的设计也无法长久。必须要将设计映射到对应的代码结构上,考虑如何组织代码树,以及在编译期和运行期采用哪种解耦合的模式。保持开放,但是一定要务实,同时要考虑到团队的大小、技术水平,以及对应的时间和预算限制。最好能利用编译器来维护所选的系统架构设计风格,小心防范来自其他地方的耦合模式,例如数据结构。所有的实现细节都是关键的!