写好一个系统需要额外付出多少努力

当今软件生态已经越来越复杂了,一个简单的产品,

可能动辄涉及到十个,甚至数十个子系统,

把所有这些子系统当做一个整体来看的话,写好它是一件很难的事情。


本文就来探讨一下,一个勉强能跑的系统,跟一个生态优良的系统,

到底有什么区别,以及需要额外付出多少努力。

异常

一个勉强能跑的系统,不会覆盖所有的异常流程,

每个函数只有一个返回值,它们被看做永远不会抛异常


然而,这肯定是一种不合理的假设,

当有一个函数可能会抛出异常时,事情就变得复杂了。

这个函数的调用方到底该怎样“消化”这个异常呢?


除此之外,甚至在业务层面,也会包含一些“非正常”的业务逻辑,

比如一个没有注册的用户访问授权页面,应该怎么处理?

或者是,一个接口被传入了跟预期不符的参数时,应该怎样反应?


把所有功能看做一个整体,那么正常流程应该只有一种,

出错的可能,会有很多种。


如果每一种出错,都经过了深思熟虑的处理,而不是默认结果,

那么,加上异常流程的系统,会比勉强能跑的系统,复杂数倍

一个可靠的接口,会由一个正常结果,与 N 个异常组成。

日志

对一个没有日志的系统排查问题,是非常困难的,

很难还原出当时的运行现场。


一个常见的方法是在业务逻辑的字里行间写下日志,

这种侵入性的日志编写方法,会降低代码的可读性,

让逻辑变得晦涩难懂。


而非侵入的日志编写方法,可能无法覆盖到所有细节,

一般是通过异常切面来做。


不论用哪种方法,增加了日志功能的系统,会比勉强能跑的系统复杂了一些。

这是毋庸置疑的。

因为日志功能已经算是原系统的一种非功能需求了。

单测

专业程序员喜欢用日志和单测来检查问题,遇到问题跑一下单测,

能省掉很多构造数据的成本。


然而,有单测覆盖的系统,也一定会增加不少代码量,

对于那些功能频繁变动的系统,甚至每改一些代码,就得相应的改改单测。


单测虽然一定程度上增加了代码的可靠性,

但同时,也增加了代码的编写成本


除此之外,软件的每一次持续集成都会跑一遍单测,

花在修补单测上面的时间,可能会出乎意料的多。


因此,编写成本和软件质量是不可兼得的。

没有一劳永逸的办法。


有单测的系统,在维护者看来,确实更加复杂,

单测可以看做是给代码加了“有测试覆盖”这条额外的属性

环境

有些系统是要分本地环境和线上环境的,

本地开发时,跟线上运行时,系统的表现可能会不同。

简而言之,某些行为可能会跑到不同的分支里面。


最起码,他们也可能会从不同数据库中读取信息。

这样做也会增加系统的复杂性。


要发布一个系统,我们必须保证它在好几个环境都表现良好,

而不是仅仅线上表现良好就够了。


一旦有多个子系统,都区分了不同环境,问题就更麻烦了,

在调用某个接口的时候,我们必须严格区分调用的是哪个环境中的接口。

这比只有线上一套环境的系统,又复杂了好多。

调试

有些系统的设计之初,只是为了满足功能,

可一旦跟其他系统打交道时,就会涉及调试和排查问题。


一个不能调试的系统,在排错时是特别麻烦的,

我们只能一行行的读代码,在大脑中记录程序状态,人肉调试。


这种情况在遇到多态函数时,会变得超级烧脑。

很多运行时数据的丢失,导致我们不知道程序到底该执行哪段代码。


因此,一个设计优良的系统,一定会在必要时考虑它自身的调试问题,

怎样对开发者友好,如何通过程序断点,调试进去。


系统具备可调试性,也相当于给系统增加了额外要求。

文档

一个接口,并不是仅提供功能就完事了,

接口以及如何使用这个接口,本来应该是一体的。

没有使用文档的接口,相当于只完成了一部分功能。


除此之外,系统的架构文档,也一样重要,

它被划分为了几个子系统,每个子系统是如何交互的?

否则,子系统之间类似“元胞自动机”一样的诡异交互方式,就没办法理解了。


注释应该也算是一种说明性的文档,只不过它是写到代码中的,

有丰富注释的代码,确实会省去每个人的阅读成本,这些成本累积起来是很可观的,

但对代码编写者来说,也会花费不少精力。


有详细文档和注释的系统,会比勉强能跑的系统,更好维护,

从可维护角度来讲,这是对系统提出了更高的要求。

监控

现在有很多系统是 7*24 小时运行的,一旦出错每一秒都会有损失,

因此,有监控是对这种系统提出的另一种要求。

当出现错误的时候,我们希望能检测到,通过报警机制,自动通知相关的责任人。


增加了监控机制的系统,会比正常的系统略微复杂一些,

如果把监控系统也看成原系统一部分的话。


如果没有额外的通用监控系统,为每个系统单独配一个监控,是一件麻烦的事情。

还得保证这些监控系统自身不会对现有功能产生影响。


不可监控的系统,相当于扔出去的炸弹,不知道什么时候它会爆炸,

但是使得系统可监控,也在一定程度上增加了复杂度,

需要额外的努力才能做到。

回滚

可回滚是系统的一种较为苛刻的属性,要想能回滚,需要系统先具有这种属性才行。

一个系统是否可回滚,在设计阶段就得考虑清楚。

并且它的每一次发布,都得指定详细的发布和回滚计划。


一个能回滚的系统,得做到它每一次发布所做的修改可撤销

包括对数据库的修改,以及接口的兼容性,还有依赖方其他系统的影响。


这件事看起来简单,实则很难做到,

比如,对数据库结构进行了变更,而老接口又不可兼容的时候,

很多系统其实是不可回滚的。


又比如一些工具类库,版本号只能往上加,不能撤销,

那么一旦发布了某个不兼容的版本,就只能硬着头皮往下走了。

结语

为系统提出的每一个要求,都是要付出成本的。

一个勉强能跑的系统,人们对它的要求更低。


如果要求它更可靠,覆盖所有的异常流程,有日志可跟踪,

有单测覆盖,支持多套环境,可调式,有详细的文档,

支持监控,可回滚,它就会变得越来越复杂。


除此之外,肯定还有对系统的其他要求,例如自动扩缩容,

自动降级,安全方面的防护,等等。


因此,写好一个系统,并不是仅仅让它能跑就完事了,

额外还要做出很多事情。


这些功能加起来,足以让一个简简单单的应用,变得超级复杂,

很多侵入式的写法,会让代码变得几乎不可维护。


最后,一个系统到底要不要做得这么复杂,其实是设计者该考虑的事情,

系统要不要具备这样的隐性功能,要不要投入这么多人力去实现,

都是值得商榷的,并不是额外的功能越多越好。


没有好的系统,只看是否达到预期罢了。