一个 30 年老程序员的修炼之道

阅读数:14489 2019 年 7 月 8 日 12:05

一个30年老程序员的修炼之道

本文作者 Julio Biason 从 1990 年开始从事软件开发工作,以下是他从过去 30 年软件开发生涯总结出来的一系列冷笑话式的经验之谈。

关于软件开发

规范先行,然后才是代码

在知道要解决什么问题之前,请不要写代码。

Louis Srygley 说过:“如果没有需求或设计,编程就成了一门往空文本里添加 bug 的艺术”。

有时候,仅仅一两段简单的描述就足以说明一个程序是用来干什么的。

每当你停下来,看着代码,并开始思考下一步该做什么的时候,通常是因为你不知道下一步该做什么。这个时候,你需要做的是与同事讨论,或者可能需要重新思考之前的解决方案。

用批注的方式把实现步骤写下来

如果你不知道从哪里下手,先使用英语(或者你的母语)把程序的流程写下来,然后在批注中添加代码。

你也可以把每个批注当成是一个函数,然后用代码实现它们。

用好 Gherkin

Gherkin 是一种测试 DSL,用来描述“系统处于某种状态,如果发生某个事件,这就是所期望的状态”。即使你不使用测试工具,Gherkin 仍然可以帮你更好地了解你能够从程序中获得哪些东西。

单元测试还不够,最好还要有集成测试

在我目前的工作中,我们会进行模块和类级别的测试。这些测试可以让我们知道模块或类的行为,但无法让我们知道系统整体的行为——而集成测试可以告诉我们这些。

测试让 API 变得更健壮

代码是分层的:存储层负责数据持久化,处理层负责转换存储的数据,视图层负责呈现数据,等等……

分层测试可以让你更好地了解各个层的 API,知道如何更好地调用各个层:API 会不会太复杂了?要进行一次简单的调用,是否需要保留很多数据?

通过命令行运行测试用例

命令行对于任何一个项目来说都很重要。在你知道了如何使用命令来执行测试用例之后,就可以进行自动化测试,然后将它们集成到持续集成工具中。

做好丢弃代码的准备

有很多人在开始使用 TDD 时会感到很恼火,因为他们可能需要重写很多代码,包括已经写好的那些。

而这正是 TDD 的“设计哲学”:随时准备好丢弃你的代码。随着对问题研究的深入,你对要解决的问题越来越了解,不管之前写了怎样的代码,它们终究不是解决问题的最终方案。

不过你不用担心,代码并不是一堵墙,如果将它们丢弃掉,也算不上是浪费砖块。但花在写代码上的时间确实会一去不复返,不过换来的是你对问题更好的了解。

好的编程语言自带测试框架

可以肯定地说,如果一门编程语言的标准库自带了测试框架,哪怕这个框架很小,它的生态系统也会得到比那些不提供测试框架的编程语言更好的测试,即使外部为这些语言提供了很好的测试框架。

想得太长远是一种浪费

有时候,程序员在解决一个问题时会想方设法寻找可以解决所有问题的方法,包括那些可能会在未来出现的问题。

但事实是,未来的问题可能永远都不会出现,而你不得不去维护一大堆在未来可能永远都用不上的代码,甚至重写所有代码。

问题要一个一个解决,解决完眼前的,再解决下一个。到了某个时刻,你可能会从解决方案中找到某种模式,而这些模式才是用来解决“所有问题”的良方。

写文档其实是在善待未来的你

谁都知道,给函数、类或者模块编写文档是一件苦差事,但这也是在给未来的你省下很多麻烦。

文档就是契约

代码的文档实际上是一种契约:文档里怎么写的,这个函数就是用来做什么的。

如果后面你发现代码和文档不匹配,那么就是代码有问题,而不是文档有问题。

如果一个函数文档里出现了“和”逻辑,那就一定有问题

一个函数应该只做一件事情。在给函数编写文档时,如果你发现需要用到“和”逻辑,那说明这个函数所做的事情不止一件。这个时候需要把函数拆成多个,不要在文档里出现“和”逻辑。

