当今软件生态已经越来越复杂了,一个简单的产品,
可能动辄涉及到十个,甚至数十个子系统,
把所有这些子系统当做一个整体来看的话,写好它是一件很难的事情。
本文就来探讨一下,一个勉强能跑的系统,跟一个生态优良的系统,
到底有什么区别,以及需要额外付出多少努力。
异常
一个勉强能跑的系统,不会覆盖所有的异常流程,
每个函数只有一个返回值,它们被看做永远不会抛异常。
然而,这肯定是一种不合理的假设,
当有一个函数可能会抛出异常时,事情就变得复杂了。
这个函数的调用方到底该怎样“消化”这个异常呢?
除此之外,甚至在业务层面,也会包含一些“非正常”的业务逻辑,
比如一个没有注册的用户访问授权页面,应该怎么处理?
或者是,一个接口被传入了跟预期不符的参数时,应该怎样反应?
把所有功能看做一个整体,那么正常流程应该只有一种,
出错的可能,会有很多种。
如果每一种出错,都经过了深思熟虑的处理,而不是默认结果,
那么,加上异常流程的系统,会比勉强能跑的系统,复杂数倍。
一个可靠的接口,会由一个正常结果,与 N 个异常组成。
日志
对一个没有日志的系统排查问题,是非常困难的,
很难还原出当时的运行现场。
一个常见的方法是在业务逻辑的字里行间写下日志,
这种侵入性的日志编写方法,会降低代码的可读性,
让逻辑变得晦涩难懂。
而非侵入的日志编写方法,可能无法覆盖到所有细节,
一般是通过异常切面来做。
不论用哪种方法,增加了日志功能的系统,会比勉强能跑的系统复杂了一些。
这是毋庸置疑的。
因为日志功能已经算是原系统的一种非功能需求了。
单测
专业程序员喜欢用日志和单测来检查问题,遇到问题跑一下单测,
能省掉很多构造数据的成本。
然而,有单测覆盖的系统,也一定会增加不少代码量,
对于那些功能频繁变动的系统,甚至每改一些代码,就得相应的改改单测。
单测虽然一定程度上增加了代码的可靠性,
但同时,也增加了代码的编写成本。
除此之外,软件的每一次持续集成都会跑一遍单测,
花在修补单测上面的时间,可能会出乎意料的多。
因此,编写成本和软件质量是不可兼得的。
没有一劳永逸的办法。
有单测的系统,在维护者看来,确实更加复杂,
单测可以看做是给代码加了“有测试覆盖”这条额外的属性。
环境
有些系统是要分本地环境和线上环境的,
本地开发时,跟线上运行时,系统的表现可能会不同。
简而言之,某些行为可能会跑到不同的分支里面。
最起码,他们也可能会从不同数据库中读取信息。
这样做也会增加系统的复杂性。
要发布一个系统,我们必须保证它在好几个环境都表现良好,
而不是仅仅线上表现良好就够了。
一旦有多个子系统,都区分了不同环境,问题就更麻烦了,
在调用某个接口的时候,我们必须严格区分调用的是哪个环境中的接口。
这比只有线上一套环境的系统,又复杂了好多。
调试
有些系统的设计之初,只是为了满足功能,
可一旦跟其他系统打交道时,就会涉及调试和排查问题。
一个不能调试的系统,在排错时是特别麻烦的,
我们只能一行行的读代码,在大脑中记录程序状态,人肉调试。
这种情况在遇到多态函数时,会变得超级烧脑。
很多运行时数据的丢失,导致我们不知道程序到底该执行哪段代码。
因此,一个设计优良的系统,一定会在必要时考虑它自身的调试问题,
怎样对开发者友好,如何通过程序断点,调试进去。
系统具备可调试性,也相当于给系统增加了额外要求。
文档
一个接口,并不是仅提供功能就完事了,
接口以及如何使用这个接口,本来应该是一体的。
没有使用文档的接口,相当于只完成了一部分功能。
除此之外,系统的架构文档,也一样重要,
它被划分为了几个子系统,每个子系统是如何交互的?
否则,子系统之间类似“元胞自动机”一样的诡异交互方式,就没办法理解了。
注释应该也算是一种说明性的文档,只不过它是写到代码中的,
有丰富注释的代码,确实会省去每个人的阅读成本,这些成本累积起来是很可观的,
但对代码编写者来说,也会花费不少精力。
有详细文档和注释的系统,会比勉强能跑的系统,更好维护,
从可维护角度来讲,这是对系统提出了更高的要求。
监控
现在有很多系统是 7*24 小时运行的,一旦出错每一秒都会有损失,
因此,有监控是对这种系统提出的另一种要求。
当出现错误的时候,我们希望能检测到,通过报警机制,自动通知相关的责任人。
增加了监控机制的系统,会比正常的系统略微复杂一些,
如果把监控系统也看成原系统一部分的话。
如果没有额外的通用监控系统,为每个系统单独配一个监控,是一件麻烦的事情。
还得保证这些监控系统自身不会对现有功能产生影响。
不可监控的系统,相当于扔出去的炸弹,不知道什么时候它会爆炸,
但是使得系统可监控,也在一定程度上增加了复杂度,
需要额外的努力才能做到。
回滚
可回滚是系统的一种较为苛刻的属性,要想能回滚,需要系统先具有这种属性才行。
一个系统是否可回滚,在设计阶段就得考虑清楚。
并且它的每一次发布,都得指定详细的发布和回滚计划。
一个能回滚的系统,得做到它每一次发布所做的修改可撤销,
包括对数据库的修改,以及接口的兼容性,还有依赖方其他系统的影响。
这件事看起来简单,实则很难做到,
比如,对数据库结构进行了变更,而老接口又不可兼容的时候,
很多系统其实是不可回滚的。
又比如一些工具类库,版本号只能往上加,不能撤销,
那么一旦发布了某个不兼容的版本,就只能硬着头皮往下走了。
结语
为系统提出的每一个要求,都是要付出成本的。
一个勉强能跑的系统,人们对它的要求更低。
如果要求它更可靠,覆盖所有的异常流程,有日志可跟踪,
有单测覆盖,支持多套环境,可调式,有详细的文档,
支持监控,可回滚,它就会变得越来越复杂。
除此之外,肯定还有对系统的其他要求,例如自动扩缩容,
自动降级,安全方面的防护,等等。
因此,写好一个系统,并不是仅仅让它能跑就完事了,
额外还要做出很多事情。
这些功能加起来,足以让一个简简单单的应用,变得超级复杂,
很多侵入式的写法,会让代码变得几乎不可维护。
最后,一个系统到底要不要做得这么复杂,其实是设计者该考虑的事情,
系统要不要具备这样的隐性功能,要不要投入这么多人力去实现,
都是值得商榷的,并不是额外的功能越多越好。
没有好的系统,只看是否达到预期罢了。