意识到结构的复杂性后,我们的第一反应可能就是“降低组件数量”,毕竟组件数量越少,系统结构越简。最简单的结构当然就是整个系统只有一个组件,即系统本身,所有的功能和逻辑都在这一个组件中实现。
不幸的是,这样做是行不通的,原因在于除了结构的复杂性,还有逻辑的复杂性,即如果某个组件的逻辑太复杂,一样会带来各种问题。
逻辑复杂的组件,一个典型特征就是单个组件承担了太多的功能。以电商业务为例,常见的功能有:商品管理、商品搜索、商品展示、订单管理、用户管理、支付、发货、客服……把这些功能全部在一个组件中实现,就是典型的逻辑复杂性。
逻辑复杂几乎会导致软件工程的每个环节都有问题,假设现在淘宝将这些功能全部在单一的组件中实现,可以想象一下这个恐怖的场景:
系统会很庞大,可能是上百万、上千万的代码规模,“clone”一次代码要 30 分钟。
几十、上百人维护这一套代码,某个“菜鸟”不小心改了一行代码,导致整站崩溃。
需求像雪片般飞来,为了应对,开几十个代码分支,然后各种分支合并、各种分支覆盖。
产品、研发、测试、项目管理不停地开会讨论版本计划,协调资源,解决冲突。
版本太多,每天都要上线几十个版本,系统每隔 1 个小时重启一次。
线上运行出现故障,几十个人扑上去定位和处理,一间小黑屋都装不下所有人,整个办公区闹翻天。
……
不用多说,肯定谁都无法忍受这样的场景。
但是,为什么复杂的电路就意味更强大的功能,而复杂的架构却有很多问题呢?根本原因在于电路一旦设计好后进入生产,就不会再变,复杂性只是在设计时带来影响;而一个软件系统在投入使用后,后续还有源源不断的需求要实现,因此要不断地修改系统,复杂性在整个系统生命周期中都有很大影响。
功能复杂的组件,另外一个典型特征就是采用了复杂的算法。复杂算法导致的问题主要是难以理解,进而导致难以实现、难以修改,并且出了问题难以快速解决。
以 ZooKeeper 为例,ZooKeeper 本身的功能主要就是选举,为了实现分布式下的选举,采用了 ZAB 协议,所以 ZooKeeper 功能虽然相对简单,但系统实现却比较复杂。相比之下,etcd 就要简单一些,因为 etcd 采用的是 Raft 算法,相比 ZAB 协议,Raft 算法更加容易理解,更加容易实现。
综合前面的分析,我们可以看到,无论是结构的复杂性,还是逻辑的复杂性,都会存在各种问题,所以架构设计时如果简单的方案和复杂的方案都可以满足需求,最好选择简单的方案。《UNIX 编程艺术》总结的 KISS(Keep It Simple, Stupid!)原则一样适应于架构设计。
演化原则演化原则宣言:“演化优于一步到位”。
软件架构从字面意思理解和建筑结构非常类似,事实上“架构”这个词就是建筑领域的专业名词,维基百科对“软件架构”的定义中有一段话描述了这种相似性:
从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。
例如,软件架构描述的是一个软件系统的结构,包括各个模块,以及这些模块的关系;建筑架构描述的是一幢建筑的结构,包括各个部件,以及这些部件如何有机地组成成一幢完美的建筑。
然而,字面意思上的相似性却掩盖了一个本质上的差异:建筑一旦完成(甚至一旦开建)就不可再变,而软件却需要根据业务的发展不断地变化!
古埃及的吉萨大金字塔,4000 多年前完成的,到现在还是当初的架构。
中国的明长城,600 多年前完成的,现在保存下来的长城还是当年的结构。
美国白宫,1800 年建成,200 年来进行了几次扩展,但整体结构并无变化,只是在旁边的空地扩建或者改造内部的布局。
对于建筑来说,永恒是主题;而对于软件来说,变化才是主题。软件架构需要根据业务的发展而不断变化。设计 Windows 和 Android 的人都是顶尖的天才,即便如此,他们也不可能在 1985 年设计出 Windows 8,不可能在 2009 年设计出 Android 6.0。
如果没有把握“软件架构需要根据业务发展不断变化”这个本质,在做架构设计的时候就很容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。