不要将布尔类型作为参数

程序员在设计函数时通常喜欢在参数列表里添加布尔类型,但请不要这么做。

举个例子:假设你有一个消息系统和一个函数,这个函数将所有消息返回给用户,叫作“getUserMessage”。不过,有时候用户需要获取整条消息,有时候只需要获取消息概要(比如消息的第一段)。于是,你加了一个布尔类型的参数,叫作“retrieveFullMessage”。

再次提醒,最好不要这么做。

因为当别人看到“getUserMessage(userId, true)”这样的代码时,他们可能不知道“true”是什么意思。

你可以新增两个函数“getUserMessageSummaries”和“getUserMessageFull”,然后让这两个函数分别调用“getUserMessage”,并将 true 或 false 传给它,这样可以保证对外的接口是清晰的。

在修改接口时要小心

上面提到了重命名函数,如果调用函数的代码完全处在你的控制之下,那么这么做就没什么问题,你只需要把需要修改的地方找出来,然后改掉它们就可以了。

但如果被重命名的函数是作为库的一部分暴露给外部,那就不能随意修改了。因为这样做会影响到所有调用函数的代码,而这些代码不在你的掌控之下,修改函数名只会给这些代码的主人带来大麻烦。

你可以新增一个函数,然后把旧函数标记为已弃用。经过一些版本发布之后,就可以慢慢将旧函数去掉。

好的编程语言自带集成文档

如果一门编程语言为函数、类、模块提供了文档或者生成文档的方式,那么你就可以肯定,这门语言的函数、类、模块、库和框架也会有很好的文档(即使不是最好的,但肯定不会差)。

相反,不提供集成文档的编程语言通常只有糟糕的文档。

在选择编程语言时,不要只看语言本身

编程语言是你用来解决问题的得力工具,但不仅限于此:它还有构建系统,有依赖管理系统,有工具、库和框架,有社区……

在选择编程语言时,不要仅仅因为它用起来很简单。记住,你可以认为一门语言的语法很简单,但也需要考虑到社区因素。

有时候,让程序奔溃比什么都不做更好

这句话听起来有点奇怪:与其捕获了错误却什么都不做,还不如不捕获错误。

在 Java 里,经常会有人这么干:

复制代码
try {
something_that_can_raise_exception()
} catch (Exception ex) {
System.out.println(ex);
}

这几行代码除了把异常打印出来之外,什么都没做。

如果你不知道该怎么处理它,还不如把它抛出来,这样起码可以知道什么时候会出现这样的异常。

如果你知道怎么处理异常,那就处理好它

这与上一条刚好相反:如果你知道什么时候会抛出异常、错误或得到返回结果,并且知道怎么处理它们,那就处理好它们。可以把错误消息显示出来,试着把数据保存到某个地方,把用户的输入写入日志文件,等后面再回头来处理。

类型系统会告诉你数据长什么样子

内存里存的只不过是一系列字节,而字节只不过是从 0 到 255 的数字,这些数字的意义需要通过编程语言的类型系统来说明。

例如,在 C 语言中,“char”类型的 65 其实就是字母“A”,而“int”类型的 65 就是数字 65。

在处理数据时要牢记这个。

下面是我最近看到的一些 JavaScript 代码,有人用这种方式判断 True 的值,但显然是错的。

复制代码
console.log(true+true === 2);
> true
console.log(true === 1);
> false

如果数据是有模式的,那就用合适的结构来保持数据的模式

你可能会用 list(或者 tuple)来保存数据简单的数据,比如只有两个字段的数据。

但如果数据是有模式的,也就是有固定格式的,那么就应该使用合适的结构来保持数据的模式,比如使用结构体或类。

停止盲目跟风

“盲目跟风”的意思是:如果有人这么做了,那我们也可以这么做。大多数时候,盲目跟风是解决问题最简单的方式。

“如果某个大公司是这样保存数据的,那么我们也可以这样”。

“如果有大公司撑腰,那它就是好东西”。

