以终为始

144
作者 _袁英杰_
2016.07.03 15:36* 字数 4105

2000年底,当我开始设计短信网关时,由于是第一次主导设计这么重要的7X24高可靠性电信系统,并且工期又极其紧张,而可用的几个人又都经验普遍不足。

怎样基于这些令人悲观的前提构造一个高可靠系统,我们当时想到的设计决策是:进程监控树

整个系统分成了多个单一职责的子系统,各自运行在自己的进程内。而在这些子系统上面,均存在一个只是为了监控子系统进程状态的父进程,两者之间通过pipe关联。

一则父子进程之间可以通过pipe来发送一些控制信息;二则,也是最重要的初衷:一旦子进程崩溃,监控进程可以马上发现这一点,并立即对相关子系统进行恢复性处理。

父进程的可靠性,一方面靠它自身的简单,简单到几乎不可能出错;其二,则是在其之上再建立一级监控。最顶层的进程则由在系统的crontab中配置的周期性检查来确保其安全性。

而子系统之间,对于性能要求苛刻的IPC通信,则依靠共享内存;如果性能不那么重要,则可以通过异步消息;跨主机的通信则要么通过自定义的消息机制,要么干脆使用同步的RPC;而话单生成与采集,则完全以文件作为媒介。

那时候是2000-2001年,互联网刚刚兴起。可以从网上获取的信息和资料远不如今天这么丰富。所有这些设计技术,基本上全部来自两卷《Unix网络编程》。然后我们从项目的实际出发,选择和组合其中不同的具体技术来解决我们自己的问题。

2007年下半年,我加入了ThoughtWorks。某天熊节推荐给我一本书:Joe Armstrong《Programming Erlang》

一个下午,我就把整本书全部读完——真的认真读完。这让大熊感到惊讶。

之所以熊会觉得这个速度不可思议,是因为他不了解:那本书中讲到的Erlang的绝大部分机制,尤其是后来被互联网社区视作神物的actor model,正是我们之前在做短信网关时的自然选择。

而在当时并没有人告诉我们应该那么做,也没有人告诉过我们那就是actor model(惭愧的是,我到2015才第一次听说这个词汇)。正是因为所有的这些机制(除了透明的分布式机制),我们都根据我们的问题亲自实现过(当然我们没有虚拟机提供的轻量级进程:我们当时只能使用基于消息通信的多线程)。

更何况,我在Motorola四年的嵌入式开发生涯,亦让我对实时系统的模块划分和基于异步消息的模块间通信认识深刻。

这难道不应该是一个由问题出发,得到的最最自然的选择么?有什么难理解的?

至于ErlangFP的部分:模式匹配递归List, Tuple,每一件事都看起来简单而自然;不变量亦容易理解,只是我一直很担心的是它带来的性能问题。

但我当时依然感到兴奋:当初我们亲自设计和实现的那些机制,如今被一个平台和语言内建了。这对于整个社区开发这类系统是件极好的事。

类似的经历,也发生在2007年我第一次看到Android时。当时我去拜访我的Motorola的前同事,他给我展示了他一个人花了两天时间就成功的把Android移植到我们自己的手机硬件上,从而得到的一款功能强大的手机,而之上的所有应用均可共享。

看到这种情况,作为一个过去四年一直在开发手机各种特性的工程师,我感到很兴奋,马上给出了论断:Android将会一统天下。原因和Erlang平台一样,在于复用

于是,那段时间我花了很多业余时间来研究Erlang,随着越来越深入的理解,我开始对它有所顾虑,主要是关于语言的部分的:它的语言构造太原始,在编写复杂系统时,对于代码的长期可维护性不够友好。但我对它的平台和OTP,尤其是它的轻量级进程和分布式机制一直情有独钟。

直到我去印度参加ThoughtWorks Immersion,我的老师见到我在写Erlang代码,然后就给我推荐了Haskell。而Haskell语法的优美,强类型的特征,以及背后的完备理论,一下子就把我吸引了。从此以后,我就把更多的业余时间投在了对Haskell的学习上。

而这些知识,尽管没有在实际工作中直接使用,却潜移默化的影响了我,我们的C++项目之所以可以做到易于变化,易于演进,处处充满着组合式的思维,Haskell功不可没。

但我并不会因此推荐Haskell作为我所从事的电信业务领域的开发语言。因为它的性能完全不能满足我们的要求。更何况,它的IO部分处理的麻烦程度,要远高于命令式语言(关于Monad Transformer,让人用起来烦到死)。而它强制不变性,并非在所有时候都是好事。

回到我的具体问题,吸收其好的思想,帮助我更好的使用其它语言,就足够了。

这些年,随着多核、分布式、弹性计算时代的到来,FP再度得到越来越广泛的关注。

但伴随着这个过程,却出现了一些另外一种苗头:FP社区发出了扭曲现实的误导之声。比如:只有我们是正确的,你们都错了OO Sucks等等。而给出的案例和数据,都是经过精心选择,或干脆是具有误导性的不公平比较。

这些行为,有些属于无意识的(因为没有经过真正认真的思考),另外一些则是有意识的。

一个典型的例子,是下面这幅被很多人引用,但却充满刻意误导的图:


OO vs. FP

这幅图的最大问题在于,它将两个不在同一层次的概念放在比较:右边是Function,左边却是OO里具体的原则与模式,难道与Function对应的不应该是Object吗?

如果你了解FP,就会知道,对于Function的应用,也有Patterns,因而同样也可以给出一副类似的图,来说明OO优于FP:左边全是Objects, Yes, objects, Oh my, objects again,右边则罗列FP的各种Patterns

而回到问题本身,FP难道就不需要单一职责依赖倒置?不需要开放封闭接口隔离?如果连这几个原则都不能理解和遵守,function自身就能让你写出易于应对变化的软件?

