「程序员」和「软件工程师」是一回事吗?

同样是写代码开发软件,有人自称「程序员」,也有人自称「软件工程师」。真要抠字眼的话,这两者之间有怎样的区别呢?Google 今年出版的新书 Software Engineering at Google (SWE Book) 在第一章开头就提出:「编程」和「软件工程」是两回事。

Software engineering is programming integrated over time.

「编程」指生产代码的活动,而「软件工程」则是在此基础上再添加时间这一维度。正如三维的立方体不等同于二维的正方形,距离不等同于速度,「软件工程」也不等同于「编程」。

要区分一种软件开发的模式是更接近于「编程」还是「软件工程」,我们可以问这样一个问题:这份软件的预期生命周期有多长?或者说这份软件需要被维护到什么时候为止?一般而言,这些活动会更接近于「编程」:

  • 学校的编程作业往往只存在几小时到几天。
  • 移动应用的更新周期一般很短,彻底重写也时常发生(哪怕在工业界)。
  • 在与时间赛跑的早期创业公司,今天写的代码都不一定活得到下个月底。
  • 连续单人创业者、职业外包开发者等群体则完全有可能在业界干了十几年却从未维护过时间跨度超过一两年的项目。

而像 Google 搜索或是 Linux 内核这样的项目,它们的生命周期可以认为是无限期的。

「软件工程」区别于「编程」的三个方面

Time

在动辄以年为单位的预期生命周期下,改动是不可避免的。一个软件系统即使没有需求上的变动,也必然会由于时间的流逝而面临不得不做的改动,比如:

  • 系统所依赖的 library、API 等发生了变化
  • 新的安全漏洞被曝光
  • 硬件或是算法等方面的进步带来了新的优化空间

这就使得我们在软件工程中永远要考虑代码的可持续性/可维护性。

Scale

现代软件工程几乎无一例外都是团队合作的结果。随着规模的扩大,团队组织、项目结构、政策、流程、实践等等都需要跟着 scale。经验告诉我们,一些东西比较容易 scale,而另一些则更为困难。SWE Book 从多个方面进行了具体介绍1,而本文主要关注作为个人可以做的。

Trade-off

在软件工程中做决策也更为有挑战性:风险和机会成本更高、做决策时信息不充分,对问题的理解永远不够全面并且在时刻变化,等等。

「软件工程师」的自我修养

对这些区别的认知可以为我们提供一个关于「软件工程师应该怎么做」的框架。

可持续化开发

对个人而言,可持续化开发大致就是要实践笔者以前提到过的 Strategic Programming。摘录相关段落如下:

... 就像着眼全局的战略家,能持之以恒地投入资源从而在完成开发目标的同时逐步地、系统性地提升整个 codebase 的可维护性、可拓展性以及测试覆盖率等。

在信息不充分的前期,积累一些技术债务是不可避免的。学会定期偿还这些债务以保持系统的可持续开发,则是成为高效的工程师的关键。

... 一些具体操作建议:

  • 新代码必须有测试。对于缺乏测试的旧代码,改动时的第一步永远应该是添加测试。
  • 在改动一个模块之前,如果观察到代码质量不理想,可以考虑先做一次重构。当然,良好的测试覆盖是我们能够充满信心地重构的先决条件。
  • 充分利用各类代码分析工具,定期发起 Fixit 来提高代码性能和可读性、优化 build 规则、改进易错代码、补充注释、处理 todo、清理 dead code、迁移 deprecated API、拆分变得过于庞大的类和文件等等。
  • 如果开发的痛点是由不够理想甚至错误的 design 所导致的,在寻找 workaround 作为短期解决方案的同时,也要开始思考、规划和投入资源在重构或是迁移到更正确的 design 上。

正确看待 Code Review

Code is read much more often than it is written.

代码是写给别人(现在的 peer)看的,也是写给自己看的,但归根结底还是写给别人(未来的维护者)看的。在做代码改动时,我们要时刻提醒自己:code review 在很大程度上是 peer 之间的一种交流方式,而不是一种单纯的 bug 预防机制或是一种集体分担(逃避?)代码出错的责任的形式主义。