“正确的工具”可能只是个人喜好

“正确的工具”本来应该是指使用合适的工具来完成某个任务,例如,使用合适的编程语言或框架来代替目前使用的语言或框架。

但每当我听到有人提到这个说法时,他们只不过是想用他们喜欢的语言或框架来替代真正合适的语言或框架。

“正确的工具”不一定是正确的

假设你所在的项目需要处理一些文本,你可能会说:“让我们使用 Perl 吧”,因为你知道 Perl 很适合用来处理文本。

但你忽略了一点:你周边的人只懂 C 语言,不懂 Perl。

当然,如果这个项目只是个无关紧要的小项目,那么可以尝试使用 Perl,但如果这个项目对公司来说很重要,那么最好是使用 C 语言。

不要去修改项目以外的东西

有时候,人们会去直接修改外部的工具、库或框架,比如直接修改 WordPress 或 Django 的代码。

这样很快会让项目变得难以维护。当外部工具发布新版本时,你不得不去同步变更,但很快你会发现,之前修改的东西对新版本不再有效了,所以只能保留旧版本,而旧版本可能有很多 bug。

数据流胜过设计模式

如果你知道数据是怎么流经系统的,就可以写出更好的代码,这比应用各种设计模式要好得多(这只是个人观点)。

设计模式是用来描述解决方案的,而不是用来寻找解决方案的

同样,这也只是我的个人观点。

大多数时候,人们会应用设计模式,试图通过设计模式来找到解决方案,但结果是不得不对解决方案(甚至是问题本身)作出调整来匹配设计模式。

这样的事情我见得多了:先是遇到问题,然后找到一个接近解决方案的设计模式,接着开始应用设计模式,然后在解决方案里添加很多东西,让它与设计模式相匹配。

学会基本的函数式编程

你不一定要成为函数式编程专家,但请记住,有时候需要保持数据的不变性。使用新值来创建新元素,如果有可能,不要让函数或类拥有内部状态。

认知成本是代码可读性杀手

“认知冲突”是“需要同时记住两件(甚至更多)东西才能帮你理解事物”的另一种说法。同时记住不同的东西会给大脑增加负担,并且会削弱事物的相关性(因为你需要在脑子里保留更多的东西)。

例如,通过相加布尔类型来判断 True 值的个数就是一种轻微的认知冲突。假设有一个“sum()”函数,你一看就会知道这个函数是用来计算一个列表中所有数值的和。而我却见过有人用这个函数来计算一个布尔值列表中所有 True 值的个数,这样很让人感到困惑。

魔术数 7

魔术数 7 解释了人类在同一时间可以记住多少件东西。

如果你有一个函数,这个函数调用第二个函数,第二个函数又调用第三个函数,第三个函数又调用第四个函数,第四个函数又调用第五个函数,第五个函数又调用第六个函数,到最后你会发现这样的代码可读性很差。

或者更进一步,你拿到一个函数返回的结果,把它传给第二个函数,然后拿到第二个函数返回的结果,把它传给第三个函数,一直这样重复下去……

但问题是:

  1. 现如今,人们谈论更多的是魔术数 4,而不是 7;

  2. 考虑使用函数组合(先调用第一个函数,再调用第二个……)代替函数调用(第一个调用第二个,第二个调用第三个……)。

捷径虽好,但好处是短暂的

有很多编程语言、库或框架会帮你简化代码,减少输入。

但走捷径会在以后给你带来更多麻烦,甚至会让你不得不重新使用复杂的代码代替简单的代码。

所以,在采取捷径之前,要先了解它们。

你不一定要先写出复杂的代码,然后使用捷径来简化:你可以采取捷径,但一定要知道走捷径可能会导致什么后果,或者知道在不走捷径的情况下该如何实现代码。

抵制“简单”的诱惑

IDE 为我们提供了很多自动完成功能,让我们可以更容易地构建项目,但你知道背后都发生了什么吗?

你知道构建系统的工作原理吗?如果没有 IDE,你知道怎么构建项目吗?