事实上,对于这些原则的遵守,正是FP函数组合背后的关键哲学。而组合模式装饰模式策略模式等代表的组合思想,更是FP编程的精髓。

更何况,由于FP数据与算法的强制分离,有时候反而会损害内聚性(单一职责)。

因而,随着这些声音越来越响亮,我的反感也越来越强烈。任何工具,放在其特定的场景下,都有其特长;而放在另外一种场景下,则会露其短。根据我对OOFP的认知,我尚未发现谁可以全局通吃。

如果非要让我给个结论,那会是:我研究FP越多,反而会越坚定的认为OO要比FP更适合解决复杂问题,也适用的范围更广,并且越是纯粹的FP语言问题越多。并且FP的组合思想,可以被完整的应用于OO,并且由于没有FP那么多的约束,只会表现的更强大(虽然有时候也确实更啰嗦)。但是,在很多局部问题上,FP确实更胜一筹。

因而,当OO解决这个问题引入的代价和它产生的好处相比,并不比FP更优时,毫无疑问选择FP。但在更多场境,则应该毫不犹豫的选择OO

这就是我始终坚持的决策因素:投入产出比

类似的事情,也发生在微服务身上。

当初我第一次听到微服务时,我的第一反应是:难道不是理应如此吗?高内聚,低耦合单一职责组合式设计,这难道不是我们一直都是这么倡导和实践的么?

直到我的客户开始极致的追求:为了小而小。完全不考虑微服务究竟要解决的是什么问题,带来的又是什么问题。而社区内又一片热捧,几乎每个人都在谈它的好处,却鲜有人谈它带来的问题。

当我作为设计师,被不懂设计原理、把微服务当银弹的客户,强行要求把本属于高内聚的问题拆得更小时(最好做到几十行C++代码作为一个服务),我开始对此感到恼火。

对于大问题的分而治之,从来都不是一个新想法。而怎么拆才是合理的,对于大多数团队来讲都一直是个未解之谜。高内聚,低耦合,这么言简意赅的核心原则,真正理解它的设计师都不多,更不要说普通程序员。作为一个技术咨询师,这么多年的所见所闻,让我这个看法越来越坚定。

如果你的团队,连面向对象这种最基本的进程内模块化方式都做不好,你怎么可以认为,你采用了微服务—这无非是进程间模块化,你就能做好?

如果你对一个进程内模块的API该怎么定义,都做的一塌糊涂,进程间API你就能定义的更合理?

更何况,你服务划分的不合理,或者服务间API制定的不合理,由于康威定律的作用,只会让你修正起来麻烦的多。

所以,当我读到老马同志给出的那些忠告时,我从内心给他狂点了1024个赞。

但在狂热之下,又有几个人在认真聆听那怕是老马的声音?

在这个过程中,还出现了重构已死的说法。

大熊的这篇文章虽有标题党嫌疑,但其文章内的一些观点我还是非常赞同的。

首先是抛弃式编程。因为这正是我们一直在做的,一些类,今天创建,明天可能就被完整的抛弃了。如果你的类都是单一职责的的,这样的情况再正常不过了。在应对变化时,很多时候,我们不是在到处修改代码,而是直接抛弃掉一些不再合乎需要的类,创建一些新的类。可抛弃性其实是对正交性的最好检验。近几年和我一起做过项目的,会对此印象深刻。


可抛弃的类

而一旦微服务化,对于服务的抛弃,有时候和直接抛弃一个类并没有什么本质的不同,无非一个是进程内模块,一个是进程间模块。


可抛弃的服务

大熊文中的另外一个观点是:一个微服务类内部的代码写的烂点没关系。因为都被控制在黑箱里了。


服务内部影响局部化

这种观点,其实在整体架构下同样有效:毕竟,类的封装就是在封装实现细节的,客户真正耦合的是一个类的API,而不是实现细节。那些代码写的糟糕些,影响都是局部的。


Class内部影响局部化

这也正是为何,在简单设计原则下:Clean Code所代表的可理解性,其重要程度应该低于局部化影响,即易于变化性的原因。

而重构的核心价值在于:在演进过程中,发现系统的结构性不合理,从而重新划分和组合模块。而信仰演进式设计的我们,知道对于很多需求变化频繁的系统,想从一开始就在非常微观的力度看清楚相关变化,是不太现实的。在么在演进的过程中,谁的重构会更加困难?

另外熊没有提到的是:有时候一个服务内部糟糕的内部质量,往往可能会掩盖一些事情的本质。而Clean Code的价值,有时候就是让本质显现。从而发现更深层次的结构性问题(即模块划分不合理的问题)。

当然大熊将重构限定在了单一代码库上。如果单纯站在这个角度,熊的论断是总体是正确的。

可是,站在设计师的角度,他面对的是整个问题域。将一个大问题域做成整体架构,或是适度的服务划分,还是极度的微服务化,在设计师眼里只是一个架构选择问题。

正如之前所述,重构的核心价值在于:在演进过程中进行的结构性重构。因而,重构这种行为,也必须站在更大的视图上来考虑其价值和成本。

在这个前提下,我们只需要问一个问题:微服务架构,有没有降低问题本身的本质复杂度?事实上,微服务架构是通过增加系统的复杂性来换取分布式时代带来的其它好处。但这并不会影响重构的价值,因为作为设计师,你不会因为架构风格的选择不同,而降低对总体设计合理性的要求。

因而,无论一个系统是整体架构,还是微服务架构,都没有改变事情本身,甚至,微服务架构只会让重构变得更麻烦。

如果重构现在不重要,那么它过去从来就没有重要过。如果今天重构已死,那它从来就没有存活过。

以终为始。

软件设计