架构整洁之道导读(一)

2,662 阅读8分钟

我是《架构整洁之道》(Clean Architecture) 中文版的技术审校者,在审校的过程当中略有感悟,所以希望通过撰写导读的方式分享给大家。

书名的由来

《架构整洁之道》是Clean Architecture的中文译名。看似简单地延续了《代码整洁之道》(Clean Code)的翻译传统,但事实上,对于取中文名字这件事,我们还是花了不少气力的。拿到译文初稿时,编辑提供了几个备选的译名:《架构简洁之道》,《架构至洁》和《Clean Architecture》,这些名字各有各的考量,在没有了解这本书的核心思想之前,我也没有办法给出恰当的判断。所以在通读了原作和译作之后,我在ThoughtWorks咨询群里发起提案,讨论的过程很精彩,最终在骨灰级架构师新哥的建议下,结果大致趋向了整洁架构。

新哥说:“整本书在说依赖治理(管理),也就是如果降低依赖复杂度,和DDD中分离子域分层架构等想法是一致的;如同你整理你的房间,把东西分门别类放好,从这个角度,整齐比简单更合适,或者清晰也可。”

除此之外,对于《架构至洁》这个候选项,大魔头的态度是不要至洁,总感觉脏脏的。言下之意,自行体会。而读MBA的岳岳和XR(XR说他没读过MBA)从用户思维出发,认为《代码整洁之道》和《架构整洁之道》可以相互增强记忆,更容易激发用户的购买行为。

即便敲定了“整洁架构”,大家对“之道”也有不同的看法。《代码整洁之道》对应的原标题和副标题分别是Clean Code - A handbook of Agile Software Craftsmanship,而《架构整洁之道》对应的原标题和副标题分别是Clean Architecture - A Craftsman's Guide to Software Structure and Design。我们知道“道”是一种形而上的精神层面,老实讲,把Craftsman(手艺人)译做“道”是有点夸张的。

形而上是精神方面的宏观范畴,用抽象(理性)思维,形而上者道理,起于学,行于理,止于道,故有形而上者谓之道;形而下是物质方面的微观范畴,用具体(感性)思维,形而下者器物,起于教,行于法,止于术,故有形而下者谓之器。

道法术器择其一?其实凡事总有权衡,遵循前人的译法往往不会太坏。就像鲍勃大叔书中总结的稳定依赖原则,当我们依赖一种译法次数越多,它就更加稳定,这种稳定先不说能否形成品牌效应,单是SEO就能省去不少功夫,那么何乐而不为呢?

鲍勃大叔的文字平铺直叙、浅显易懂,尤其喜欢用他自己生活中的经验做例子。而且这本书是没有知识断层的,即便是初级程序员,也能在鲍勃大叔的循循善诱下,完成对软件架构认知的转变。因为他总是从最基础的知识点切入,自下而上,一步步地搭起架构的形状。

范式的实质是约束

编程范式是程序员喜闻乐见的话题,就像Vim和Emacs编辑器地位的旷日之争。它们的沉浮过往俨然就是风云诡谲的江湖。结构化编程英雄迟暮逐渐淡出程序员的视野,觊觎已久的面向对象编程(OOP)以迅雷之势称霸武林,独居一隅的函数式编程(FP)隐忍多年终于等来了一次机会。2012-2014年,江湖唱衰OOP的声音不绝于耳,FP就像一名拯救程序员于水火的侠士想要撼动这片天地。硝烟过后,眼前却不是你死我亡的惨状,而是你中有我、我中有你的大团圆结局。当Java这位OOP的保守党融汇了FP的特性lambda表达式,这场范式的冲突之争也算落下了帷幕。

程序员谈编程范式,喜欢党同伐异,作为FP的拥趸,我也不例外。可是鲍勃大叔却娓娓道来,所谓编程范式不过是约束程序的执行,告诉我们什么不能做而已。

  1. 结构化编程是对程序控制权的直接转移的规范和限制
  2. 面向对象编程是对程序控制权的间接转移的规范和限制
  3. 函数式编程是对程序赋值操作的规范和限制

Goto considered harmful

GotoConsideredHarmful