如果不借助自动完成功能,你记得那些函数的名字吗?

所以,我们要对这些背后的东西保持一颗好奇心。

给日期带上时区

在处理日期时,记得带上时区。因为你的电脑或服务器的时区有可能不对,在排查问题时可能会因为接口返回错误的时区而浪费你很多时间。

总是使用 UTF-8

在进行字符编码时也会遇到与日期类似的问题。所以,总是把字符串转成 UTF8 格式,使用 UTF8 格式保存在数据库中,API 返回的字符串也使用 UTF8 格式。

极简主义

摆脱 IDE 可以从“极简主义”开始:只使用编译器和带有代码高亮功能的编辑器,并用它们构建和运行代码……

但其实这样做并不容易。不过当你再次使用 IDE 时,你就会知道按下那些按钮之后 IDE 会做些什么。

日志用于记录事件,不需要展现给用户

在很长一段时间内,我一直通过日志告诉用户系统发生了什么。

但其实我们可以使用标准输出告诉用户系统发生了什么事件,使用标准错误输出告诉用户系统发生了什么错误,然后使用日志记录事件,便于后续分析这些事件。

你可以把日志看成是以后需要从中抽取信息的数据,它们不是面向用户的,所以不一定非要人类可读的。

调试器被过度高估了

有很多人认为不带有调试功能的代码编辑器都不是好编辑器。

但是,代码一旦进入生产环境,你就用不了调试器了,即使是你最喜欢的 IDE 也用不上了,但日志却无处不在。在发生故障时你可能不知道是怎么回事,但你可以从日志中查找原因。

我并不是说调试器毫无用处,只是它不像大多数人认为得那样有用。

一定要使用版本控制系统

你可以说“我只是想使用这个项目来学点东西”,但它不能成为你不使用版本控制系统的理由。

如果你从一开始就使用版本控制系统,在出现问题之后可以很容易回滚。

一个变更对应一个提交

我经常看到代码提交里有这样的消息:“修复问题#1、#2 和#3”。除非这三个问题是重复的(其中两个应该是已关闭的),否则应该分成三次提交,每个提交对应一个问题。

如果代码改过头了,可以使用“git add -p”

Git 允许用户通过“-p”参数进行部分提交,也就是选择只提交部分代码变更,把剩下的留到后面再提交。

按照数据或类型来组织代码,而不是功能

很多项目的代码结构类似下面这样:

复制代码
+-- IncomingModels
| +-- DataTypeInterface
| +-- DataType1
| +-- DataType2
| +-- DataType3
+-- Filters
| +-- FilterInterface
| +-- FilterValidDataType2
+-- Processors
| +-- ProcessorInterface
| +-- ConvertDataType1ToDto1
| +-- ConvertDataType2ToDto2
+-- OutgoingModels
+-- DtoInterface
+-- Dto1
+-- Dto2

也就是说,他们是基于功能来组织代码的(所有模型放在同一个目录中,所有过滤器放在另一个目录中)。

这样做其实也没有什么问题,只是如果按照数据来组织代码的,那么在将项目拆分成小项目时就会更容易些。

复制代码
+-- Base
| +-- IncomingModels
| | +-- DataTypeInterface
| +-- Filters
| | +-- FilterInterface
| +-- Processors
| | +-- ProcessorInterface
| +-- OutgoingModels
| +-- DtoInterface
+-- Data1
{1}| +-- IncomingModels
| | +-- DataType1
| +-- Processors
| | +-- ConvertDataType1ToDto1
| +-- OutgoingModels
| +-- Dto1
...
{1}

这样你就可以独立出各个模块,比如只处理 Data1 的模块,或者只处理 Data2 的模块。

如果有另外一个项目需要处理 Data1,你就可以重用 Data1 模块了。

创建公共库

我见过很多项目使用同一个单独的大代码库,与其这样,为什么不把公共部分提取出来做成公共库,然后在各个项目里引用这些库呢?

学会使用监控

