软件开发的上古智慧

余晟 余晟以为 今天

2018年,镇江

让你接手一套不稳定但要紧的在线系统,这套系统还有各种问题:变量命名非常随意,依赖逻辑错综复杂,层次结构乱七八糟,部署流程一塌糊涂,监控系统一片空白…… 你该怎么办?

前几年我就遇到了这种问题,我冒着频发的故障仔细观察,发现了最关键的问题:如果放着不动,这套系统的核心功能还是相对稳定的,但经常会有一些外围需求要开发,原有的依赖逻辑和层次结构不够清楚,就会“牵一发而动全身”,加上测试不完善,所以几乎每次外围功能上线更新,核心功能都会受影响,然后又要重复好几次“调试、改正、上线”的流程。 

怎么办?大家说了很多办法:开发态度更认真一些,把单元测试都补全,重构代码拆分核心功能和非核心功能,跟业务方谈暂停需求…… 这些办法都很对,但是,都需要时间才能见效,而我们最缺的就是时间。 

我提了个很“笨”的办法:把所有“共享变量”都抽到Redis中读写,消灭本地副本,然后把稳定版本程序多部署几份,就可以多启动几个实例,这些实例标记为AB两组。同时,在访问链路前部搭建代理服务用于分流请求,核心功能请求分配到A组(程序基本不更新),外围功能请求分配到B组(程序按业务需求更新)。这样看起来有点“多此一举”,AB两组都只有部分代码提供服务,而且要通过Redis共享状态,但是无论B组的程序如何更新,都不会影响到A组所承载的核心服务。 

虽然当时不少人疑惑“怎么能这样玩呢?”,但它确实有效。当天部署当天生效,在线服务迅速就稳定了,即便新开发的外围功能有问题,核心服务也不受任何影响。这样,业务人员满意了,开发人员也可以安心对系统做改造。 

后来有不少人问我:你怎么会想到这个办法呢?答案是:因为我是个老程序员,成长在面向对象的年代,SOC(关注点分离)、SRP(单一责任原则)、OCP(开放-闭合原则)这些东西对我来说,就好像刷牙洗脸一样是本能。具体到这个例子,无非就是识别关注点,隔离责任,保持核心关注点的封闭而已。 

后来我才知道,这办法有个专门的名字叫“蓝绿部署”。当然,我是个老程序员,不懂这些新鲜概念,我不太在乎。不过,如今不少程序员确实已经不认识SOC、SRP、OCP、LSP等等“古老”的玩意儿了,大家熟悉的是各种语言、类库、框架、代码托管网站。互联网开发场景千变万化,技术一日千里,而“面向对象”在不少人的脑海里,早就是弃之不用的老古董了。只有“老一辈”的老程序员,还记得那些古老的教诲,守着那些古拙的技巧。但是这些东西,总有一天会被时代淘汰吗?

实际上,这也是我初读Clean Architecture的疑惑。虽然Uncle Bob的名字对我们这些“老程序员”如雷贯耳,之前针对一般性软件开发的Clean Code和Clean Coder也确实很受欢迎,但如今写架构,还从结构化编程、面向对象编程、函数式编程写起,还花时间去解释SRP、OCP、LSP等等原则,实在难掩“古老”的感觉——拜托,它们和如今的“架构”有什么关系吗? 

不过如果你耐心读下去就会发现,还真有关系。

按照Uncle Bob的说法,所谓架构,就是“用最小的人力成本来满足构建和维护系统的需求”的设计行为。以前的面向对象系统,和如今的分布式系统,在这一点上是完全一致的。听取久远的教诲,尊重古老的智慧,如今的架构师也会从中受益的。

不信?我们就拿经典的三种编程范式来举例,看看这些“老掉牙”的玩意儿和如今的架构设计有什么关联。

结构化编程

一般理解,结构化编程是由if-else, switch-case之类的语句组织程序代码的方式,而杜绝了goto导致的混乱。但是从更深的层次上看,它也是一种设计范式,避免随意使用goto,使用if-else, switch-case之类控制语句和函数、子函数组织起来的程序代码,可以保证程序的结构是清楚的,自顶向下层层细化,消灭了杂错,杜绝了混淆。 

联系到如今的分布式系统,我们在设计的时候,真的能够做到自顶向下层层细化、结构清晰吗?有多少次,我看到的系统设计图里,根本没有“层次”的概念,各个模块没有一致的层次划分,“子系统交互的不是子系统,而是一盘散沙式的接口”,甚至接口之间随意互调、关系乱成一团麻的情况也时常出现,结果就是维护和调试的噩梦。

