软件进化论
2018 年 09 月 04 日
很久以前看到一本开发书,应该是侯捷的《深入浅出MFC》,里面大致有这么一句:有些人习惯由道到器,有些人习惯由器到道。当时就对这句话印象很深。让我从另一个更高的角度开始理解软件开发。
说起来,软件开发中,什么是道?什么是器?周易中说:形而上者谓之道,形而下者谓之器。因此很明显,思想是道,物件是器。那么软件应该是器,但设计软件的思路是道。软件开发,就是将道和器结合,生成新的器的过程。
我是那种习惯由器到道的人。也就是说,我习惯于先了解各种工具的特性,然后尝试使用工具,之后才能慢慢结合实际使用工具的效果,进入设计阶段。但由于前面提到的那句话,我相信有些人可以做到高屋建瓴,从一开始就给出一套自洽的设计,然后再在实现中逐步完成。
说实话,如果都能做出东西来,这两种习惯或者说指导思想其实没有明显的优劣。由器到道的设计往往追求实际,在前期以实现为主要目的,基础坚实,实现简洁明了,有可能失于远见。由道到器可能得到超乎想象的产品,但其设计需要天才的大局观,而且在执行时,很可能因细节的限制而功亏一匮。实际上,在很多现实场景和文学作品中,都有对这两种指导思想分歧的表现;比如讲开源的《大教堂与集市》;再比如我们更熟悉的,华山的剑宗与气宗。绝大多数情况下,软件是经验主义的产品。开发者总要依据某一种开发思想、借助于多种已有成熟工具、受限于客观的软硬件环境、遵循各种语言的范式和推荐,才有可能做出可以用的软件。不是人人都像Linus那样,说要有SCM,就可以自己做一个git出来解决问题。
以我自己的经验为例。前一段时间做了KaiOS的FOTA,总共代码在10K行以上,包含两层十几个模块,工作量算是比较大了。早期的FOTA底层库用纯C实现,包含自实现的http/xml/json库,代码量在50K行以上;加上上层应用,总代码量在60K行,以个人眼光来看,根本就是维护不能。所以从一开始就选择了彻底重构。
重构过程可以说是一个完整的进化过程。我依序做了下面这些事情:
- 在最开始什么都没有的时候,先做了一个自测用的App,只是用来单独调用底层接口;
- 根据底层需求,尝试着做了一组接口,跟App联调;
- 联调通过后,底层进行初步分层分块设计,并根据分层分块做了假的模块实现,以测试分层分块设计的思路是否正确;
- 分层分块思路基本验证完成之后,拥有了一个可以用的假底层,这时上层自测App已经基本没有用了,于是开始写正式的App,以确定上下层通信的内容;
- 正式App基本完成后,开始写真正的实现模块,为了减少工作量,暂时保留了原有的底层库,只是将其压缩到一个模块中,做好后期替换的准备;
- 保留原有底层库的正式底层实现完成后,主体工作就暂且告一段落了,一方面这部分工作上传git,另一方面在一个独立的linux cli应用中,尝试重新实现原底层库功能;
- linux cli实现完成后,将新的底层库替换原有的底层库,再次上传git。
经过这样一组层次分明的开发之后,我才得到了一个完整的FOTA应用。此时设计上仍有缺陷,比如因为KaiOS本身API的限制做出的一些妥协,事实上还可以找到相对更好的方法来解决。这需要留待后期逐步完成。
回顾这次开发经历,很多道理跟现实世界是完全相通的。比如:
- 命名要规范。从一开始就要对整体的名词进行规范,以避免产生误解。比如上面用了Notify,底层就不能改成Report。
- 责任要清晰。软件分层分块之后,有很多功能实际上是可以选择在多个模块中实现的。这时如果初期设计的各个模块责任明晰的话,就不会出现这个问题。一个功能很自然的就会找到所属的模块。
- 不能做空中楼阁。像自测App和假的底层实现,虽然最后都会被删除,但这些都是实际开发中必要的脚手架。理论上不写这些的话,写的代码更少。但实际上如果不写这些脚手架代码,出错时验证的麻烦更多。
- 最后,原则虽然重要,但现实更重要。比如最初的设计中,业务逻辑都在底层实现,但由于KaiOS Alarm接口只能在上层调用,不得已把一部分逻辑放到了上层。
Kevin Kelly有一种观点,软件是生物。从这个角度来看,前面提到的两种指导思想正好也代表了生物诞生的两种观点,进化论和创造论。这个比方并不是说两种指导思想必然有一个是错的。相信即使在现实世界中,也有无法用进化论解释的生物存在。换回大教堂和集市的比喻,有米开朗基罗那样的天才大师,才有西斯廷教堂整面墙上那样宏大的壁画;这跟芸芸众生进进出出的集市相比,没有对错,更不是对立。前者是美的一种表现,后者是生命的一种表现。大多数时候,用进化方式生成软件更为合理。
毕竟,我们都是凡人。