以前,为了了解系统的行为,我会往系统里添加很多度量指标。在习惯了这些之后,如果一个系统没有监控,我就会觉得很奇怪。只是通过发送简单的请求根本不足以判断一个系统是否健康。

尽早给系统加入监控可以让你更好地了解系统的行为。

使用配置文件

假设你写了一个函数,它只接受一个值作为参数。如果你有两个值需要分两次传给它,就需要调用这个函数两次。

你也可以使用配置文件,把这两个值分别写在两个配置文件里,然后运行这个程序两次。

命令行选项很有用

在将参数写到配置文件里之后,你还可以增加命令行参数,用来指定使用哪个配置文件。

有很多编程语言都提供了命令行参数解析器,可以用它们构建一个好用的命令行程序。

不只有函数组合,我们还有程序组合

Unix 的哲学是“一个程序只做一件事,而且做到极致”。

你可以使用一个程序和多个配置文件,但如你需要使用所有程序的运行结果,那该怎么办?

你可以再写一个程序,把多次运行结果合并起来变成一个。

程序组合也可以从简单的开始

程序组合模式到最后会变成微服务架构,而微服务架构要求服务之间具有良好的通信机制。

不过你也不用担心个问题,你可以让程序通过文件来通信,一个往文件里写,一个从文件里读。

在你了解了网络通信机制之后再去考虑其他更复杂的通信问题吧。

把代码优化工作留给编译器

你想要获得更好的代码性能,于是总想着在这里优化一点,在那里优化一点。

但你要知道,优化代码正是编译器的拿手好戏。聪明的编译器甚至会帮你把返回相同结果的代码移除掉。

你需要做的是如何更好地设计你的代码,而不是想方设法改进已有的代码。

延迟求值

在很早以前,一些编程语言会在表达式被用到时求值,而不是在它们出现时求值。

Lips 在很早以前就这么做了,现在有很多编程语言也这么做了。

关于团队协作

代码评审不是为了检查代码风格

在进行代码评审时,请把时间花在架构和设计问题上,不要对代码风格问题吹毛求疵。没有人会喜欢这样的代码评审:“这一行开头多了一些空格”、“括号里少了空格”……

代码格式化工具不能解决所有问题

为了避免在代码评审时讨论代码风格问题,有些团队在提交代码之前会使用格式化工具格式化代码。

但这样还有一个问题:我们是人,不是计算机,计算机能够读懂的东西不一定适合人类。格式化工具虽然为我们带来了某种程度的可读性,但不一定都是对的。

遵循代码风格

如果你的项目定义了一种代码风格,就必须遵循它。

C/C++ 只有一种代码风格:K&R

其他代码风格都是不对的。

Python 只有一种代码风格:PEP8

既然社区都在遵循 PEP8,你也应该这样做,这样你的代码就可以更容易与社区的代码集成。

显式胜于隐式

你知道到目前为止最糟糕的函数名是哪个吗?

是“sleep()”。

究竟要 sleep 多久?是以秒为单位还是以毫秒为单位?

虽然诸如“sleepForSecs”或者“sleepForMs”之类的函数名也算不上最好,但肯定比“sleep”好。

通才的职业生命线比专才长

如果你精通某一门编程语言,可以很容易地找到一份工作,但从长远来看,编程语言也会老去。如果你也懂其他编程语言,可以让你走得更远。

我一直在坚持一种编程原则:工作中使用的编程语言和工作之余使用的肯定不一样。我就是通过这种方式学会了很多东西:通过写 Rust 代码了解 Java 泛型原理;想要在 C++ 中实现依赖注入,所以对 Spring 也有了更深入的了解。

把“花了一个小时以上才解决的愚蠢错误”记录下来

有些错误花了你一个小时以上才得以解决,比如“忘了加依赖”、“忘了加注释”,把它们记录下来,这样以后不需要花那么多时间就能找到问题所在了。

关于个人

如果累了,就要停下来休息

如果代码写不下去了,就停下来。如果无法再往前走了,就停下来。不要逼自己往前走,那样只会让事情变得更糟。

