其实,本文想要描述的,是一个大部分人已经知道的道理。只不过,我打算从另一个角度把前因后果向你剖析明白,这个角度就是大脑的缓存容量。
什么是程序呢?
对你(编写者)来说,程序是一行行文字;而对于程序的执行者——计算机来说呢?大家都玩过大富翁、飞行棋类的游戏吧?程序就像大富翁里的一张地图。电脑在执行写(编译)好的程序时,就像是玩家,它按照设计者(你)规划的路径前进。有时会走到有条件判断的格子里,满足某个条件时往一个方向,不满足时则往另一个方向。有时会往前跳,有时往后跳,有时还来回转圈。单线程程序只有地面一层,一个玩家。多线程程序可能有地上地下好几层,多个玩家同时玩。编写者的目的是:在不同的条件下,让玩家沿着你设计的地图走到正确的地方去,并且一路上拨动该拨动的开关(I/O)。那么什么是稳定的程序呢?无非就是在所有环境下,都能一直保持正确罢了。
那么要令这张地图一直正确,你必须时刻在大脑里装着这张地图。什么的条件下,会走入怎样的分支,以及最终会产生什么结果,全在你的掌控之中。这对你大脑的运行速度和缓存能力都产生了极大挑战。无论你当前在编写程序中的哪一处,你都需要对这一段程序所关联的所有路径都有着清晰的记忆。是不是很令人崩溃?但恐怕只有这样,你才能随时知道:自己写下的每一行代码会影响哪些地方的运行情况——我把它叫做编码波及范围——也才可能保证你画的“地图”一直是正确的。
怎么破?
大家应该都用过各种手机上的地图app。回想你是怎么用它的吧!当你看北京市的全景地图时,你看到的只是几个环,很多细节都被隐藏了。当你逐渐放大它,所观察到的区域变得越来越小,但区域中的细节逐渐清晰,直到最后,你找到了你家的楼。
我们大脑的缓存空间有限,就像这小小的手机屏幕一样。只不过有的人屏幕大点,分辨率高点而已,但差别也就如此了。那么编写程序时,我们是不是也可以让大脑只缓存手机屏幕那么大的部分呢?这部分内容要么是范围广而忽略细节的,要么是范围小而细节精致的。可是问题就来了,上一段里我说过,编写程序时,除了要关注这行程序本身,更要对可能波及的范围都有清晰的记忆。如果如果可能波及范围在我这块屏幕之外怎么办?听天由命?
所以有几个基本原则要掌握。
第一,块与块之间的关联要尽量少。这也就是所谓低耦合的模块化设计。如果模块之间的接口足够少,那么在进行模块内部的修改时,就不用过多考虑模块外的事。这也就使得上面说的,“只让大脑缓存一个小块的地图”这种事成为了可能;
第二,尽可能减少会被多线程调用的函数。多线程运行的程序,就像你的地图有地上地下好几层。那么在处理一小块地图时,你还得记住它里面有哪些楼梯,都通往哪里。所以最好少些楼梯,否则你的大脑不够用;
第三,定期重构代码。程序难免会被改动。有时应客户的各种需求,又受工期制约,你可能不得不临时修些“栈道”,绕开模块之间的接口,以迅速完成功能。可时间一长,程序就可能已经远离了上面的两个原则。想想你的玩家在地图中随时可能遇到栈道,跑到出乎你意料的地方去,是否令人头大?所以务必定期重新设计模块和接口,并拆除多余的栈道。
我们总是看到很多程序员在彻夜加班修改问题。在笔者看来,许多问题都源自各种不良设计。导致编码时大脑需要缓存的东西太多,以致hold不住。只好凭感觉胡写一气,再把质量寄托在测试人员身上(笔者还发现一个现象,越是大脑缓存能力强的人,反而越愿意花时间在前期设计上,从而使得编程过程所需的缓存空间更少,结果总能轻松hold住)。前期如果没有花足够多的精力来进行结构设计,那么后期的所谓努力工作,无非是在用战术上的勤奋来弥补战略上的懒惰罢了。
光写完代码还不够
就如写文章难免有错字,代码也一样。是的接下来我要说测试了。首先我假设你已经拥有了功能测试(所有软件产品在出厂前必经的环节)。所以这里我想重点谈一谈另外两种测试。
第一种是单元测试。首先,能够进行单元测试的代码,一定是功能区域划分清晰的。其次,单元测试覆盖的代码越多,你的每一次修改带来的未知隐患就会越小。更进一步说,如果你能做到先写测试用单元测试用例,再完成各个功能,那就再好不过了。因为这会强迫你用模块化的思维去构建框架;
第二种是极限测试,也就是用脚本和自动化工具,对程序进行各种高频次的、长时间的、正常的和不正常的操作。这种测试的核心是暴露各种,隐藏在系统中的问题。这些问题可能出现的几率极低,比如万分之一——然而当你的产品有十万用户使用,那就一定有十个人会遇到这个问题。极限测试的目的就是暴露这种,在正常使用中很很长时间才能出现的问题。
总结一下
程序像地图。要想写出正确的程序,需要在大脑里缓存这张地图。缓存空间有限,一次只能缓存一部分,所以要进行分块设计,且降低各部分间的耦合。实现的时候要从上到下,逐层细化,最后记得做单元测试和极限测试。
现在,道理你都懂了。但相信我,在经过大量实践之前,你没有真的懂。好在剩下的只是实践了。祝你好运!