“千帆过尽仍少年”,对于程序员来说,保留技术初心、不断提升实力是夯实自己的不二法则。而本文的作者,作为一名有着三十多年开发经验的“老”程序员,就在本文中详细总结了自己这些年踩过的坑和实践得出的真理,谈到了包括软件开发、团队工作、个人成长等方方面面。相信阅读本文后,会帮助你成为更优秀的程序员。
声明:本文已获作者JulioBiason翻译授权。
作者
JulioBiason
译者
王艳妮,责编
郭芮
以下为译文:
这是我30年来从事软件开发过程中所学到的一些实际经验,可能有些听起来愤世嫉俗,但都是我的切身经验之谈。
再次强调,有些内容真的是愤世嫉俗,有些则是对不同工作岗位的长期观察。
软件开发
先明确问题,再开始写代码
如果你不知道你想要解决的问题是什么,那你肯定就不知道要写些什么代码。在编写任何代码之前,先明确地把应用程序是如何工作的写下来。
“如果没有需求或设计,编程就是向空文本文件不断增加bug的艺术。”——LouisSrygley
有时,即使只是“电梯演讲”(指短时间内表述结果内容)那么长——用仅仅两个自然段来描述这个应用程序的功能——也足够了。
有时候我看着自己的代码发呆,不知道下一步该怎么做,其实往往是因为下一步本来就还没有被定义出来。一般出现这种情况,就意味着是时候停下来,与同事们讨论一下了——或者重新考虑解决方案。
将步骤写为注释
如果你不知道如何开始,请先用自然语言、英语或你的母语描述应用程序的流程,然后用代码填充注释之间的空白。比这更好的做法是:将每个注释视为一个函数,然后编写出能完全实现其功能的代码。
Gherkin是帮助你了解期望(expectation)的好帮手
Gherkin是一种测试描述格式,它指出“鉴于系统处于特定状态,当发生某些事情时,这是预期的后果”。即使你不使用任何能读取Gherkin的测试工具,它也会让你很好地理解应用程序的预期效果。
单元测试很好,集成测试更好
在我目前的工作中,我们只测试模块和类(例如,我们只为视图层编写测试,然后仅测试控制器层,依此类推)。它能让我们了解到某一部分有没有出错,但缺乏对整体的观察——而集成测试测试了整个系统的行为,在这方面会表现得更好。
测试可以让API更好
我们在不同层次中编码:有一个存储层,应该使我们的数据永久存储;有一个处理层,应该对存储的数据进行一些转换;有一个视图层,它有关于数据必须如何被展示出来的信息......等等。
正如我所提到的,集成测试感觉更好,但是单独测试不同层可以让你更好地了解其API。然后你可以更好地了解如何调用东西:API是否太复杂了?是否需要保留大量数据才能进行一次调用?
做你知道如何在命令行上运行的测试
也不是说命令行对于任何项目都很重要,但是当你知道运行测试的命令时,你就知道如何让测试的执行自动化起来,然后你可以在一个连续的集成工具中使用这些测试。
时刻准备好扔掉你的代码
很多人在刚开始使用TDD(测试驱动开发,Test-DrivenDevelopment)时,一旦被告知他们可能不得不重写很多东西,就会变得很生气。
TDD“旨在”扔掉代码:越了解你的问题,那么你就会越明白,无论你写了什么,从长远来看都无法解决问题。
所以你不应该担心这个。你的代码不是一面墙:如果你必须永远抛弃它,那也不是白白浪费了材料。当然这意味着你编写代码的时间一去不复返了,但是你现在对这个问题有了更好的理解。
好的语言生来带有综合测试
可以肯定的是,如果一种语言在其标准库中自带一个测试框架——即使小得不能再小——那么与没有测试框架的语言相比,它周围的生态系统仍将拥有更好的测试,无论该语言的外部测试框架有多好。
未来思路是垃圾思路
当开发人员试图解决问题时,他们有时会试图找到一种方法来一下解决所有问题,包括未来可能出现的问题。
但现实就是这样:未来的问题永远不会到来,你最终要么必须维护一堆永远不会被完全使用的庞大代码,要么得整个重新写,因为有一大堆屁用没有的东西......
解决你现在遇到的问题,然后解决下一个,然后再下一个。直到有一天,你会发现这些解决方案中显现出了一种固定的模式,然后你才能真正地“一次性解决所有问题”。
文档是写给未来自己的情书
我们都知道,为函数、类(class)和模块编写该死的文档是一个痛苦的过程。但是以后当你看到文档就能回想起来当时你编写函数时的思路,你就会明白将来文档能在关键时刻救你一命。
功能文档是份合同
当你以编写文档作为自己编程工作的起始点时,你实际上是在签订合同(可能是跟未来的自己):我说了这个函数要做这件事情,那么它就必须做这件事情。如果稍后你发现代码与文档不匹配,那你就是代码出了问题,而不是文档出了问题。
如果一个函数的描述包含“和”,这就是不对的
一个函数应该且仅应该做一件事,真的。当你编写函数文档并发现你写了“和”这个字的时候,这意味着该函数不仅仅是做一件事。那么就需要将该函数分解为两个独立函数并删除“和”。
不要使用布尔型变量作为参数
当你设计一个函数时,你可能会想要添加一个flag——不要这样做。
现在,让我给你举个栗子:假设你有一个消息传递系统,并且有一个函数可以将所有消息返回给用户,称为getUserMessages。但有一种情况是需要返回每条消息的摘要(例如,第一段)或完整消息,因此,你添加一个名为retrieveFullMessage的flag/布尔参数。
再说一次,不要这样做。
因为任何读你代码的人都会看到getUserMessage(userId,true)并想知道这里的true到底是个什么意思。
你可以将函数重命名为getUserMessageSummaries并使用另一个getUserMessagesFull或类似的东西,但每个函数只调用原始的getUserMessage为true或false——但是类/模块外部的接口仍然是清晰的。
但是一定“不要”在函数中添加flags/Boolean作为参数。
注意界面的变化
在上面几点中,我提到了重新命名函数的问题,如果你能控制使用该函数的整个源头,那就不算是问题,只需要搜索和替换即可。
但是,如果该函数实际上是由库公开的,那么你不能随便地更改函数名称。这将打破你无法控制的许多其他应用程序,并惹恼其他人。
你可以通过文档或某些代码功能创建新函数并将当前函数标记为已弃用,然后,经过几次释放后,你终于可以Kill掉原来的函数了。
(你可以做的一个有些混蛋的举动是创建新函数,将当前函数标记为已弃用,并在函数开头添加一个休眠,这样一来使用旧函数的人会被迫更新。)
好的语言自带集成的文档
如果语言有自己的方式来记录函数、类、模块或其他,而且带有一个哪怕最简单的文档生成器,你就可以确切知道所有的函数、类、模块、库、框架都具有良好的文档了(不是说一定特别好,但至少是比较好的)。
大多数情况下,没有集成文档的语言,文档方面做得都不怎么样。
一门语言绝不仅仅是一门语言而已
编程语言就是你写的、而且能做事情的东西,但在特殊关键词以外它还有很多别的东西:它有一个构建系统,它有一个依赖控制系统,它有一种使工具/库/框架互动的方式,它有一个社区,它有一种与人打交道的方式。
不要仅仅因为一种语言容易使用就选择它。永远记住,你可能因为一种语言的语法很简明而支持这种语言,但是与此同时你也是在支持维护人员对待这个社区的方式。
有时候,宁愿让应用程序崩溃也不要什么都不做
虽然这听起来很奇怪,但即使在处理过程中添加了某些错误,也不要默默地捕捉到错误但什么都不做。
Java中一个可悲的常见模式是:
try{something_that_can_raise_exception()}catch(Exceptionex){System.out.println(ex);}
这看起来跟处理异常没有什么关系——除了重复了一遍,仅此而已。
如果你不知道如何处理它,那就随它去吧,你早晚会知道它会发生什么。
如果你知道如何处理该问题,那么就处理它
与前一点相反:如果你知道什么东西在何时会导致异常/错误/某种结果,并且知道如何处理它,那么就请处理它吧。显示错误信息,尝试将数据保存在其他位置,将日志文件中用户的输入捕获到以便以后处理,但要记得处理它。
类型决定你的数据是个什么东西
内存中只是一串字节序列;字节只是0到之间的数字组合;这些数字的真正含义取决于语言的类型系统。
例如,在C中,值为65的char型变量可能是字母“A”,值为65的int型变量是数字65——处理数据时请不要忘记这一点。这也是为什么大多数人在用布尔型变量做加法以查看True的数量时经常出错。
现在,让我展示一下我最近看到的一个JavaScript里的例子:
console.log(true+true===2);trueconsole.log(true===1);false
如果你的数据具有模式(schema),请使用结构(structure)来保留它
你可能会想要使用列表(或元组,如果你用的语言允许的话)来保存数据,如果它很简单——比如说,只有2个字段。
但是如果你的数据有一个模式(schema),有一个固定的格式——你应该每次都使用一些结构来保存它,不管是用struct还是class。
理解并保持cargocult的方式
“Cargocult”是一种理念,如果其他人那样做了,那么我们也可以。大多数情况下,cargocult只是对一个问题的偷懒的解决方法:
“如果X这样做了,我们为什么要考虑如何正确存储我们的用户数据?”“如果有某巨头公司是这样存储数据的,那么我们也可以”。“如果有某巨头公司支持这种做法,那就说明这种方法很好。”......
不要管所谓的“合适的生产力工具”,你只需要尽力去push进程
“合适的生产力工具”其实意味着:对于某件事情,有一个正确的工具和一个错误的工具——例如,应该使用另外的某种语言/框架而不是当前的语言/框架。但每当我听到有人提到这个词时,他们都是在试图推销他们喜欢的语言/框架,而不是合适的语言/框架。
“正确的工具”比你想象的更明显
也许你当前的项目需要处理一些文本,也许你很想说“让我们用Perl吧!”,因为你知道Perl在处理文本时非常强大。
但你漏掉了哪一点呢?你在一个C的团队工作,每个人会C,而不是Perl。
当然,如果它是一个小的、“放在角落”的不起眼的项目,那么用Perl就可以了——但如果它对公司很重要,那么最好还是用C。
PS:你的英雄项目(本文稍后将详细介绍)可能因此而失败。
不要跟你项目之外的事情纠缠
有时人们会试图改变外部库/框架,而不是使用适当的扩展工具——例如,直接对WordPress或Django进行更改。
这样很容易让你的项目秒瘫痪,变得无法维护。一旦发布了新版本,你就必须与主项目保持同步,并且很快就会发现改动不再适用,你将把外部项目留在一个旧版本中,且充满了安全漏洞。
数据流动比模式更重要
(再次说明,这仅仅是个人意见)当你了解数据如何在代码中流动时,你的代码质量就会更上一层楼,这比无脑应用一堆设计设计模式(designpattern)好多了。
设计模式是用来描述解决方案的,但它不能找到解决方案
(同样,个人观点)大多数时候我看到设计模式被应用的时候,它们被用作寻找解决方案的一种方式,所以你最终会扭曲一个解决方案——有时候,甚至是扭曲问题本身——来适应某个设计模式。
首先,解决你的问题,找到一个好的解决方案,然后你可以检查模式,以提供如何命名该解决方案的思路。
我经常看到这种情况发生:我们有这个问题;一个设计模式接近正确的解决方案;让我们使用这个设计模式吧;现在我们需要在适当的解决方案基础上添加很多东西以适应这个设计模式......
学习函数式编程的基础知识
你不需要彻底搞懂“什么是一个单子(monad)?”和“这是一个函子(functor)?”等问题,但要知道不能一直改变数据——使用新值创建一个新元素(将数据视为不可变),并尽可能使函数/类不保留某些内部状态(纯函数/类)。
认知成本是可读性的杀手
“认知失调”是一种高大上的说法,但其实意思就是“我需要同时记住两个(或更多)不同的东西才能理解这一点。”把这些不同的东西保留在你的头脑中会产生成本,并且事物之间关联性越小,这种成本就越会不断积累(因为你必须把所有这些都记在脑子里)。
例如,将布尔值相加以计算True的数量就是一种轻微的认知不协调;如果你正在阅读一段代码并看到一个sum()函数,你知道它是列表中所有数字的总和,你就预料到列表由数字组成,但我看到过人们使用sum函数计算布尔值列表中的True的数量,这也太特么容易让人糊涂了吧。
MagicalNumber7,正负二(7+-2的范围内)
“magicalnumber”是一篇心理学文章中提到的概念,意思指人们可以在同一时间记住的事物的数量。如果你有一个函数,它调用一个调用函数的函数,该函数又调用一个调用函数的函数……再往下说下去你可能要疯。
想一想:我会得到这个函数的结果,然后将它传递给第二个函数,得到它的结果,然后传递给第三个函数。但是:
当今,心理学家更多地谈论MagicalNumber4,而不是7。把函数当成写作文(如“我将调用该函数,然后该函数,然后该函数......”),而不是函数作为主体(如“该函数将调用该函数,将调用该函数......”)。走捷径挺nice的,但只是在短期内如此
许多语言、库、框架都有缩短工作时间的方法,减少了需要你打字输入的内容。但是,稍后,这些东西会让你栽跟头,你将不得不弃用这些捷径并懂得人间正道是沧桑的道理。
因此,在使用之前,先了解那些捷径是如何做事情的。
你不需要先用难的方式写东西然后再用捷径的方式清理:你只需要走捷径在后台做事情,所以你至少知道使用它可能出错的地方在哪里,以及如何用非捷径方式替换它。
抵制“轻松”的诱惑
当然IDE会帮助你完成大量的自动填充并让你轻松构建你的项目,但是你明白发生了什么吗?你了解你的构建系统是如何工作的吗?如果你必须在没有IDE的情况下运行你的程序,你知道该怎么做吗?如果没有自动填充你能记住你的函数名吗?是不是有办法打破/重命名一些东西让它们更容易被理解?......
要对窗帘后面的东西保持好奇。
总是在你的日期中使用时区
处理日期时请始终添加时区。你的计算机时区和生产服务器时区(或其中一个实例时区)将始终存在问题,你将花大量时间来调试为什么界面总是显示错误的时间。
总是使用UTF-8
在日期上出现的问题,也将出现在对字符的编码上。所以时刻记得将你的字符串转换为UTF8,将它们作为UTF8保存在数据库中,在你的API上返回UTF8。
你可以转换为任何其他编码方式,但UTF8赢得了编码战争,因此更容易保持这种方式。
从笨办法开始
远离IDE的一种方法是“从笨办法开始”:只需获取编译器并获得一个带有代码突出显示的编辑器(任何编辑器),做你该做的事情:写代码,构建它,运行它。
不,这并不容易。但是当你跳进某个IDE时,你看到某个按钮只会简单地想,“是的,它会运行它”(顺便说一下,这正是IDE所做的)。
日志用于事件,而不是用户界面
很长一段时间,我使用日志向用户显示正在发生的事情——因为,你知道的,使用单个东西会更容易一些。
使用标准输出通知用户发生了什么事件,使用标准错误通知用户有关错误的信息,但使用日志来捕捉可以在日后轻松处理的东西。
将日志想成是你必须解析以便在那时从中提取一些信息的东西,而不是用户界面,它不一定要是让人看得懂的明文。
Debugger们被高估了
我常常听到很多人抱怨,不能Debug的编辑器有多糟糕。
但是当你的代码投入生产时,你无法运行你喜欢的Debugger;哎呀,你甚至不能运行自己喜欢的IDE;但是logging......logging随处都可以运行......你可能在崩溃时没有所需的信息(例如,不同的日志记录级别),但你可以启用日志记录以便稍后找出某些内容。
不是说Debugger们很糟糕,只是它们没有大多数人想象的那么有用。
始终使用版本控制系统
“这只是个随便写的破程序,我只想学点东西”——这不是一个不使用版本控制系统的好借口。如果你从一开始就使用版本控制系统,那么当你做了一些傻事时,撤销会更容易。
每次提交一个更改
我见过人们编写提交消息,如“修复了问题#1,#2和#3”。除非所有这些问题都是重复的——那么其中两个应该已经不存在——它们应该分三次提交,而不是一次。
尝试在每次提交中只进行一项更改(并且这里的更改并不是“一个文件更改”;如果一个更改需要更改三个文件,你应该将这三个文件一起提交。想想“如果我还原这一步,那是什么消失了?“)
当你过度交换时,“gitadd-p”是你的朋友
(仅限Git的主题)Git允许使用“-p”部分合并文件,这允许你仅选择相关更改并不管其他更改——可能是为了新的一项提交。
按数据/类型组织项目,而不是功能
大多数项目的组织如下:
.+--In