为何你的代码如此难以理解

640?wx_fmt=png&wxfrom=5&wx_lazy=1

图片源自:电影《降临》

“我到底在想些什么?!?”

 

凌晨1:30分,我正盯着我不到一个月前写的一段代码。当时一看简直是件艺术品!不但完全说得通,而且优雅、简单、令人叹为观止。而此时再看到这段代码的时候,却和当初的情况完全不同。明天就到截止日期了,而我在刚才还发现了一个bug。现在再一看,当时所认为的简单和逻辑也都说不通了。可以肯定的是,这段代码是我写的,那么我应该有足以能够理解这段代码的智慧吧?

 

然而这种情况我已经经历过不止一次了,于是我便开始认真地去思考一个问题,那就是:为什么我的代码在我开始编写的时候很清楚,而当我几周或几个月后回过头再来看的时候,它们却变得如此让人费解。

 

640?wx_fmt=gif&wxfrom=5&wx_lazy=1

留意下:这图会动

问题1,过度复杂的心智模型

想要理解为什么当你休息了一段时间后,再回头看自己写的代码,会令你感到费解的原因。首先第一步,是要理解心智模型的问题。你写的所有代码,几乎都是在试图解决现实世界的问题。在你写代码之前,你需要理解你所要试图解决的问题。这往往也是编程里最难的一步。

 

为了解决现实世界的问题,我们首先需要形成该问题的心智模型 ,并以此作为你的编程目的。接下来你需要形成实现编程目的的方案模型,我们姑且称为语义模型(semantic model)。不要混淆你的编程目的和此目的的解决方案。我们倾向于主要考虑解决方案方面的问题,而常常忽略构建目的的模型。

 

你下一步是尽可能地构建最简单的语义模型,这是第二容易搞错的事情。如果你不花时间去真正理解你将要解决的问题,那么你会在写代码时在模型这一块跌倒。另一方面,如果你真的考虑过你将要尽力做的事情的话,你通常只是想到一个十分简单的模型,但这足以帮你达到你最初的目的。

 

如果您想要易于维护的简单代码,那么尽可能消除会出现意外的复杂性尤为重要。一般我们试图解决的问题都很复杂。如果你不需要,那就不要增加。

 

问题2,语义模型到代码的糟糕转化

一旦你形成了最好的语义模型,你就可以将其转化为代码了。我们称之为句法模型(syntactic model)。你接下来就是将语义模型的含义转换成计算机能够理解的语法。

 

如果你有个非常不错的语义模型,但在转化为代码时搞砸了,那么你在之后某个时间段再回头修改代码时,你将会比较痛苦。当你脑子里还有语义模型时,把你的代码映射到语义模型上还是会比较容易的。例如,此时你回忆起变量“x”实际上代表一条记录被创建的日期、而“y”代码记录被删除的日期,这并非难事儿。然而当你3个月后脑海中已经没有这个语义模型,再回过头来看这段代码时,就很难理解同样的变量名了。

 

把语义模型转化为句法的任务就是尽可能多地留下线索,这将会让你在今后回查时,能够重建起当初的语义模型。

 

所以,你知道该怎么做了吧?

 

类结构和命名

如果你在使用面向对象语言,请尽量让你的类结构和命名靠近语义模型。领域驱动设计(Domain Driven Design) ❷ 是一种非常重视这种实践的运动。即使你对DDD方法并不买账,你也应当仔细考虑类结构和命名。每个类都是你留给自己和其他人的一条线索,它会有助于你在将来返回时重建你的心智模型。

 

变量、参数和方法命名

尽量避免普通的变量和方法命名。当“PaySalesCommision”更有意义时,就不要把方法命名为“Process”;当它应当是“currentContract”时,就不要把变量命名为“x”;当“outstandingInvoices“更好时,就不要把参数命名为“input”。

 

单一功能原则(Single responsibility principle,简称SRP)

SRP ❸ 是面对对象设计原则的核心之一,关联着好的类和变量命名。它认为,任何类或方法都应该完成一个单一的功能,只能是一个单一的功能。如果你想给类和方法一个有意义的名字,那么它们需要有一个唯一的明确的目的。如果一个单一类从数据库中读取和写入、计算销售税、通知交易客户并生成账单,那么你就可能无法给出一个合适的名字。我经常停滞在重构一个类上,因为我很难给它一个足够短的名字来描述它所做的一切。为了更多地讨论SRP和其它面向对象原则,可以参考我的博文《面向对象设计》。关键词排名查询工具

 

