反者道之动,过犹不及,物极必反。做人如此,做事如此,软件设计也不例外。从业的时间长了,看到过、经历过太多教条式的错误。所以特意写此文告诉大家,软件设计无其他,唯有中庸之道。
1. 理解中庸
在进入技术讨论之前,让我们先理解一下什么是中庸。中者,不偏不倚、无过不及之名。庸,平常也。
中庸的要义在于执两用中。我们知道走极端肯定不是中庸,然而正中间也不一定就是中庸。中,不是数学和物理的绝对中间、一半、百分之五十。而是说,事物皆有两面性或多面性,问题皆有两种解决办法或多种解决办法。认识复杂事物,解决烦难问题,要全面,要辩证,要灵活,不能片面化、简单化,按孟子说法是不能“执一”。实际上,强迫症式的找到那个正中间点,实际上也是一种偏执,也是一种“执一”,当然也就不是中庸了。
中庸的中是抽象的哲学概念,代表“适中”的选择。意在中正、不偏、适度、恰当、无过无不及。正如河南人说的“中(三声)”,就是好的意思。要权衡“两端”之间各种可能性,不可执于“一端”。对于所做出的选择,也不可“执一”,还要考虑到因地制宜、应时而变。
朱熹说“两端未是不中”,即有时候走极端也可能是中庸。怎么理解呢?上面说过了,中是“适中”的选择,为了应对极端情况,有时候只能采用极端的选择,当这种选择不可避免时,它也可能是适合的。
比如说测试覆盖率,有些公司片面的追求数字,最后往往是搞得鸡犬不宁。对于一般的业务系统,这个coverage确实没有那么重要。然而,对于人命关天的关键系统,比如航天控制系统,就不仅仅是100% coverage的事了,关键链路还需要冗余设计,这种对系统正确性、可靠性极端的要求,是一般系统很难想象的。对于这种关乎人生安全的系统,极端的质量要求,反而就是“适中”的了。
类似于这样的中庸选择,在软件设计中无处不在。任何的固定思维在软件设计中都很难行得通,因为在这个场景适用的原则、模式、方法,换一个场景就不一定适用了。
正如《Effective Java》作者Joshua Bloch所说:“同大多数学科一样,学习编程艺术首先要学会基本的规则,然后才能知道什么时候去打破规则(Learning the art of programming, like most other disciplines, consists of first learning the rules and then learning when to break them. )”。
2. Reuse和Repeat
Joshua说地很对,任何的原则都可以被打破。重复代码是典型的代码坏味道,它可能会造成散弹式修改的问题。消除重复代码在大部分情况下,都会让我们的系统变得更好。即使这样,我们仍然不能简单地走向Reuse这个极端。
这是因为代码重复(Repeat)也有益处,Repeat最大的好处就是解耦,因为任何的复用(Reuse)都会引入耦合。关于这一点,我想那些维护复杂的酱缸老系统的人应该深有体会。他们之所以选择用ctrl c+ctrl v的方式解决问题,就是不敢和老代码有任何的关联,因为你不知道会动到哪个铆钉,整个大厦就塌了:)copy出来再增加新特性最安全。
关于这一点,Neal Ford看地很透彻,他在《Fundamentals of Software Architecture》一书中说:“When an architect designs a system that favors reuse, they also favor coupling to achieve that reuse, either by inheritance or composition. However, if the architect's goal requires high degrees of decoupling, then they favor duplication over reuse"。
大意是说架构师永远要在Reuse高耦合和Repeat低耦合之间做一个权衡。这种权衡就是Reuse和Repeat的中庸之道。
譬如,我们常见的Context Mapping(上下文映射)的问题,就是一个典型的Reuse和Repeat的权衡问题。采用Shared Kernel,可以增加系统的复用性,但会增加耦合;采用Anti-Corruption复用性减少了,同时耦合也变少了。
再比如,前段时间中台架构造成的灾难,很大程度上就是因为过分强调对中台能力的Reuse,从而造成前台业务和中台的高度耦合,其结果是Reuse了中台,反而比烟囱式的前台效率更低。关于这部分更多内容你可以去看《程序员的底层思维》中批判思维一章的内容,也可以看我写的《业务中台的困境》这篇文章。
3. Waterfall和Agile
BDUF是Big Design Up-Front的缩写,意思是说提前做好大而全的设计。这个术语基本等同于waterfall的开发模式,因为在敏捷出现前的几十年中,软件开发都是沿袭着需求-->设计-->研发-->测试-->发布这样的瀑布流程进行的。
2001年,随着敏捷宣言(http://agilemanifesto.org/)的颁布,敏捷思想以迅雷不及掩耳之势,开始席卷整个软件行业。打着敏捷的旗号,大家都低头“冲刺(Sprint)”,期望设计能够在后期迭代中自然涌现,一时间,No Design的思潮甚嚣尘上。这股思潮一度被软件人员奉为圭臬,我也是拥泵之一,就好像一个一直被闷在CMMI酱缸里的人,突然抬起头来,很是轻松自在。
然而,事情果真有这么美好吗?期望毕竟是期望,好的软件设计从来没有因为敏捷迭代而自然涌现。相反,Agile压缩了工时,变相的压榨工程师变成了“搬砖工”,软件开发变成了特性工厂(Feature Factory)。
正如敏捷宣言的起草人Dave Thomas所说:“Big design up front is dumb, but doing no design up front is even dumber.”
John Cutler 也说 :“Good” waterfall beats abused Agile any day.
我觉得Dave和Cutler说的都没错,虽然他们是敏捷宣言的起草人,但能清晰的认识到设计的重要性。实际上,我想说BDUF和No Design是两个极端,我们需要”执两取中”,找到一条中间路线。比如说JEDUF(足够的提前设计,Just Enough Design Up Front )。
JEDUF是说我们要做设计,但是也拥抱变化,承认信息获取是一个过程,不指望一步到位。因此,我们可以在每一次迭代中都做“足够设计”(Enough Design)。实际上JEDUF就是研发过程的中庸之道,即我们既需要waterfall给我们带来设计指引,又需要敏捷给我们带来快速反馈,两个极端都不work,执两用中才是正道。
换句话说,软件开发生命周期其实不是只有Waterfall和Agile,而是类似一个连续光谱——有从瀑布式到敏捷,以及他们之间的多种可能性。例如,下面这种改良的迭代开发就是一种很不错的折中。
4. TDD的伦敦学派
TDD(Test Driven Development)测试驱动开发是Kent beck在2003年提出的概念,也是敏捷运动中非常重要的方法论。与传统的先实现功能再进行测试相反,TDD提倡测试先行。Kent说“TDD encourages simple designs and inspires confidence.“
TDD是一种延迟性决策策略,也叫最晚尽责时刻(Last Responsible Moment,LRM)。也就是说,与其在信息不足的情况下做决定,不如延迟到信息更多,或是不得不做出决策的时机再决策。这种策略的重点在于,在保持决策有效性的前提下,尽可能地推迟决策时间。
如果架构愿景不清晰,那么“最晚尽责时刻”让我们不必花费时间进行空对空的讨论,可以尽早开始实现功能,再通过重构从可工作的软件(Working Software)中提取架构。这种方式也被称作 TDD 的经典学派(Classic School)或芝加哥学派(Chicago School)。
但是好的软件设计真的可以从TDD的小步快跑中自动涌现吗?亦或有一些架构和模型已经成型,我也不能提前设计吗?
比如MVC的分层架构,COLA应用架构都已经是比较好的架构实践了,我还需要通过TDD让他一遍一遍的“涌现”吗?当然不需要,于是便有了提倡足够设计(Enough Design)的TDD 伦敦学派(London School)。
如果说我们的软件实现的起点是设计,那么终点就是测试。不管是从左向右走,还是从右向左走,目的都是为了产出能满足业务需求的优质软件。我们已经知道BDUP有问题,但经典TDD也有Agile的毛病,我认为合适的做法还是中庸之道,TDD伦敦学派就是对这种极端的纠偏,伦敦学派认为设计和测试驱动都很重要,只有同时从两端向中间逼近,才有可能获得成功。
5. Domain里放什么
初识DDD的同学,最大的困惑不外乎是“我到底要把什么东西放到Domain里面?”并由此引发出一系列的设计模式:
-
失血模型:模型只是数据接口,没有任何的方法(能力)。
贫血模型:模型包含了一些原子能力。
充血模型:模型包含了除了持久化之外的所有能力。
胀血模型:模型无所不包。
很明显,失血和胀血是两个极端,一点领域能力没有,那么要这个领域层干什么呢?无所不包,那和把这些能力都放在应用层有什么区别呢?解决方案我想你应该已经知道了,那就是中庸之道,从两端往中间走。怎么走的细节我就不在这里详细说了,有兴趣的可以进一步阅读《跨越DDD从理论到落地的鸿沟》。
6. 单体和微服务
单体太大,是一个极端;微服务太微,是另一个极端。怎么办?当然还是中庸了。这里的中庸,不是简单的折中,而是要对业务维度、质量维度、团队维度进行仔细的考量和权衡,最后做出一个相对合理的决定。关于微服务到底要怎么划分的问题,可以看我最近写的文章《微服务到底要多微》,这里主要是想强调中庸在服务划分中所起到的微妙的权衡作用。
7. 总结
中庸是一个大智慧,做到不容易,所以孔子才会说:“中庸其至矣乎!民鲜能久矣!”(中庸大概是最高的德行了吧!大家缺乏它已经很久了!)。要时刻做到中庸那就更困难了,正如《中庸》有言:“君子之中庸也,君子而时中。”只有君子才能“时中”(时时刻刻做到中庸)。
中西文化很多都是相通的,比如,亚里士多德 的“黄金分割点”也体现了中庸的思想。他说,勇敢是过度鲁莽和过度怯弱之间的“黄金分割点”,幽默是过于严肃和过于滑稽之间的“黄金分割点”。
软件设计之所以很难,就在于很少有固定的范式,每一个项目我们都要小心权衡,期望找到那个“黄金分割点”,至于那个点到底在哪里?对不起,我只能告诉你,它在两点之间,请执两用中!