有一次,我在犯了偏头痛的情况下继续写代码。第二天,偏头痛好了,但我不得不把之前写的代码全部重写,因为在偏头痛时写的代码简直就是一坨屎。

学会说不

有时候,你需要勇敢地说“不”。

有一次,我对我们的 CTO 说:“我可以做这件事,但我并不认同这么做”。结果呢?结果那个 App 因为我们不恰当的做法被禁了。

你要对自己的代码负责

要做到这点很难,非常难。

写出用来捕捉人脸或辨别种族的代码并没有错,但你要想一下,这些代码会被用在什么地方。

成长过程很艰辛

无法通过编译的代码会让我们感到挫败,用户的无理要求会让我们感到生气。

在遇到这些问题时,我们通常无法冷静,而冲动只会让我们自己陷入麻烦之中。

从麻烦中学习

你会感到挫败、生气、恼火……你会让自己陷入麻烦之中。你也会看到其他人因为这类事情招来大麻烦。

但你必须从麻烦中学习,不要轻易忽略它们。

我从挫折中学到的一件事是:当我感到沮丧时,就会变得很好斗。现在,当我发现自己开始感到沮丧时,就会向别人寻求帮助。你可以看到别人也会有同样的问题,不仅仅是你。

注意别人的反应

有时候,当我问别人一些事情时,他们的反应很激烈,就好像我在说他们的解决方案是错的一样。

这个时候我会补上一句:“我并不是说你们的方案是错的,我只是感到有点疑惑”。

这样你就不会惹上麻烦。

远离“有毒”的人

你会发现,有些人虽然不会对你“闲言碎语”,但会在公开场合对其他东西——包括对其他人——“碎碎念”。

请远离这样的人。

你根本不知道他们的这种态度会让你变得多么沮丧。

你总是要经历“英雄项目”

“英雄项目”是指为了解决一系列项目问题而提出的另一个项目、规范变更或框架。

你利用空余时间去做这些事情,可能只是为了证明自己的观点。

有时候,你会发现你的想法是错的。

不要把“英雄项目”和“英雄综合症”混淆起来了

我至少见过两次了:有人声称如果他不在场就什么事都成不了,或者他不需要别人的帮助就可以完成任何事情。

这就是所谓的“英雄综合症”——这个人是唯一一个能够解决所有问题的人。

但请你不要成为这样的人。

什么时候可以考虑离职?

当一些奇怪的事情导致你无法及时完成项目,而你的老板却无法理解你。

当你的同事一直对你进行“微攻击”。

当那个喜欢搞恶作剧的人无休止地说一些废话,还有那个患上”英雄综合症“的人……

这个时候,你可以考虑准备简历了,不管他们给你多少薪水,也不管项目有多好。

IT 这个圈子真的很小

IT 圈子真的很小。今天和你共事的人,在经历了几次工作更迭之后,可能会在 15 年之后又和你成为同事。

便利贴真的很有用

我曾经多次尝试”无纸化“,但到了最后发现有时候手头有一个笔记本和一支笔其实很管用。

在博客上分享想法比保持安静更好

你可能会觉得”我还没有准备好分享自己的想法“,或者”这个想法很蠢,我不应该把它分享出来“。

你其实可以通过博客把想法分享出来,虽然你觉得它很愚蠢,但可能会比其他人的好。

把评论功能关掉

有些不怀好意的人会在你的博文底下捣乱,他们可能会说”这个想法很愚蠢“。所以,把评论功能关掉,不要让这些人影响你。

把你不知道的东西记到清单里

著名的物理学家 Richard Feynman 喜欢把他不知道的东西记在本子里。

如果你发现了一些很酷的东西,并且想进一步了解它们,可以把它们记录下来。

一个30年老程序员的修炼之道

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论

最新评论

用户头像
提莫 2019 年 07 月 18 日 09:17 0 回复
打铁还需自生硬
用户头像
有人让我改个名 2019 年 07 月 12 日 09:39 0 回复
写的特别好,能转发吗
没有更多了