明确这样的心态以后,下面是一些具体的建议:

  • 对绝大多数不那么显然的改动,永远应该先提及改动的动机/目的是什么。假设作为一个团队我们想要达到目标 X,为此你认为需要达到中间目标 Y,从而需要做 Z。如果 reviewer 没有意识到这个逻辑链条、不够敏锐或是偷懒,对话可能就会局限于「如何做 Z 才是最好的」,从而忽略了「Y 并非必需的中间目标」、「有更简单的方式可以直接达成 X」、「达成 Y 会导致你没有意识到的副作用从而使得 X 无法被达成」等可能性。勤用 5 whys 技巧
  • 包括动机/目的在内,所有 reviewer 大概率会用得上的信息都应该被包括在改动里面。一方面这样便于未来的维护者在翻阅代码改动历史时了解当时的背景,另一方面在改动里写了「废话」的成本往往比信息提供不充分而导致的几轮来回对话小多了。这种 over-communication 在当合作的双方无法面对面交流或是有时差等情况下尤其能够大大节省彼此的时间。
  • 对于绝大部分不关键的可读性上的分歧,接受 reviewer 的意见往往是更好的选择,因为代码的作者往往会偏向于高估代码的可读性。除了非常极端的情况,切忌认定对方看不懂一段代码是因为「不够聪明」。

减轻认知负荷

有人说,写(高级语言)代码的本质就是在给(机器语言)0 和 1 写注释。类似地我们也可以说,之所以讲 Naming 是计算机科学中最困难的两件事情之一2,就是因为它的本质是在给一堆对人脑而言毫无意义的内存地址赋予合适的名字,从而让人能跟得上机器的思路。但是机器可以在记住成千上万个变量的地址和值的同时用难以置信的速度做五花八门的运算,相比之下人脑则是高度不可靠的。当我们说一份代码难以维护,其最直观的表现就是给试图阅读和修改它的人带来很高的认知负荷(Cognitive load)。

能正确地写出思维复杂度高却又缺乏注释和文档的代码(或者说,只有自己看得懂的代码),在某种意义上或许也算是一种成就,但长远来看绝不是一个好主意3:不仅你的 peer 会看不懂,几个月的你大概率也会看不懂。在自己踩过几次坑之后,笔者的经验是:

  • 以写 deep module/class/method4 为目标:一个模块、类或者方法的具体实现可以非常复杂,但 interface 一定要越简单越好。与此同时,对外部状态的依赖越少越好。
  • 如果一个函数的参数列表很长,命名和注释就至关重要;如果光用语言难以准确描述,可以借助实际的输入输出例子来更直观地告诉读者一个函数在干什么。这些例子可以写在注释里,或者以单元测试的形式来呈现则是更佳。
  • 在有些情况下,把同一个模块的代码写两遍比试图一次性写出优雅、高效、便于理解的代码来的更快。因为当模块复杂到一定程度,有很多细节(尤其是一些关键的 interface 应该怎么定)是不自己写一遍就很难事先想明白的。
  • 避免闷头苦干然后把一个巨大的改动砸向 peer。每个改动控制在多大因人和团队习惯而异,但一般而言 interface 的设计等关键改动需要独立出来,方便 review 也避免由于不得不改 interface 从而导致大规模重构。
  • 切忌迷信「一个函数不能超过/少于多少行」等教条式的建议。

知识共享

