《Clean Architecture》 笔记
看书名不难猜出这是 Bob 大叔的另一本书,无意间翻到这本书的时候瞬间被吸引,因为书中所引出的问题也是自己最近在思考的问题。
1. 架构到底是什么
首要的问题是,我们讨论的架构到底是什么?这个问题的答案可能相当模糊,每个人所给出的回答也不仅相同。书中指出,设计与架构没有区别,架构包含所有底层设计细节。底层细节和高层架构是不可分割的,所谓的底层和高层本身就是一系列决策组成的连续体,并没有清晰的分界线。
软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。我们可以用满足用户需求的成本来衡量架构的优劣。
两个价值维度:行为价值和架构价值。行为价值可以理解为新增代码有没有满足需求,架构价值可以理解为新增代码有没有使架构变得更好。相信大家都有这样的经历,接手一个旧项目,维护起来极度痛苦而自己也不得不按旧的架构来编写丑陋的代码。大多数的程序员忍无可忍的时候总是会希望重写整个项目,可管理者总是拒绝。在管理者看来,项目的重写没有带来任何行为价值,所以如果不是频繁的维护是得不偿失的。
2. 编程范式
接下来谈论了编程范式,在之前从来没有想过要为这个争论,来到澳洲之后与之共事的同事背景各不相同。我本身对函数式编程并没有偏见,认为函数式编程有自身适用的场景,其中的一些概念也值得学习。但是之后的几次争论让我开始怀疑,其中一次争论:我们项目中服务请求代码是这样的 request(session, url, payload);
,每次都需要显示的传递 session,session 在整个应用周期是不会变化的,消费者也无需感知。实在忍受不了到处都在取 session, 传 session, 于是我提议将其改为 request(url, payload)
request 自己获取 session。 本来以为一个显而易见的改进会被接受,可不想遭到两个开发的反对:request 不再是纯函数;对测试不友好。即使我搬出设计原则,他们也无动于衷,当然 FP 的信仰者是不会被 OOP 的原则所影响的。这件事也促使我思考,究竟在编程之中我们应该遵循哪些原则,什么样的代码才算上是好代码,我们是不是应该为测试而编程。还有千万小心小心那些固执的书呆子类型的开发,他们搬出的名词其实自己一点都没有理解。之前自己特别愤慨:把简单问题复杂化才显得出自己的能力。在实现前端权限鉴权时,照搬中间件的概念用一组中间件完成了本可以用一个简单的函数实现的任务,不敢相信这是高级开发写的代码。
之前也也多少阅读过关于编程范式的文章,包括为什么动态语言不需要设计模式。这儿 Bob 大叔也指出其实这些语言没有太多本质区别,面向对象编程语言中的特性我们都能够在结构化编程语言中找到。封装,继承,多态其实都不是面向对象编程语言才有的。根本来说: 结构化编程对程序控制权的直接转移(GOTO)进行了限制和规范。面向对象编程对程序控制权的间接转移进行了限制和规范。函数式编程对程序中的赋值进行了限制和规范。
看到这副图的时候才忽然发现控制反转的奇妙之处。
3. 设计原则
广为人知的 SOLID 原则。 SRP 原则曾经包含 任何一个模块的应该有且仅有一个被修改的原因。
这些原则也适用于结构之中。
4. 组件构建原则
这儿说的组件是软件的部署单元,可以完成独立部署的最小实体,比如 Java 的 jar 文件或者 DLL 文件。哪些类应该被组合成一个组件呢?三个基本原则:
- REP: The Reuse/Release Equivalence Principle
- CCP: The Common Closure Principle
- CRP: The Common Reuse Principle
软件复用的最小粒度应等同于其发布的最小粒度。第一个原则看起来好像不言自明。
第二个原则是共同闭包原则,我们应该将比较可能同时修改的类放到一个组件中。这个原则与 OCP 比较紧密,也可以说是 SRP 的组件版,平时的新增功能应该只涉及到很少的模块和文件。
第三个原则是共同复用原则是不要强迫一个组件的用户依赖他们不需要的东西。
组件耦合三个原则
- 无环依赖原则 组件依赖关系图中不应该出现环。
- 稳定依赖原则 依赖关系必须要指向更稳定的方向。
- 稳定抽象原则 稳定的组件应该是抽象的,不稳定的组件应该包含具体实现。
5. 软件架构
软件架构这项工作的实质就是规划如何将系统切分成组件,并安排好组件之间的排列关系,以及组件之间互相通信的方式。而设计软件架构的目的,就是为了在工作中更好地对这些组件进行研发、部署、运行以及维护。其策略就是要在设计中尽可能长时间地保留尽可能多的可选项。保留可选性就是推迟细节相关的决定。
软件架构设计本身就是一门划分边界的艺术。GUI 与业务逻辑无关,数据库与业务逻辑无关,它们之间应该有条边界线。插件式架构比较形象,GUI 和数据库都是可插拔的,它们都不会影响到业务逻辑。
层次可以用策略(业务逻辑)距离输入输出的远近来衡量。我们经常听到的原则:高层模块不应该依赖底层模块,二者都应该依赖于抽象,抽象不应该依赖于细节,细节依赖于抽象。书中举了加密程序为例:
这里面的问题就是高层组件依赖于底层的读写函数。更好的设计应该是:
在这个图里面可以看出,底层细节与高层策略就是解耦的,而且底层组件是可插拔的。
尖叫的软件架构一章里说,当你阅读的是图书馆的建筑设计图时,整个建筑设计都在尖叫着跟你说:这是一个图书馆。当我们查看应用程序的结构和源码时,它应该在喊:XX系统,而非一堆技术名词。在我们按层组织项目时,尤其常见,首先看到的就是controllers, services。良好的架构应该尽可能允许用户推迟和延后决定采用什么框架,数据库,Web 服务以及其他与环境相关的工具。这个听起来很有道理,但是在实际做项目时,往往会先考虑这些或者这些本身就是既定的。
首先是独立于框架,UI,数据库以及外部设备。外层代表的是机制,内层代表的是高层策略,依赖关系是底层机制指向高层策略。
Main 组件负责创建,协调监督其它组件的运转。Main 组件是系统的底层模块,处于整个架构的最外层,主要负责为系统加载所有必要信息,然后将控制权交回系统的高层组件。
关于服务。微服务近年来非常流行。原因可能有:服务之间是隔离的,服务被认为是支持独立开发和部署的。虽然服务化可能有助于提升系统的可扩展性和可研发性,但服务本身却并不能代表整个系统的架构设计。对微服务迷信的人,想用微服务拆分单体项目,很可能得到的只是一个分布式的单体项目。
最近就有同事讨论拆分单体应用,某个项目太大了维护起来费力,不免想用 lerna 管理多仓库的形式。我个人并不认为不应该拆分,而是认为项目本身的问题不在那儿。即使拆分了,得到的可能也是多个单体项目,即便是单体应用也可以有很好的组织和边界。仔细想一下:为什么将项目划分为几个包就能够解决问题了,前端的包与文件夹有多大的不同?查看下每个文件,每个模块的依赖,是否满足上面提到的原则,重新组织项目要比拆分来的更实用。
6. 实现细节
Web,数据库和应用程序框架都是实现细节。以 Spring 为例作为依赖注入框架还可以,但是使用 @autowired
的话业务对象就感知到了具体的框架。最近也刚好想引入一个新的框架 NestJS,我们也知道一旦引入框架很可能与框架相绑定,所以不得不仔细评估。