编写代码
如何命名
我在工作中接触的第一项任务是开发一款ReactUI。当时我们拥有一个主组件,用于容纳其它所有组件。我喜欢在代码当中加点幽默元素,所以我把它命名为GodComponent。但在代码审查时,我才意识到为什么命名工作如此重要、也如此困难。
计算机科学领域有两大难题:缓存失效、命名以及缓冲溢出错误。-——LeonBambrick
我命名的每一段代码都包含隐藏的含义。GodComponent?这个组件的含义,就是我会把所有不知道该放在哪的组件都放在这里。它囊括一切。如果我把它命名为LayoutComponent,后续我才会意识到它的作用就是布局分配,其中不包含任何状态。
我发现的另一项心得在于:如果其体积过于庞大,就像是这里提到的包含大量业务逻辑的LayoutComponent,那么我就会意识到是时候进行重构了,因为通过名称就能看出业务逻辑并不属于这里。但使用GodComponent这个名称,我们无法判断业务逻辑出现在这里是否正常。如何命名集群?最好是在运行了服务之后再对集群进行命名,而后根据运行内容的变化重新调整名称。最终,我们用自己的团队名称完成了集群命名。
函数命名的情况也是一样。doEverything()这个名字就不怎么样,其会带来严重的后果。如果这项函数能够完成所有操作,那么我们将很难测试函数当中的某些特定部分。而且无论这个函数有多大,我们都会觉得很正常,毕竟它的名字可是叫“everything”。所以,最好的办法当然是更换名称,进行重构。
但是,我们在命名中也要考虑到另一类问题。如果名称的含义太过具体并忽略了某些细微差别,该怎么办?例如,在SQLAlchemy当中调用session.close()时,关闭会话不会关闭基础数据库连接。(我本应该跳出手册限制,对这项bug进行处理,具体情况将在调试部分进一步说明。)在这种情况下,我们可以考虑x,y,z这样的名称,而非count(),close(),insertIntoDB(),从而避免为其分配隐含的意义。太过具体,会迫使我们不得不在后续维护时费力检查这些函数到底是用来干嘛的。
最后,当时的我从来没想到命名会成为值得单独一提的重要工作。
遗留代码与下一位开发者
大家有没有面对一段代码时,感觉摸不着头脑?他们为什么要这么写?这完全说不通啊。
我就“有幸”接手过遗留代码库。其中就存在类似于“跟穆罕默德确认过情况之后,取消注释”这类说明。这话是谁说的?穆罕默德又是哪位?
在这方面,我们不妨做个角色转换——考虑下一位接手我所编写代码的开发者。他们同样会发现我的代码非常奇怪。同行评审能够很好地解决这个问题。这不禁让我想到上下文原则,即:了解团队开展工作时的实际处境。
如果我跑去忙别的事,稍后又回来,我可能也无法重新建立这种上下文。我坐说,“当时我是怎么想的?这根本没道理……哦等等,我原来是这么干的。”
正是为了实现这种提示作用,文档与代码注释才会如此重要。
文档与代码注释
文档与代码注释的意义,在于保持上下文并分享知识。
正如Li在如何构建良好软件中所言,“软件的主要价值并不在于生成的代码,而在于生成代码的过程中开发者所积累下来的知识。”
“软件的主要价值并不在于生成的代码,而在于生成代码的过程中开发者所积累下来的知识。”-Li
我们当时有一套面向API端点的随机客户端,好像从来就没人用过。那么要不要把它删除掉?毕竟这也属于技术债务。
但如果我告诉大家,每年在特定的国家/地区,都会有10名记者将新闻发送到该端点,又该怎么办?我们是如何测试的?如果没有文档(也确实没有),我们找不到答案。因此,我们删除了该端点,并在对应时间点上发现了问题——这10名记者无法发送10份重要的报道,因为该端点已经不复存在。
了解产品的成员已经离开了团队,现在只能靠代码当中的注释来解释该端点的作用。
从这件事上,我意识到文档是每个团队都在努力解决、但却难以奏效的问题。除了代码文档之外,与代码相关的流程也有类似的情况。
时至今日,我们也没有找到完美的解决方案。
原子提交
如果必须要回滚(而且回滚需求早晚会出现,我们将在测试部分具体讨论),此次提交还是否有意义?
在删除垃圾代码时要充满信心
删除垃圾或者过时的代码总是让我感觉很不舒服。我总觉得以往的工作成果有种神圣不可侵犯的意义。我那时候认为,“在他们写与这些代码时,肯定是有所考量的。”这是一种传统的理解方式,而且与第一性原则有所冲突。出于类似的理由,我在每年进行代码审查与清理时也是困难重重。这样的糟糕习惯,让我吃了不少苦头。
我曾经尝试调整代码问题,也有些老成员习惯于绕过这些代码。但删除,删除听起来更严重正经。一个永远用不上的if语句、一个永远用不上的函数,会在我的一声令下彻底消失,这样不好。因此,我更多是把自己的函数覆盖在上面。但这并没有减少技术债务,只是增加了代码的复杂性与误导性。如此一来,后继者将更难把这些片段以有意义的方式拼凑起来。
我现在采取的方式是:总会存在我们无法理解的代码,也总会存在我们永远不会使用的代码。删除这些永远不会使用的代码,但对无法理解的代码保持谨慎的态度。
代码审查
代码审查是学习中的重要组成部分。审查的过程,就是从编写代码、到了解如何更好地编写代码的反馈循环。我们自己的编码思路,跟其他人的编码思路有何不同?我在每一次代码审查时都会问自己:“他们为什么要这样做?”如果实在找不到合理的答案,我就会跟他们当面聊聊。在第一个月的过渡期结束之后,我开始疯狂地从同事的代码当中查找错误(当然,他们也不会放过我)。真的很疯狂,这也让评审工作变成一项有趣的调剂——或者说像是一种游戏,能够改善我们编码水平的小游戏。
我的心得:在理解代码作用之前,不要轻下断言。
测试
我特别喜欢测试这项工作,事实上如果不加测试,我根本就不愿意直接在代码库中编写代码。
如果您的整个应用程序只需要执行一项任务(我在学校里的实验性项目就是这样),那么手动测试即可解决问题,我以前也一直习惯于这种方式。但是,当应用程序当中包含上百种功能,情况又会如何?我不想拿出大量时间挨个测试,而且我也知道自己肯定会忘掉某些需要测试的部分。这绝对会是一场噩梦。
这时候,我们就该请出测试自动化方案了。
在我看来,测试跟记录文档差不多。测试的过程,就是记录我对于代码的假设是否正确的过程。测试会告诉我,我自己(或者是当初写下代码的开发)当时希望代码如何运行,以及认为哪里有可能出问题。
因此,现在再编写测试时,我会牢记以下两点:
演示如何使用我正在测试的类/函数/系统。展示我认为可能出问题的部分。第一条相信很多朋友都能理解,毕竟在大多数情况下,我们需要测试的其实是行为,而非实现。但我个人总会忽略第2条,即bug可能出现在哪里。
因此,每当我发现bug时,我都会确保代码修复程序在相应的测试(也就是回归测试)当中记录下其它有可能引发错误的方式。
当然,编写这类测试本身并不能提供代码质量,只有真正编写代码才会真正影响质量。不过我从阅读测试结果当中获得的见解,确实能够帮助自己编写出更好的代码。
这就是测试的宏观意义。
除此之外,测试还肩负着另一项重要使命:确定部署环境。
大家可能拥有完美的单元测试,但如果没有进行系统测试,就有可能发生以下情况:
锁到底是好的,还是坏的?
对于经过良好测试的代码也是如此:如果您的机器上没有其需要的库,代码就会崩溃。
您开发所在的机器环境。(「一切都能在我的机器上正常运行!」)您测试所在的机器环境。(可能就是您开发所使用的那台机器。)最后,您部署所在的机器环境。(请一定换一台别的机器。)如果测试与部署机器间的环境不匹配,那一般都会出点问题。而这,正是部署环境的意义所在。我们在自己的机器上使用docker构建本地开发环境。
在这套开发环境当中安装有一组库(及开发工具),我们则以此为基础安装已经编写完成的代码。所有与其它依赖系统相关的测试,都在这里完成。
然后是beta测试/分段环境,其与生产环境完全一致。
最后是生产环境,也就是负责运行代码并为实际客户提供服务的机器。
我们的基本思路是努力捕捉那些不会在单元与系统测试中出现的错误。例如,请求与响应系统之间的API不匹配问题。
我猜个人项目或者小型企业的情况可能有所不同,毕竟并不是每个人都有资源来设置自己的一套基础设施。但是,如果大家愿意使用AWS以及Azure等云服务,这里提到的方法仍然适合各位。大家可以为开发以及生产环境设置单独的集群。AWSECS利用docker镜像进行部署,因此各环境之间相对一致。比较棘手的部分,就是如果与其它AWS服务顺利整合。例如,我们是否从正确的环境中调用了正确的端点?
大家甚至可以更进一步:为其它AWS服务下载备用容器镜像,并利用docker-