接手一份代码并让它运行起来是相对比较容易的,但这并不意味着今后维护这份代码所必需的知识也会自动跟着转移5。任何有实际应用场景的代码,在某种意义上都只是将我们在其对应的问题领域所积累的知识形式化地表示出来的介质。对代码的 ownership 是表面的,对知识的 ownership 才是关键的。

  • Code Review 就是一种知识共享的形式,尤其当发生在 junior 和 senior 之间。有时可以把 review 过程中产生的对话整理后放入注释/文档,从而避免未来的维护者也产生类似的疑问。
  • 当你设计开发或是接手了一个复杂度比较高的模块/系统,如果能比较好地实践上一小节中提到的建议,有足够的文档和良好的测试,再在团队内部做一个分享,这就是很良性的知识共享,也是一种很 scalable 的让一个团队能够 own 更多代码的方式:如果你在一个模块上花了 60 个小时,然后能在一个小时内向 peer 讲清它的工作原理和如何维护、peer 们之后能够在一个小时内把握和修改你留下来的代码,这样的投资就是很理想的。与此同时,多跟 peer 分享自己的工作也有助于保持士气、提高个人成就的可见度、并在一定程度上避免自己被绑定在某些特定的项目上6
  • 对他人的工作保持好奇心。随着公司的规模扩大,我们往往会发现有不同的团队都在做领域相近、解决方案类似的事情。很多时候我们都可以参考甚至复用他人的经验,或是发现新的合作机会。

一个简单的原则:多问,多写。

降低犯错成本

Shifting Left

SWE Book 中提出的 shifting left, aka getting feedback early (and often):代码挂在 integration test 上永远要比挂在 production 上好;单元测试能捕捉到的 bug 往往要比手动测试中发现的 bug 复现起来更容易;在写代码之前意识到 assumption 的错误自然要比写完代码以后再去改来得轻松……

动手开发之前多思考和验证 assumption 的正确性、在 design 上多征求 peer 的意见、多依赖静态分析工具、细心写单元测试、认真对待 code review……坚持做好这些事情,永远有意想不到的回报。

Hard Things First

我们在思考 trade-off、做决策的时候会去权衡各种成本。有些成本是很显然的,而另一些就因为种种原因不那么好量化。经验告诉我们,要想做出更好的决策,永远先做难的事情。

假设要搭建一个新的系统,其中大部分组件你都很熟悉,但其中有一个模块要用到你没有接触过的工具/语言等。出于惰性,这时候人往往倾向于先把手到擒来的部分完成。但其实我们应该首先花时间去加深对不熟悉的模块的了解,必要的话还可以动手写个简单的 prototype。如果不这样做的话,你会很难去准确地估计整个系统开发的成本和可能的风险。

这可能也是二八原则的一种体现:一个项目中 20% 的部分需要我们花 80% 的时间去事先研究,并且它们对于项目是否能够低风险完成贡献了 80% 的影响。

后记

对「编程」和「软件工程」、「程序员」和「软件工程师」这些表达方式做区分,绝不是为了当语言警察。除了在法律上混淆不同的头衔可能带来问题之外,我们可以随意混用这些表达,只要它们在语境下是合适的。类似地,使用「码农」这类表达也无伤大雅;这也不是 gatekeeping。任何人都应当被欢迎进入业界工作或是发表自己的意见,不论他们的实际头衔是什么,也不论他们对自己的定位是「程序员」还是「软件工程师」。

做这样的区分,是因为即使这种区分不被所有人同意,但只要是在某个明确的定义下,的确会存在两个或更多相关却有所不同的领域。它们有着不同的约束条件、价值追求和最佳实践。有些工具和做法在一个领域很有效,在其他领域可能就不那么有价值了。

许多人的职业道路都是从学校或者小作坊进入大公司或是非早期小公司,在这个过程中大概率会经历从「程序员」到「软件工程师」的角色转变。SWE Book 和本文所讨论的区分,是为了建立对「软件工程的本质是什么」这个问题的思考框架,从而为这样的角色转变提供理论支撑和切实可行的建议。


Notes

  1. 关于在人力资源调配和沟通等方面中存在的难题,建议阅读著名的《人月神话》。
  2. https://martinfowler.com/bliki/TwoHardThings.html
  3. 或许可以说:在编程中,评价一个人在「耍小聪明」是一种赞美;而在软件工程中这则是一种批评。
  4. A Philosophy of Software Design 第四章 Modules Should Be Deep
  5. Knowledge does not automatically transfer to the next generation. -- Good times create weak men
  6. 另见对孤军奋战说不

Subscribe via RSS

CC BY-NC-SA 4.0 © 2019 - 2020 ❤️ Linghao Zhang