什么样的代码才是美的代码?一千个coders可能会给出一千个答案。今天,让我从一个简单的角度来谈谈对于代码之美的理解。
相信大家都有过这样的经历:接手一个项目要修复bug或者开发新功能的时候,发现代码可读性非常差。哪怕是在有说明文档的情况下,都不太敢提交代码,唯恐引入新的bug或者直接导致系统崩溃。
软件系统在代码方面的成本分为两部分,第一部分是开发成本,第二部分是维护成本。
开发成本很好理解,就是把一个系统开发出来需要支付的各种成本,其中最大一块很可能是人力成本。任何一个大型软件系统都不可能没有bug。系统开始运作之后,这些bug可能会导致业务出错、运行缓慢,甚至是宕机,需要程序员找出漏洞并且修复上线。在这一阶段的成本,我们称之为维护成本。
运维成本往往会超过开发成本。我毕业后的第一份工作是在一家ERP公司做二次开发,最早的代码注释时间有十几年前的,常年从事维护开发工作的人员超过十个人。开发需要的时间远比维护的时间要短得多,维护人员的数量并不会比开发人员的数量要少太多。
如果适当增加开发成本可以大幅减少维护成本,那么对节省整体成本有着极大的帮助。
在进入到互联网时代的今天,像ERP这样开发完成之后变化不大的“重”系统已经不多了,模块化、服务化的“轻”系统已经占到绝大多数。这些现代软件系统强调的是迭代开发,以月或者周为单位更新版本,可以说几乎永远不会进入到所谓的“维护”阶段。因此,如何平衡版本间的成本占比,是程序员和管理者必须要关心的主题。
《重构》里有这么一段话:“任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。”如果只是写出可以运行的代码,却不具备可读性或可读性很差,负责接手项目的下一位程序员就很难在原有的基础上再进行开发工作。这样做固然初始的开发成本会比较低,但是后续维护和再开发的成本就很容易变得不可接受地高。
哪怕有人抱着“我走后,哪管它洪水滔天”的想法,却仍要考虑更普遍的一种情况。在更多的时候,需要读懂这些糟糕的代码并且付出代价的人,往往就是几个月后已经忘记这段代码为什么要这么写的你自己。
可读性差的代码有很多特征,其中最典型的就是存在大量过长的函数。
过长的函数之所以会导致难以理解,主要是因为里面做了很多件事,而且容易导致抽象层次不一致。在完全陌生的情况下去读这些代码,我们经常会读着读着就不知道某几行代码是在做什么、为什么要这么做,往往要花很大的功夫去蒙去猜。
刚入门的程序员很容易问出这样的问题:“到底多少行代码的函数才算过长呢?”我们可以从Martin Fowler那里得到答案,“关键不在于函数的长度,而在于函数‘做什么’和‘如何做’之间的语义距离”。这句话换个角度理解,首先你要确保你的函数做完且只做一件事,然后这个函数里的代码都只为这件事服务。
如果函数已经过长了应该怎么办?不要犹豫,立刻切分它。我们先要读出面前这个过长函数到底做了多少件事,然后逐一地按照事务来提取代码,以子函数的形式对其进行抽象。
有人可能会担心,生怕这样会导致一个类里面的函数过多,会导致其他不良后果。这种忧虑其实毫无必要,因为实际经验告诉我们:只要切分是恰当的,不但不会有坏处,还会有除了提升代码可读性的其他好处。
其中一个最明显的好处就是可复用性会增强。一段代码如果没有提取出来成为独立的函数,其他地方如果要想用到就必须复制粘贴,这不但体现不出代码可复用的好处,还会引发代码一个新的坏味道:重复代码。
对于注释,我们的第一印象往往是正面的。注释并不是代码,对软件系统本身并不会起任何作用。它是为了让读源代码的人更好地理解原作者的意图而存在的,因此我们在阅读一段很难理解的代码的时候总是希望能有注释。
正式参加工作之后,我们对注释的看法可能会发生一些转变。首先,有些人的注释写得很糟糕,表达出来的意思常常让人云里雾里,甚至还不如不看这些注释。然后,有些人的代码写得很糟糕,注释虽然有所帮助,但是这就成了程序员逃避被要求写出整洁代码的法宝。更何况,还有一部分人的代码和注释都写得很糟糕。
另外,注释也有维护成本。注释依附于代码,所以如果代码有所改动,注释也要相应有所更新,这里就产生了成本。如果更新不及时或者没有更新,注释就会过时,产生误导代码阅读者的风险。
因此,除了涉及到比较复杂的算法或者不得不特别加以说明的约定等情况,我并不建议优先使用注释。命名良好的独立函数应该成为优秀程序员的第一选择。
如果你在阅读自己写的或者是需要重构的代码时,发现某一个地方需要用注释来说明,这就表明这里很有可能应该提取出成一个独立函数。新的独立函数需要有一个良好的命名,要做到让人可以轻易地“望文生义”,这才是最好的“注释”。
注释不是代码,要想提升代码可读性,最好的途径还是应该通过代码本身去实现。
无论我们怎么努力,也很难一下子就写出可读性很强的代码。这就像写文章一样,我们的大部分精力都放在表达思想上面,文从字顺有的时候就不太顾得上。写代码,第一要务是能运行,能实现软件系统的功能。
《代码整洁之道》的作者写道:“我没指望你能够一次过写出整洁、漂亮的程序。如果说我们从过去几十年里面学到什么东西的话,那就是编程是一种技艺甚于科学的东西。要编写整洁代码,必须先写肮脏的代码,然后再清理它。”
很多人写出了可以运行的、“肮脏”的代码,或者说接手了一个可读性比较差的系统,往往不愿意去重构它们。他们的理由看上去是十分充分的,那就是容易引入新bug。业界有名言可以作为依据:如果没坏,就别去修它(If ain’t broke, don’t fix it)。
我最近才犯了一个错误:在没有经过测试的情况下,把一段重构过的代码上线到生产环境。结果就是导致一个重要功能不可用,最终要通过紧急版本进行修复。
事后反省时,我并没有赌咒发誓以后再也不重构代码,而是在重构之外加上一个必要的环节——测试。
《重构》一书开头便写道:“每当我要进行重构的时候,第一个步骤永远相同:我得为即将修改的代码建立一组可靠的测试环境。这些测试是必要的,因为尽管遵循重构手法可以使我避免绝大多数引入bug的情形,但我毕竟是人,毕竟有可能犯错。所以我需要可靠的测试。”
测试有很多种类型,其中单元测试和功能测试最为常用。对于Java程序员,我们可以使用JUnit等测试框架去写单元测试,往往可以避免相当一部分引入bug的情形。然后,针对不同的业务场景,我们还需要做一些简单的功能测试。
重构和测试当然都需要成本,还要承担不可能完全避免的引入bug的风险。但是我相信,为了提升代码的质量,为了降低维护和后续开发的成本,这些都是值得付出的代价。
对于代码的理解,不同人能到达不同的层次。刚开始学习,停留在行级。入门之后,进步到方法级。工作之后,可以到达类级和模块级。经验积累足够多了,会精进到架构级,甚至是更高的级别。
但是无论如何,任何进步都是从一点一滴的努力中得来的。如果你自我评价还没有到达很高的级别,那就不妨一起来尝试学习和实践重构,让经过自己手的代码变得更美。
《重构》里有一段话非常有启发性:“一开始我所做的重构都像这样停留在细枝末节上。随着代码渐趋简洁,我发现自己可以看到一些以前看不到的设计层面的东西。如果不对代码做这些修改,也许我永远看不见它们,因为我的聪明才智不足以在脑子里把这一切都想象出来。Ralph Johnson把这种‘早期重构’描述成‘擦掉窗户上的污垢,使你看得更远’。研究代码时我发现,重构把我带到更高的理解层次上。如果没有重构,我达不到这种层次。”
技术的精进要靠什么?有的人说是天赋,这是老天爷赏饭吃,一般人都达不到。有的人说是经验,一定要有多个大型项目的积累才有可能成为专家。有的人说是学习,必须把原理研究得很深很透才能把握技术的本质。然而,这些都不是一个脚踏实地的好答案。
技术的精进,靠的应该是一个又一个经过时间检验的、得到业界认可的工具。从业人员通过使用这些工具优化自己的工作输出,日积月累,最终成为各自领域的佼佼者。重构,就是这么一个经典的、优秀的、实用的工具。
学习重构、实践重构、掌握重构,能够帮助我们成为更好的coder。