适当的注释

如果你因为某些原因需要去做某件事,而此时还不能让代码变得清晰,为了不让你将来留下遗憾,那就留下个注释来说明你为什么不得不那样做。注释往往很快就会过时,所以我宁可让代码自描述,注释用来说明为什么你不得不那样做,而不是它如何做。

 

问题3,没有足够的组块

在心理学中,组块被定义为单个实体的信息分组。那么,这该如何应用到编程上呢?作为一名开发者,在你积累经验时,你会开始看到重复的模式在你的解决方案中反复出现。极具高度影响力的设计模式:《可重用的面向对象软件》(Elements of Reusable Object-Oriented Software)是第一本整理和解释一些模式的书。尽管如此,组块不仅仅用在设计模式和面向对象。在函数式编程(FP)里,存在大量的众所周知的标准函数,它们具有相同的用途。算法是组块的另一种形式(稍后会详细介绍)。

 

当你使用组块(设计模式、算法和标准函数)时,它会让你停下来思考,你编写的代码是如何运行的,而不是考虑它做了什么。这缩短了你的语义模型(你的代码)和句法模型(你脑中的模型)的距离。这个距离越短,在后期你就越容易重建你的心智模型。

 

640?wx_fmt=jpeg

图片源自:电影《降临》

 

问题4,费解的用法

目前,我们主要讨论了如何结构化你的类、方法和变量命名。心智模型的另一个重要部分是理解这些方法是如何被使用的。再次强调,当你最初形成心智模型时,这是很清楚的。当你后来返回时,就非常难以重建你的类和方法的所有有意图的用法了。通常这是因为不同的用法散布在你的程序其它地方。有时甚至跨许多不同的项目。

 

我就是在这种情况下发现测试用例是非常有用的。除了相应地知道一个修改是否破坏了代码的明显好处之外,测试还为你的代码提供了一整套的示例用例。你不必搜遍上百个文件,只需通过查看测试就能得到引用的全景。

 

注意为了达到这个目的,您需要有一组完整的测试用例。如果你的测试仅仅覆盖了一部分,而你认为测试是完整的,那么你之后将陷入困境。

 

问题5,不同模型之间没有清晰的路径

通常,你的代码从技术角度看,常常是非常好、非常优雅的,但是从程序意图到语义模型、再到代码会存在非常不自然的跳跃。考虑你选择的一堆模型的透明性是很重要的。从程序意图到语义模型,再到代码的过程需要尽可能平滑。你应该通过每个模型看到对应问题的每个模型的各个方面。多数情况下,最好选择一个特定的类结构或算法,而不是孤立地考虑它的优雅,而是能够连接各种模型下,并为重构意图留下一条自然的路径。当你从抽象的编程意图走到具体的代码时,你所做的选择应该被清晰的代码所驱动,这样你就可以在接下来表现出更抽象的模型。

 

问题6,发明算法

作为程序员,我们经常认为,我们为了解决问题而发明着算法。但这几乎是不可能的。在多数情况下,已经有现成的算法可以被组合在一起解决你的问题了。像最短路径搜索法、字符串相似度算法、粒子群算法等。大部分编程是以正确的组合、选择现存算法来解决你的问题。如果你正在发明新算法,那么,要么你不知道合适的算法、要么你正忙于你的博士论文。

 

640?wx_fmt=jpeg

图片源自:电影《降临》

最后总结

作为一名程序员,你的目标是构建一个最简单的语义模型来解决你的问题。将语义模型尽可能地转换为一个句法模型(代码),并提供尽可能多的线索,便于你之后无论哪个人看你的代码,都能重建像你最初脑子里的、相同的语义模型。

 

设想一下,当你走过被你的代码照亮的那片丛林时,也要记得在你的身后留下面包屑。相信我,当你需要找到回去的路时,丛林仍将充满着黑暗、朦胧和不详。

 

这听起来很简单,实际做起来是很难的。

posted @ 2018-08-28 17:52  万象更新新  阅读(140)  评论(0编辑  收藏  举报