学习C语言编程的第一天,老师就告诉我们不要在程序中使用goto语句,因为goto会破坏程序的结构化。Dijkstra在论文Go To Statement Considered Harmful中证明了goto语句阻止了将大程序递归分解成更小的可证明的单元,这意味着大量使用goto语句的程序是不能被证明的。这里,不能被证明的语义是不可判定,类似说谎者悖论——“我在说谎”这句话不能被证明和证伪,所以不用goto其实是在保证小的程序单元可判定。可惜的是,Dijkstra并没有证明程序单元,这项工作被科学方法——测试取代了。在保证程序单元可判定的前提下,测试是一种可以对其可证伪的科学方法。命题“天下乌鸦一般黑”就是可以证伪的,我们不可能枚举天下所有的乌鸦,等到哪天找到了一只白乌鸦,我们就可以说这个命题是错误的,这就是证伪。Dijkstra说的“测试只能说明bug存在,而不能证明不存在。”是同样的道理。

测试可以保证,在当前已知情况下,程序单元是正确的。一旦有新的测试用例导致程序单元出错,那么我们就可以修正程序,让程序更加接近真相。这或许就是TDD(测试驱动开发)的妙处所在吧。

去除了goto语句之后,我们发现具备顺序,循环和分支判断能力的计算过程还是图灵完备的,也就是说goto的有无并不会影响计算能力。那么goto的在程序中的作用便是弊大于利的。再加上goto的滥用会导致程序结构容易混乱,不利于程序员理解,这更得尽力避免。所以结构化编程限制了对程序直接转移的控制权。

Pointer considered harmful

PointerConsideredHarmful

人人都知道面向对象编程有三大特征:封装,继承和多态。

封装是为了构造抽象屏障(Abstract Barrier),到达隐藏信息的目的。任何编程范式都不会缺少封装,因为这是人的需求,是人类简化问题认知的方式。

继承是一种函数(过程或者API)复用的方式,以前我们想在多个结构相似的数据上使用同样的函数,需要通过强制转换到函数可接收的数据类型(结构体指针)上,这必然存在风险。面向对象的世界里,我们不再需要手动强制转换,只要通过显式地表明继承关系,编程语言就能在运行时自动做到这点。

多态(polymorphism)是一种将不同的特殊行为和单个泛化记号相关联的能力,和多态概念对应的参考实现——运行哪段代码的决策叫做分派,大部分分派基于类型,也可以基于方法参数的个数及其类型,而分派的具体执行过程则仰仗函数指针。当作为单个泛化记号的函数被声明出来,它的具体实现可以多样化。通过这样的记号,事实上,我们解耦声明和实现,而这种解耦的过程恰恰是通过函数指针间接地找到目标函数完成的。所以面向对象编程限制了对程序间接转移的控制权。

Mutability considered harmful

MutabilityConsideredHarmful

Neal Ford在《函数式编程思想》(Functional Thinking)中提到面向对象编程是通过封装可变因素控制复杂性(makes code understandable),而函数式编程是通过消除可变因素控制复杂性的。函数式的一个显著的特点就是不可变性。不可变性意味着更多的内存消耗,更差的性能?其实不尽然。像Scala,Clojure这些基于JVM上的函数式编程语言大量使用了持久化结构(如:Persistent Vector,见脚注1),在不损失效率的前提下,实现了不可变的数据结构。这样的数据结构在高并发的环境下具有非常巨大的优势,尤其相对于面向对象编程中为人所诟病的临界区和竞态条件。

不可变的数据结构是无法重复赋值的,所以函数式编程限制了对程序的赋值操作。

小结

鲍勃大叔一针见血地指出,我们过去50年学到的东西主要是——什么不应该做。这等于给全书奠定了基调。可以类比,良好的架构也在传达同样的道理。

为什么从编程范式开始谈起?在审阅完整本书之后,我慢慢发现鲍勃大叔其实在传递一种设计理念:架构设计里,自顶向下的设计往往是不靠谱的。就像本书的目录,从程序的基础构件,谈到组件,最后谈到架构,这个过程非常符合系统自组织的特征。

为什么自顶向下的设计往往不靠谱?本书的第4部分“组件构建原则”会有答案,有需要,且听下回分解。