summaryrefslogtreecommitdiff
path: root/content
diff options
context:
space:
mode:
Diffstat (limited to 'content')
-rw-r--r--content/library/2026/clean-architecture.md76
1 files changed, 72 insertions, 4 deletions
diff --git a/content/library/2026/clean-architecture.md b/content/library/2026/clean-architecture.md
index 0955af39..4dd8062a 100644
--- a/content/library/2026/clean-architecture.md
+++ b/content/library/2026/clean-architecture.md
@@ -9,7 +9,6 @@ cover = 'https://img.eltr.ac/geedeapro/library/2026/clean-architecture/cover.png
draft = true
toc = true
toot = ''
-endnotes = ['本文第一段里,我说「很不喜欢软件架构课老师的授课风格」,其实我真正想说的是「教我软件架构的老师是个神经病」,但我想这种话放在文章开头总归不太合适。']
+++
由于很不喜欢软件架构课老师的授课风格,所以决定完全自学。选这本书当教材可能不太合适,但的确收获颇多。眼看 Coding Agents 的趋势只增不减,就算在个人项目和自由软件项目中拒绝使用,工作时也无处可躲,我想每个程序员都需要做架构师了。如果缺乏整洁清晰的架构设计,那 Coding Agents 就只是在高速产出屎山代码而已,后续的维护只会是一场接一场燃烧 Token(或者说燃烧美元)的灾难。总之有必要关注架构问题。<!--more-->
@@ -32,7 +31,7 @@ endnotes = ['本文第一段里,我说「很不喜欢软件架构课老师的æ
>
> A service might be a single component, completely surrounded by an architectural boundary. Alternatively, a service might be composed of several components separated by architectural boundaries. In rare2 cases, clients and services may be so coupled as to have no architectural significance whatever.
-然而在译本中,这一节有四段话。在原本应该结束的地方,译者毫无预警、肆无忌惮地把自己想写的东西紧跟着写在了译文的后面,看起来就像那些文字本来就在那个地方一样:
+然而在译本中,这一节有五段话。在原本应该结束的地方,译者毫无预警、肆无忌惮地把自己想写的东西紧跟着写在了译文的后面,看起来就像那些文字本来就在那个地方一样:
> Bob 大叔的案例非常精彩,确实,……*(此处省略三四段极其平庸的论述)*
@@ -40,7 +39,7 @@ endnotes = ['本文第一段里,我说「很不喜欢软件架构课老师的æ
倘若译者真的觉得自己有什么值得一读的见解,完全可以写在译者序或者后记里(*这样方便我直接跳过*),或者自己写一本书,而不是在别人的书里「借楼打扰」。
-我读的译本是机械工业出版社出版的,我找到了由电子工业出版社出版,由孙宇聪翻译的译本,后者的第 27 章的小节只有原文出现过的两段,没有多余的内容。孙译本也没有翻译插图,文字段落中使用的名称也是英文原文,术语是统一的。此外,机械工业出版社的译本中其他的很多大大小小的问题,在孙译本里都没有出现,比如第 34 章《The Missing Chapter》被我手上的译本译作「细节决定成败」,我真的哭笑不得,孙译本的翻译是「拾遗」,不一定是最好的,但至少和原文不至于毫无关联。
+我读的译本是机械工业出版社出版的,我找到了由电子工业出版社出版,由孙宇聪翻译的译本,后者的第 27 章小结只有原文出现过的两段,没有多余的内容。孙译本也没有翻译插图,文字段落中使用的名称也是英文原文,术语是统一的。此外,机械工业出版社的译本中其他的很多大大小小的问题,在孙译本里都没有出现,比如第 34 章《The Missing Chapter》被我手上的译本译作「细节决定成败」,我真的哭笑不得,孙译本的翻译是「拾遗」,不一定是最好的,但至少和原文不至于毫无关联。
总之,如果你想读这本书,我推荐直接读《Clean Architecture》英文原本;如果要读译本,请选择电子工业出版社的孙宇聪的译本,绝对不要读机械工业出版社这四个译者译出来的垃圾译本。
@@ -48,9 +47,78 @@ endnotes = ['本文第一段里,我说「很不喜欢软件架构课老师的æ
---
-## 架构分为策略和实现细节
+## 架构是永恒的
+
+> 当你仔细观察编程实践时,你会意识到在过去 50 年里几乎没有什么变化。编程语言变得更好了,工具也变得更加出色了,但计算机程序的基本构建块并没有变化。
+
+编程语言在更新迭代,编程范式和开发模式也在推陈出新,但组织软件系统的方式、定义组件边界的方式、软件设计的基本原则、管理依赖关系的方式,几乎都没有改变。架构是永恒的,此外,良好的架构也能让延长软件的生命周期。
+
+Bob 大叔认为软件有两种价值维度:行为价值和架构价值。前者是大多数人更关心的「软件能做什么」,非开发人员希望软件能更快地推出新功能,也就是允许更多的行为。然而,架构价值可能比行为价值更重要,因为架构决定了开发开放、维护难度和变更成本。如果急着满足需求,实现行为价值,而使用了不合理的架构,那么后续增加新功能时要修改的代码就会越来越多,还有可能出现修改了某一段代码导致另一段代码不可用,需要继续修改的情况。我想这有点像一些人说的「慢就是快」,一开始求快只会在整体上拖慢速度,甚至最终不得不重头来过。
+
+书本还用艾森豪威尔矩阵解释了两种价值维度的关系,这个矩阵就是生产力爱好者常说的「重要且紧急」「重要不紧急」「不重要但紧急」和「不重要不紧急」的划分。架构属于「重要不紧急」的范畴,而软件行为是普通人更关注的「不重要但紧急」的事项。至于另外两个象限是什么:紧急的事情永远不重要,重要的事情永远不紧急。
+
+架构往往和变更成本联系在一起,如果一开始使用了不好的架构,那么变更就会变得很难,添加新代码会很难,重构的成本也很高。如果一开始使用了良好的架构,那么整体而言软件开发的速度会更快,变更的成本也更低。好的架构往往是灵活的架构,而坏的架构很难改变,在这个意义上,架构也是永恒的。
+
+## 编程范式、设计原则和组件原则
+
+本书的第 3、4、5、6 章讲的是编程范式,主要有三种:结构化编程、面向对象编程和函数式编程。前两种大多数程序员应该都很熟悉,而函数式编程通常是 Lisp 实现的编程范式。三种范式并没有给编程增加什么新东西(可能有一些新概念,但实际上没有范式也能做到这些概念做的事情),相反,他们限制了某些东西。
+
+1. 结构化编程限制了对程序控制权的直接转移,也就是 `goto` 语句的使用,认为程序由顺序、分叉和循环几种基本结构组成,这样就能把程序分成可证伪、可测试的基本单元;在以前,`goto` 语句有害还不是共识,那个时候人们对 `goto` 的反对者 Dijkstra 冷嘲热讽。
+2. 面向对象编程限制了对程序控制权的间接转移,主要是限制函数指针的使用,因为指针很危险;所谓的封装、继承和多态,在 Bob 大叔眼里都不是 OOP 的新东西,这些都可以通过操作函数指针实现;OOP 的另一个好处是可以很方便地实现依赖反转,让其他组件成为业务规则的插件,而不让核心的业务规则知道其他技术细节的存在。
+3. 函数式编程限制了赋值操作,变量通常是不可变的(不过我印象里像 Fennel 这样的 Lisp 方言并不是纯血函数式编程,可以正常声明和操作变量,只是有括号的 Lua 语言而已);函数式编程的一个思想是,如果有足够的空间和储存能力,就可以用不可变、纯函数的方式实现程序,只存储事件记录而不储存状态,Git 就是这样的。
+
+---
+
+第 7 到 11 章讲的是 SOLID 设计原则,即:
+
+1. Single Responsiblity Principle(单一职责原则)
+2. Open Close Principle(开闭原则)
+3. Liskov Substitution Principle(里氏替换原则)
+4. Interface Segregation Principle(接口隔离原则)
+5. Dependency Inversion Principle(依赖反转原则)
+
+所谓单一职责,并不是说一个模块只做一件事情,因为「一件事情」其实很难定义。准确的定义是:一个模块只对一个行为主体负责,或者说,一个模块只因为一个主体发生变更。具体来讲,要分离不同主体相关的代码,不同的主体不应该依赖同一个模块,这样一个主体的需求变更而引发的代码更改就不会影响到另一个主体。
+
+所谓的开闭原则,是指软件应当对拓展开放,对修改关闭。所有组件的依赖关系都应该是单向的,最终指向不会经常更改的组件。这个没有任何依赖的组件往往是核心业务逻辑,与任何技术细节都无关,而数据库和用户界面等技术细节应该在外围,去依赖业务逻辑。如此一来,用户界面就作为「插件」存在,是对业务逻辑的拓展,可以随时拓展更多的其他用户界面,比如分出桌面端应用和 Web UI,这都不会导致核心业务逻辑的修改。
+
+所谓的里氏替换原则,主要应用在 OOP 编程,用来指导「继承」的使用。比如,Square(正方形)不能作为 Rectangle(矩形)的子类,因为不符合 LSP。这两个类若是相互替换,软件的行为就会发生改变,参考一下代码:
+
+```java
+Rectangle r = ...
+r.setW(5);
+r.setH(2);
+assert(r.area() == 10);
+```
+
+如果 r 是 Square 类型,因为是 Rectangle 的子类,所以能通过类型检测,但最终 assert 会失败。
+
+所谓的接口隔离原则,是为了避免依赖不需要的东西。假设 `User1` `User2` `User3` 分别依赖 `OPS` 对象里的 `op1()` `op2()` `op3()` 方法,更好的做法是创建 `U1Ops` `U2Ops` `U3Ops` 这三个接口,由 `OPS` 实现这三个接口。三个 `User` 去依赖各自的接口,避免直接依赖 `OPS`,因为它提供了许多自身不需要的东西。如果不这么做,`OPS` 的更改就会导致三个 `User` 都需要重新部署,若不直接依赖 `OPS`,就不会有这个问题。
+
+最后是依赖反转原则,这是说源代码应该多依赖抽象类型而不是具体实现,这有点像前面的开闭原则和接口隔离原则,代码的依赖关系应该最终指向最抽象、最不容易发生变更的核心业务逻辑,同时避免依赖自身不需要的东西。这能避免不必要的代码变动,以及因为所依赖的组件发生变更而不得不重新编译和部署多个组件的繁琐工作。
+
+常见于 Java 等 OOP 语言里的工厂设计模式也是为了依赖反转而服务的,也有了先写 `XXXService.java` 接口,再写 `XXXServiceImpl.java` 实现类的做法,这样可以使源代码依赖于抽象工厂和抽象的服务接口,而不直接依赖易变的具体实现;而 `ServiceImpl` 和 `ServiceFactoryImpl` 等类由于是对接口的实现,依赖关系也是从具体实现指向抽象接口的,保证依赖关系从具体指向抽象。
+
+以前我总觉得这种编程模式很繁琐,如今也算是理解了一些背后的原因。这么说来,我的 Java 老师还真是一点没讲清楚。我想这样的 Java 开发者应该不占少数,这才是 Java 这门语言令我厌烦的根本原因吧。无脑复用某种软件组织方式,完全不明白这么做的目的,最终违反了某些原则也完全不自知,最终的软件架构照样很乱。
+
+---
+
+第 12、13 和 14 章讲的是组件原则,包括组件的历史、重定位和链接器等技术,以及组件内聚和组件耦合相关的原则,不只是「高内聚低耦合」那么简单。
+
+组件内聚主要要保证复用/发布等价原则(REP)、共同闭合原则(CCP)和共同复用原则(CRP)。REP 如今已经成为事实标准了,它说的是「没有通过发布流程获得版本号的组件是无法复用的」,主流编程语言几乎都有发布组件的标准流程,锁定版本号以保证组件行为可预测也很重要。CCP 是单一职责原则的组件版本,即「将相同变更原因而需同时更改的东西放在一起,将不同变更原因或无需同时更改的东西分开」。CRP 简单来说就是「不要依赖不需要的东西」,不这么做的后果,JavaScript 开发者深有体会,他们的大部分工作都是在更新、升级和维护依赖,因为;如果依赖某个组件,最好是依赖该组件的每一个类。
+
+组件内聚的三个原则构成了一个「不可能三角」,需要架构师权衡取舍,一般来说只能同时实现两个。如果牺牲 REP,就要面临组件难以复用的后果;如果牺牲 CCP,就可能会出现太多不必要的版本发布;如果牺牲 CRP,就会出现太多的组件变更。
+
+组件耦合的三个原则是无依赖环原则(ADP)、稳定依赖原则(SDP)和稳定抽象原则(SAP)。ADP 就是不应该出现循环依赖,这个很好理解。SDP 提出了一个稳定性指标,依赖的组件越少、被越多组件依赖的组件越稳定,否则越不稳定,而组建的稳定性指标应该沿着依赖关系递减,即依赖关系最终指向最稳定的组件(往往是核心业务逻辑)。SAP 是说,稳定的组件应该是抽象的,有多稳定就有多抽象。
+
+稳定但具体的组件维护起来是痛苦的,这部分应该是数据库 Scheme 和 `string` 之类的少数易变组件;非常抽象但不稳定的一般是那些没有被实现的抽象类,是无用的;而中间的那部分、稳定且高度抽象,或者不稳定但十分具体的组件,应该占据大多数。
+
+---
+
+以上各种原则和概念名称都是深入理解软件架构的基础,我个人感觉 Bob 大叔非常强调依赖反转原则,它的确也串联起了很多其他的设计原则和组件原则。如果上面的内容太多太杂,只要记住核心业务逻辑应当不依赖任何组件,程序的其他部分应当作为业务逻辑的插件存在,去依赖业务逻辑而不影响它。
## 尽可能推迟细节决策
+软件分为策略和实现细节,
+
## 依赖关系和可维护性