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

2019-07-09 15:08 稿源:InfoQ公众号  0条评论

黑客2

声明:本文来自于微信公众号InfoQ(ID:infoqchina),作者: Julio Biason,授权站长之家转载发布。

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

 1 关于软件开发

规范先行,然后才是代码  

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

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

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

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

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

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

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

用好 Gherkin  

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

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

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

测试让 API 变得更健壮  

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

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

通过命令行运行测试用例  

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

做好丢弃代码的准备  

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

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

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

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

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

想得太长远是一种浪费  

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

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

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

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

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

文档就是契约  

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

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

如果一个函数出现“和”逻辑,那一定有问题

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

不要将布尔类型作为参数  

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

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

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

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

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

在修改接口时要小心  

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

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

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

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

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

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

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

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

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

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

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

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

    image.png

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

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

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

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

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

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

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

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

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

      image.png

      如果数据有模式,用结构来保持数据模式  

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

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

      停止盲目跟风  

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

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

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

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

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

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

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

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

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

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

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

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

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

      数据流胜过设计模式  

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

      设计模式是描述解决方案,不是解决方案

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

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

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

      学会基本的函数式编程  

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

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

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

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

      魔术数 7  

      魔术数7(https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus_or_Minus_Two)解释了人类在同一时间可以记住多少件东西。

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

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

      但问题是:

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

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

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

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

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

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

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

      抵制“简单”的诱惑  

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

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

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

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

      给日期带上时区  

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

      总是使用 UTF-8  

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

      极简主义  

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

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

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

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

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

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

      调试器被过度高估了  

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

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

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

      一定要使用版本控制系统   

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

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

      一个变更对应一个提交  

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

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

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

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

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

        image.png

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

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

          image.png

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

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

          创建公共库  

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

          学会使用监控  

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

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

          使用配置文件  

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

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

          命令行选项很有用  

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

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

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

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

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

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

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

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

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

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

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

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

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

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

          延迟求值  

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

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

          声明:本文转载自第三方媒体,如需转载,请联系版权方授权转载。协助申请

          相关文章

          相关热点

          查看更多