吹散历史的迷雾,不正是古老的goto陷阱的再现吗? 

面向对象编程

一般理解,面向对象编程是由封装、继承、多态三种特性支持的,包含类、接口等等若干概念的编程方式。但是从更深的层次上看,它也是一种设计范式。多态大概算其中最“神奇”的特性了,程序员在确定接口时做好抽象,代码就可以很灵活,遇到新情况,新写一个实现就可以无缝对接。 

联系到如今的分布式系统,我们在设计的时候,真的能够做到接口足够抽象,新模块能无缝对接吗?有多少次,我看到接口的设计非常随意,接口不是基于行为而是基于特定场景的实现,没有做适当的抽象,也没有为未来预留空间,直接导致契约僵硬死板。新增一种终端呈现形式,整个内容生产流程就要大动干戈,这样的例子并不罕见。

许多人都熟悉那个经典的例子:三角形、圆形、正方形都是形状,都必须提供draw方法,只是各自实现不同。这个道理很容易讲通,但它对如今的开发真的有什么用吗?要我说,还真有用。比如某在线教学系统,有各种类型的学习计划,也使用分布式架构。但是不管这些学习计划分布在哪里,用什么语言实现的,都必须提供“完成百分比计算”的方法,虽然具体的计算逻辑各异。照这样做了,教学情况统计就简单异常,不照这样做,就会麻烦异常。

抹去历史的尘埃,这类设计背后的逻辑,不正是“多态”的思想吗? 

函数式编程

一般理解,函数式编程是以函数为基本单元,没有变量(更准确地说是不能重复赋值)也没有副作用的编程方式。但是从更深的层次上看,它彻底隔离了可变性,变量或者状态默认就是不可变的,如果要变化,则必须经过合理设计的专门机制。所以,它也避免了死锁、状态冲突等众多麻烦。 

联系到如今的分布式系统,我们在设计的时候,真的能够彻底隔离可变性,避免状态冲突吗?有多少次,我看到状态或变量的修改接口大方暴露,被不经意(或者恶意)修改,导致奇怪的故障。Uncle Bob举了个相当有趣的例子,如果又要保证操作原子性又要能精确还原各时刻的状态,有个办法是这样的:只提供CR操作,而不提供完整的CRUD操作(就像MySQL的binlog那样)。平时只要追加操作记录即可,各时刻的状态永远通过重放之前的操作记录得出,这样就彻底避免了状态的错乱。这个办法看起来古怪,但我真的在之前的开发中用过(当然是在程序生命周期有限的场景下),而且真的从没出过错。 

坦白说,看完Uncle Bob的书,我心里好过点儿了。因为我发现,我们这些这些老程序员的知识其实没有过时,如今不少光鲜的架构,其实骨子里还是那些古老的问题。只是多亏了Uncle Bob的妙手点拨,我才能穿越时空,享受到“重新发现智慧”的愉悦。 

当然,架构设计是一门复杂的学问,要综合考虑编码、质量、部署、发布、运维、排障、升级等等各种因素,作出权衡。好消息是,Uncle Bob的这本书覆盖广泛,各方面都有涉及,认真读完全书一定会有不小的收获。唯一的问题是,你要适应这个老程序员的口味和节奏:他当然也会拿如今流行的打车系统做例子,但他更熟悉的,还是链接器、C语言、UML图等等。这些玩意儿对如今的程序员来说,确实透出一股“古老”的感觉。

不过我觉得,这都不是大问题。看得出类之间的依赖关系不合理,自然容易发现子系统之间的依赖不合理;搞得懂Unix如何巧妙定义通用的IO设备,自然容易想到对PC Web、Mobile Web、App内页面做合适抽象;认得清各线程、进程、链接库的职责,自然容易明白微服务也需要避免跨边界调用。更妙的是,掌握这种古老的视角,往往更能摆脱细节的困扰,把握问题的核心。就像老子说的那样:治大国如烹小鲜。 

噢,对了,“治大国如烹小鲜”,这也是久远的教诲,也包含着古老的智慧。 

这是我为《架构整洁之道》写的推荐序,技术大牛“左耳朵耗子”陈浩也为这本书撰写了推荐序,比我的好,值得阅读。