什么是好的软件工程

Posted: December 10, 2020

我写程序已经很多年了。若把 13 年开始做算法竞赛当作起点,那么这个尺度是 7 年,如果还算上在那之前因为玩单片机而自学的汇编和 C 语言,这个时间还会更长。

用写程序的年限来判定一个人的水平,其实是不可取的。我们想当然地觉得,经验和水平会随着时间的推移而增加,这个论断是否成立我们暂且不表,就凭不同的增加速度就足以让时间显得毫无意义——同样是线性增加,有的人的斜率会比一般人高的多,更可怕的是非线性的增长。

七年的编程经验并没有让我觉得骄傲——甚至让我觉得羞愧。我仍然觉得,自己离一个好的软件工程师还差的远。一方面,我过去的水平增长速度并不持续,高一高二高强度做竞赛自然是不错的,但高三回归课堂那一年就几乎没怎么写过程序了;另一方面,我对程序设计和软件工程的认知也并不全面,无论是竞赛时写的单文件程序,还是后面做科研时只追求完成、不在意质量的验证代码,离真正的软件工程还差得远。

我在离开学校后给自己树了一个「成为优秀的软件工程师」的小目标,而众所周知,刻意练习和反思是高速成长最有效的手段。我想先在这样一篇文章中梳理下自己对「好的软件工程」的认知,谬误和疏漏之处难免,请各位看官不吝赐教。


什么是程序?

绝大多数人的对程序的看法是「写给计算机执行的代码」。但从工程的角度而言,我最喜欢的定义来自一本非常经典的程序设计入门书 Structure and Interpretation of Computer Programs(计算机程序的构造与解释)——programs are meant to be read by humans and only incidentally for computers to execute(程序写来是给人阅读的,只是恰好可以在计算机上运行)。

倘若是那种一次性代码,那么为了快速完成或者追求极致性能,代码确实只需要写给计算机执行即可。但对于需要长时间建设以及需求不断变更的软件工程而言,代码是需要被维护的。无论是给未来的自己看或者是给合作伙伴看,写让人更容易理解、更容易修改和维护的代码,绝对是比在计算机上执行更重要的追求。从这个角度上说,code is not assets, but liability(代码不是资产,而是责任)。

为了达成「好理解、好维护」这个目标,从微观和宏观角度出发,我分别阐述。

微观指的是编码习惯和细节。在这个层面,降低理解成本最重要的手段是保持一致性。

一方面是对良好代码风格(style)的坚持,比如缩进、换行等。通过一致的风格,人们在阅读代码的时候可以更容易找到重点所在。维护一致代码风格的最好手段是使用格式化工具(比如 go fmt 或者 prettier),因为人总是会有疏漏,特别是多人协作的时候,所以在持续集成中加上自动的格式检测是维护风格的最佳实践。

另一方面是命名规范。变量和函数的命名对于理解其含义和用途而言是至关重要的。虽然命名的确是一个困难的问题,至少准确性和一致性是需要保证的。准确性在于准确地描述作用,绝对不能买羊头挂狗肉,这种误导可能会让后来看代码的人口吐芬芳。一致性则是命名的风格要保持一致,如 C/C++/Rust 常用蛇形(is_float)表示变量和函数;全局变量通常使用全大写(MAX_LOG_SIZE);Go 用小驼峰(isFloat)表示局部变量,用大驼峰(TableScaner)表示类型。此外,要尽可能避免在代码中出现魔数(magic number),即值的字面量,而是用良好命名的全局变量来表示。

其他一些小点:

如果说微观上的认知更多的是「术」这一层的理解,那么宏观上的「设计」就是「道」了。我的经验离论道的水平还差得远,所以一个很好的方法是从大师前辈们说过的话中汲取精髓。比如说

过早优化是万恶之源。考虑太多未来的需求和性能上的提升会让代码在一开始就难以维护。好的设计应该让重构变得容易,而不是消除重构。

软件工程的本质就是构建抽象,好的抽象提供了明确的语义层级,给出了功能边界和权责界限,使得程序员在编写程序的时候可以只关注一部分代码,极大地降低了心智负担和重构成本,从而提高可维护性。设计模块和函数的目的就在于此——每个模块只需要给外部提供接口函数,在保证接口不改变的情况下,内部可以换实现方式,从而隐去实现细节。好的抽象也应该服务于解耦合(decoupling)的目的。

这方面的论述还有 rule of thumb 非常的多,比如

保持简单,避免 overkill。

复制粘贴不可取,抽象成函数;数据不要放在多个地方,可以通过规范化(normalization)构建 single source of truth。

这方面的论述有很多,可以搜索 software engineering quote 来专门找(比如这里),或者参考这个维基百科页面。下面这些说法背后的原理都应该了解一下

以及这里列出整整一页的反模式(anti-pattern)。


不过了解的原则再多,落到设计的实践上,最重要的问题反而是取舍(trade offs)。

性能、可读性、可维护性、代价等等,内部有非常复杂的耦合与拮抗的关系,而这往往是最困难,也是最需要根据具体情况来分析的。了解上述的那么多原则,本质上的作用是增加人在做程序设计时的品味(taste),避免写出「难闻」的代码。

希望在未来的程序员生涯中,我能仔细观察和思考,找到好的设计与大家分享。