什么是好的软件工程

By: Jeff Hu
Posted: December 17, 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),即值的字面量,而是用良好命名的全局变量来表示。

其他一些小点:

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

“The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; ipremature optimization is the root of all evilib (or at least most of it) in programming.” - Donald Knuthi

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

“The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.” - Edsger W. Dijkstrai
“A good API is not just easy to use but also hard to misuse.” - JBDi
“Making things easy to do is a false economy. Focus on making things easy to understand and the rest will follow.” - Peter Bourgoni
“This is a cardinal sin amongst programmers. If code looks like it’s doing one thing when it’s actually doing something else, someone down the road will read that code and misunderstand it, and use it or alter it in a way that causes bugs. That someone might be you, even if it was your code in the first place.” - Nate Finchi

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

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

Keep it simple and stupidi - Famous Sayingi

保持简单,避免 overkill。

Do not repeat yourself - Famous Sayingi

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

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

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

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

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

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