【译】再谈 SOLID 原则

译者序

原文地址:Solid Relevance - clean coder blog

原作者:Robert C Martin,即 Uncle Bob

SOLID 的五条原则在本文中分别翻译为:

  • 单一责任原则
  • 开闭原则
  • 里氏替换原则
  • 协议隔离原则
  • 依赖倒置原则

我对于原则/法则/规矩一类的东西其实比较反感,它们武断地把试错的经验和过程浓缩为几个字或字母,坏处就是带来了无数的误解。往往越是一知半解的人,越是奉之为信条,越是常挂在嘴边,仿佛特立独行。

另一方面,对原则发起反诘是另一种标新立异的做法,也同样会拥有大批拥趸。拥戴反面立场的人常常更为疯狂,他们明明是原则的反对者,却更加盲信盲从。

那么正确的做法是什么呢?

原文作者 Uncle Bob 总结了 SOLID 原则,他对它自然最为拥护;如果 SOLID 真是错的,或者已经过时了,他会是抵赖得最凶的那个人。

那么反方论点呢?像我上面说的,总结出短短几个字的法则,虽然易于传诵,但大家未必能正确领会它的意思。因此,反对者真的理解 SOLID 了吗?也许 SOLID 是有价值的呢?

正确的做法当然是思考:透过表面,思考其本质。SOLID 原则原本的含义到底是什么?套用于今天是否需要有什么变体?各个原则的成功的例子都有什么?

思考使你真正地“站在巨人的肩膀上”。

再谈 SOLID 原则

2020 年 10 月 18 日

最近我收到一封信:

好几年来,我们都在面试里考 SOLID 法则,我们希望候选人在工作经历中对这些法则有充分的了解。最近我们的一个不怎么写代码的主管对此提出了质疑,这真的合理吗?比如开闭法则,现在大家都是微服务了,直接改动微服务的代码很快、很安全。里氏替换原则早就过时了,已经过去20多年了,现在没人还总想着继承了。我觉得现在应该采用 Dan North 对 SOLID 原则的看法:写简单的代码就行。

我写下如下回信:

SOLID 原则仍然适用于今天,就跟当初 90 年代(其实更早)的时候一样。因为软件这么多年来并没有那么大的变化,自从图灵 1945 年写下电子计算机的第一行代码到现在,软件仍旧是 if 语句、while 循环、赋值语句,序列、选择、循环。

每一代人都相信世界已经和上代人那时候完全不同了。这是每一代人都有的错误想法,等下一代人过来告诉他们世界变了的时候,他们就能明白过来了。<笑>

我们一条一条地来过一遍这些原则。

SRP,单一责任原则

把会因为相同的原因而改动的东西放到一起,把会因为不同的原因而改动的东西分开。

很难想象这条原则怎么会过时。我们不会把业务逻辑和 GUI 混在一起,我们不会把 SQL 和通讯协议混在一起。会因为不同的原因而改动的代码应该要分开,这样的话,改动这一部分的代码就不会影响到另一部分;如果两个模块未来可能因为不同的原因而改动,要确保它们之间没有依赖,确保它们不会搞在一起。

微服务没有解决这个问题。如果你把不同改动原因的代码混在了一起,那你的微服务照样可能缠成一团。

Dan North 的 PPT 完全没理解这一点,我敢肯定他根本不懂这条原则(或者他就是说反话,我认识 Dan,感觉这种可能性比较大)。他说“写简单的代码”,我同意,单一责任原则正是写简单的代码的一种方式。

OCP,开闭原则

一个模块应该允许扩展(开放)、禁止修改(关闭)。

在所有这些原则之中,对这一条的反对声音让我对行业未来感到恐惧。模块当然要可以免修改地扩展啊。你能想象一个系统调用每个设备的逻辑都是写死的吗,保存到硬盘的逻辑,输出到打印机、输出到屏幕、输出到 pipe,到每一种设备都要写一套单独的逻辑,你能想象吗?

还有嘛……你觉得抽象概念要和具体概念分开吗?业务逻辑要和 GUI 脏代码、微服务通讯协议、数据库逻辑隔离吗?当然要!

Dan 的 PPT 再一次完全把这一点搞错。假如需求只要我们修改某些有错的代码,我们肯定不想为了把它改对,还不得不去碰其他的正确的代码。他说“写简单的代码”,我仍赞同,简单的代码一定是符合开闭原则的。

LSP,里氏替换原则

【译者注:此处的 Interface 一律翻译为协议,不翻译 subtype 一词以免与 subclass 混淆】

协议的使用者应当不必关心协议的具体实现。

很多人(包括我)曾经误以为里氏替换原则只和继承有关,但实际它不光如此,它讲的是所有的 subtype。一个协议的每一种实现都是它的一种 subtype,我们可以认为 subtype 在类型上隐式地等同于它(即鸭子类型,duck typing)。协议的使用者在使用时,只需要关心协议本身的含义,不用关心具体使用了哪一种 subtype。如果一种 subtype 对协议的实现不符合使用者的预期,那我们就不得不写一堆 if/switch 来收场了。

这个原则就是讲怎样做更清晰、更好的抽象的,这个理念怎么可能过时了呢?

Dan 的 PPT 在这个话题上完全正确,只不过没把这一点搞对罢了。“简单”的代码,subtype 之间的关系就应该是清晰的。

ISP,协议隔离原则

协议要尽量地小,这样用户就不用依赖一堆用不着的东西了。

现在的主流仍然是编译型语言,仍然依赖模块的修改日期来判断要不要重新编译、部署它,只要现状如此,我们就无法避免这样的问题:模块 A 依赖于模块 B,只要这种依赖是编译时而非运行时的,那么对模块 B 的改动会导致模块 A 也必须重新编译和部署。

对于 Java、C#、C++、Go、Swift 等静态类型语言,这个问题尤为严重;动态类型相对好一些,但仍不能完全幸免。Maven 和 Leiningen 就是专门用来解决这个问题的。

Dan 的 PPT 在这个话题上显然是错的。现在模块 A 调用了模块 B 中的一些方法,此外还有很多模块 A 没有调用的方法,此时如果改动了一个没调用过的方法,模块 A 仍不得不重新编译和部署的话,显然我们可以认为,模块 A 对模块 B 中那些它没有调用的方法也是有依赖的。Dan 的最后一点目前来看还算是对的,若一个类实现了两个协议,如果你能把它拆分成两个类,那么拆分会更符合单一责任原则,但这种操作经常是不可行,甚至不合理的。

DIP,依赖倒置原则

应当根据抽象的方向建立依赖关系,高层级模块不应该依赖于低层级模块。

很难想象一个违背这条原则的架构会是什么样子,没人希望高层级的业务逻辑依赖于低层级的实现细节,这应该还算显而易见吧?那些给我们挣钱的运算逻辑,不应该被 SQL 或者底层的验证、格式化问题给污染了。高层级抽象应该和低层级的细节隔离开,我们要小心地管理整个系统里的依赖,保证源码里所有的依赖,特别是跨层级的那些,总是指向更高层级的抽象,而不是更低层级的细节。

Dan 的 PPT 的每个例子都是用“只要写简单的代码”这句话结尾的,这话不错。但这么多年的经验教育我们,“简单”是需要这些原则来约束的,正是这些原则定义了“简单”,正是这些原则规范着开发者生产出更接近于“简单”的代码。

想搞出一团复杂的烂摊子的最好办法,就是告诉别人“只要简单就行”,然后别的什么也不教他。

你可能感兴趣的:(前端后端androidios)