本文是彭博社的一位开发者所写的文章,介绍了从一位资深工程师同事的身上学到的一些开发经验。
过去一年中,我坐在一位资深的软件工程师旁边,可以仔细地观察他是怎么工作的。
我们两人经常共同编程,使得这项观察更为容易。此外,在团队文化中,从背后窥探写代码的人并不令人反感。以下是我所学到的:
历史代码和下一名开发者
你曾否看过一些代码,觉得它们很奇怪?这些代码为什么这么做呢?它们的实现一点都不合理。
我曾负责过遗留代码库。代码中有诸如「当 Mohammad 发现情况时取消注释代码」这类的注释。这是在做什么?谁是 Mohammad?
在这里可以做下角色转换——想象下一个人来看我的代码,他们是否会觉得奇怪?
文档和注释有助于维护上下文和分享知识。
正如李在《如何构建好软件》中所说,「软件的主要价值不是编写它的代码,而是编写它的人所积累的知识。」
如果说,在某个特定国家,有 10 名记者会一年一次将他们的报道发送到这个终端,怎么办?你如何测试它?如果没有开发文档(那时就没有)就不能测试。所以我们没有测试。我们删除了那个终端。过了几个月后,到了一年中发送的时间,因为这个终端已经不存在了,10 名记者也就无法发送这 10 份重要报告。
虽然熟悉产品的人已经离开了团队,但是现在代码中有注释解释终端的作用。
据我所知,文档是每个团队都在努力的东西。不仅仅是代码的文档,还有关于代码的流程。
自信地删掉垃圾代码
我尝试基于已有代码进行工作,但是资深工程师会尝试解决掉它——全部删除。一个永远无法到达的 if 声明?一个不应该调用的函数?是的,都消失了。
代码审查对学习来说非常有用。这是你写代码和其他人写代码时进行的外部反馈循环。
两种实现有什么区别呢?一种方法比另一种好吗?每次代码审查时我都问自己:「他们为什么这样做?「。每当我找不到合适的答案时,我就会去和他们谈谈。
在第一个月后,我开始在同事的代码中找到错误(就像他们对我代码做的一样)。同行审查对我来说变得更有趣了——这是我期待的游戏——一个提高我代码意识的游戏。
我的启发是:在理解代码如何实现前不要批准它。
我非常喜欢测试,以至于如果没有测试就将代码写入代码库我会感到非常不舒服。
如果整个应用程序只做一件事(就像我所有的学校项目),那么手动测试是可以的。但是如果该应用程序可完成 100 种不同的功能,那该怎么办呢?我不想花半个小时来测试所有的功能,何况有时候还会忘记一些需要测试的地方。
我认为测试是一种文档,是对代码假设的文档。测试会告诉我(或我之前的人)他们预想代码是如何工作的,以及他们预期哪里会出错。
所以,当写测试时,我会记住:
记录如何使用测试时用到的类/函数/系统。
记录我所想到的会出错的地方。
我在 #2 中遗漏了一些东西,那里是 bug 出现的地方;
所以每当发现 bug 时,确保修复 bug 的代码也有相应的测试(称为回归测试),用于记录信息:这里可能出现另一种错误。
仅仅编写这些测试并不能提高我代码的质量,而编写代码却可以。但是我从阅读测试代码中获得了写更好代码的直觉。
有一台你用于开发的机器;
有一台你用于测试的机器;
最后,有一台你部署的机器(请不要用与开发程序使用同一台)。
我们先有本地开发环境,在我的机器上是 docker;
然后有服务器上的开发环境,机器上安装了一系列的库(和开发工具),我们在安装了代码的机器上进行开发。其他相关依赖的测试都可以在这里进行;
接下来是 beta/stage 环境,它与生产环境完全一样;
最后是生产环境,它是代码运行和服务于实际客户的机器上的环境。
你可以为开发和生产设置分开的集群。AWS ECS 使用 docker 镜像来部署,所以即使跨环境事情也会相对平稳。棘手的一点是其他 AWS 服务之间的集成。你是否可以在正确的环境中调用正确的终端呢?
你甚至可以更进一步:下载其他 AWS 服务的备用容器镜像并使用 docker-compose 来配置本地完整的环境。它会加速反馈循环。
为什么我要将设计放到写代码和测试的后面呢?设计本应该在第一位,但是如果我没有在环境中写代码和测试,我可能会不擅长设计一个遵循环境特性的系统。
在设计系统时,有很多事情需要考虑:
使用编号是多少?
有多少用户?预期增长是多少?(即需要使用多少数据行)
未来可能出现的问题是什么?
本地开发如何运作?
怎么打包和部署?
如何进行端对端的测试?
怎么对新的服务进行压力测试?
怎么管理机密信息?
CI/CD 集成?
谁会想到对产品中的机密信息进行部署会变得如此棘手呢?
你不能将这些信息存到代码中,因为这样任何人都能看得到。
把它们作为环境变量?这是一个好主意。但你怎么把它们放在那里?(每次机器启动时访问 PROD 机器来填充环境变量是一件痛苦的事情)
部署为机密文件?文件从哪里来呢?怎么进行填充呢?
最后我们使用了一个有角色访问控制的数据库(只有我们的机器可以与数据库对话)。我们的代码在启动时从这个数据库中获取秘密数据。这个能在开发、测试和产品之间很好地复制——在各自的数据库中都有机密。
同样的,对于像 AWS 这样的云供应商,这可能非常不同。你不必考虑太多机密。获取你角色账户,在用户界面中输入机密数据,在需要的时候你的代码会找到它们。它简化了很多时间,这非常酷,而我很高兴有经验领会这种简易性。
设计系统是件令人兴奋的事。维护系统呢?就没那么有趣了。
我在维护过程中遇到了这个问题:系统为什么会降级,以及如何降级?
首先,系统不应当舍弃旧的东西,而是在已有的基础上增加更多功能。系统更新倾向于增加而不是删除。
其次,带着最终目标来设计。一个进化到做不该做的事情的系统和一个从零来设计做同样事情的系统一样,没有用。这是一种系统的倒退。因此需要对系统进行降级。
将业务逻辑和基础设施分开:通常是对基础设施降级——当使用量增加、框架过时、出现零日漏洞等情况下;
围绕系统维护建立流程。对旧的和新的组件都使用相同的更新。这可以防止组件之间出现差异,保持整个代码「现代化」;
确保一直修剪你不想要的/旧的东西。
接下来要问的问题是:为什么想要把功能进行捆绑呢?
部署是否花费过多时间?
代码审查是否容易进行?
如果一个功能中有 bug,将妨碍另一个功能执行;
增加整体出错的风险。
当事情出了差错,我自然倾向于赶快解决 bug。事实证明,这并不是最理想的解决方案。与其修复哪里错了,即使只是「修改一行」,所做的第一件事应该是回滚版本。回到之前的工作状态,这是让客户恢复工作最快的方法。
过了这个时候,才应该看看哪里出了问题并修复那些 bug。
在你的集群中出现一台「垮掉」的机器也应当是同样的做法——在试图找出机器出了什么问题之前,先把它停了,并标记它不可用。
之后,我的启发是,首先开始广度优先搜索,然后再深度优先搜索,去除最顶端的节点。能否用已有的资源确认:
机器启动了吗?
是否安装了正确的代码?
配置是否正确?
<代码特定配置>,像代码中的路由是否正确?
模式版本是否正确?
然后进入代码。
但是我现在还是会记录花了 1 个多小时来解决的 bug:遗漏了什么?这通常是一些我忘记检查的愚蠢错误,比如像设置路由、确保模式版本和服务版本匹配等。这是熟悉使用的技术堆栈的另一步,而且只有经验会告诉我为什么系统无法运行。
这是我以前从未想过去做的事。说句公道话,在全职编码之前,我从没维护过系统。我只是搭建它们,使用 1 个星期后然后进行下一项工作。
有两个系统,一个有良好的监控,另一个并不那么好。我逐渐非常喜欢监控。如果我不知道 bug 在哪我就不能修改错误。其中一种最糟糕的感觉是从客户那里知道有 bug。
「我做了什么?!我甚至不知道我的系统出了什么问题?」
我认为监控由 3 个部分组成——日志、衡量标准和警报。
以代码中进行日记记录就像人写日志一样,是一个进化的过程。
你要找到你可能需要监控的东西,日志记录下来,运行系统。一段时间后,你会发现你没有足够信息来解决的 bug。这是增强日志记录的好时机——你的代码少了些什么?
几乎不可能在没有日志的情况下进行调试——如果你不知道系统的状态,你怎么重新创建它呢?
警报是把所有东西整合到一个的强大监控系统的粘合剂。如果一个衡量标准是当前产品中运行的机器数量,当这个数字降到 50% 时,这是一个很好的警报——你知道有什么出错了。
失败计数高于某个阈值时?是的,又一个警报。
我还不知道如何监控 UI。即使吧组件测试到位,也还不足以了解出错的情况。这些错误通常是由客户来告诉我们的——这看起来不太对劲。
总结
原文:https://neilkakkar.com/things-I-learnt-from-a-senior-dev.html#when-things-go-wrong
文章来源于网络,版权归原作者所有,如有侵权,请联系删除。
关注【一起学嵌入式】,回复“加群”进技术交流群。
觉得文章不错,点击“分享”、“赞”、“在看” 呗!