高级软件工程师教会小白的那些事!
作者 | Neil Kakkar
译者 | 王艳妮,责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下为译文:
一年前,我开始了在彭博社的全职工作。我那时就想象着要写这篇文章了。我想象着自己脑中会充满各种各样的想法,而时机成熟时,可以将其诉诸笔端。仅仅一个月以后,我就意识到了这件事并不容易:我总是在慢慢忘记我已经学到的东西。它们要么被完全内化,以至于我的大脑让我以为我本来就知道这些,要么就被我渐渐淡忘了。
这是我开始写“人类日志”的原因之一。每一天,当我遇到一个有趣的情况时,我都会记录下来。我很幸运的能坐在一位高级软件工程师旁边,这样我可以仔细观察他在做什么,以及这与我的做法有何不同。我们经常组队编程,这使得观察他更容易了。此外,在我的团队文化中,在别人写代码的时候站在后面看并不是什么不好的事情。每当我感觉到有趣的事情正在发生时,我就会转过去看看。由于经常凑过去看,我总是知道事情发生的前因后果。
我有一年的时间都坐在一位高级软件工程师旁边工作。以下是我学到的一些东西。
写代码
如何命名
我接手的第一样东西就是React UI。我们有一个主要组件,它容纳了其他所有组件。我喜欢在代码中加入一点幽默感,我想把它命名为GodComponent。在code review的时候,我才明白为什么命名是一件很难的事情。
计算机科学有两个难点:缓存失效,给变量命名,以及差一错误。- Leon Bambrick
我经手的每一段代码都带有隐喻意。GodComponent?那时用来盛放所有那些我不知道该放到哪里的的烂代码的。它包罗万象。如果我将一个变量命名为LayoutComponent,未来我会知道,它所做的只是规划布局,而不涉及任何状态。
我发现的另一个好处是:如果它看起来太大了,就像包含大量业务逻辑的LayoutComponent一样,我知道是时候重构了,因为业务逻辑不应当属于那部分。而如果使用GodComponent这个名称,那对里面的业务逻辑就不会产生任何影响。
命名你的集群?根据在它上面运行服务来命名是个好主意,可是你以后还可能会在上面运行其他东西。最终,我们是用团队名称来命名的。
对于函数来说也是一样。doEverything()是一个可怕的名字,这会产生很多后果。如果这个函数可以完成所有操作,那么测试这个函数的特定部分就会变得特别难。无论这个函数有多大,你都不会觉得奇怪,因为毕竟这个函数就是要做所有事情的。所以需要换个函数名,重构。
有意义的命名也有不好的一面。如果名称太有意义并隐藏一些歧义怎么办?例如,在SQLAlchemy中调用session.close()时,closing sessions不会关闭基础数据库连接。(我本应先读手册(RTFM, Read The Fucking Manual)并防止该bug——关于这一点在debug部分详细展开)
在这种情况下,将名称视为x,y,z而不是count(),close(),insertIntoDB()可以防止赋予它们隐含意义——并迫使我仔细检查它们正在做什么。
从来没想到,关于命名我要说的东西居然不能用一句话就概括完。
旧代码和下一个开发者
你有没有看过一些代码并觉得很奇怪?那些开发者为什么这样做?这完全说不通啊。
我有幸曾经使用过遗留代码库。其中有类似这样的注释,“在与穆罕默德一起解决了这个问题以后,注释就删掉了。”你在做什么?谁是穆罕默德?
我可以在这里做一个角色转换——想想以后来接手我代码的人们——他们会不会发现它很奇怪。Peer review 部分解决了这个问题。这让我意识到了环境的重要性:要时刻记得我的团队正在工作的环境是什么样的。
如果我忘记了代码,稍后又看到它,而无法重新回想起当时的环境时,我会说:“到底为什么他们会这样做?这讲不通......哦等等,这是我自己写的。”
这就是文档和代码注释发挥作用的地方了。
文档和代码注释
它们有助于保留环境(上下文,语境),以及分享知识。
正如Li在“如何建立良好的软件”中所说的那样,“软件的主要价值不在于生成的代码,而在于产生它的人所积累的知识。”
“软件的主要价值不在于产生的代码,而在于产生它的人所积累的知识。”——Li
我们有一个面向客户的API终端,似乎没有人使用过。我们只是删除它吗?毕竟,这是技术负债。
如果我告诉你,每年在特定国家/地区,10名记者会将他们的报告发送到该终端,该怎么办?你要如何测试?如果没有文档(现实中确实没有),我们就没办法。所以,我们没有那么做。我们直接删除了该端点。几个月以后那个一年一度的时刻到了。十名记者无法发送10份重要报告,因为终端不再存在了。
拥有关于这个产品的知识的人离开了团队。当然,现在代码中有一些注释解释了端点的用途。
据我所知,文档是一个每个团队都在努力解决的问题。不仅仅是代码文档,还有代码周围的流程。
我们还没有找到一个完美的解决方案。
我很喜欢Antirez对不同类型的有价值的代码注释的详细分类。
原子提交
如果你必须回到之前的步骤(是的你会的。详见测试部分),这个提交作为一个单元是否合适?
在删除烂代码的时候有自信
删除烂代码或过时的代码会使我感到非常不舒服。我认为多年之前被写下的代码是神圣的。我的想法是“当他们写下这些东西时,他们肯定是考虑到一些事情的。”这是传统和文化与第一原则思维方式之间的较量。删除一年一次的终端也是如此。我在这方面得到了太多具体的教训。
我会试着从周围解决代码,而高级工程师则会试着从中间解决。删除所有内容。一个永远不会运行的if语句?一个不应该调用的函数?是的,一切都没了。我?我只会在最上面写下我自己的函数而已。我没有减少技术债务。如果我做了什么的话,我也只是增加了代码复杂性和给他人的误导而已。对下一个人来说把这些代码功能拼凑到一起会更艰难。
我现在使用的启发式是:现在有的代码你无法理解,而且你知道有些代码是你永远也不会用到的。删除那些你永远不会用到的代码,并对那些你不理解的代码保持谨慎的态度。
Code Reviews
Code review是非常棒的学习途径。这是一个外部反馈循环,反映了你现在和将来会怎么写代码。差别在哪里?有一种方式比另一种更好吗?我在每次code review时都会问自己这个问题:“为什么他们那样做?”。每当我找不到合适的答案时,我都会和他们谈谈。
在第一个月之后,我开始在我的队友代码中发现一些错误(就像他们曾经为我做的那样)。这太疯狂了。同行评论对我来说变得更加有趣了——变成了我期待的一场游戏——一场改善我的代码感的游戏。
我的启发是:在我了解代码如何工作之前不要批准代码。
我的Github数据
测试
我非常喜欢测试,以至于如果没有测试,在代码库中写代码会使我感到很不舒服。
如果你的整个应用程序只做一件事(就像我所有的课设一样),那么手动测试仍然可行。我以前就是这么做的。但是当应用程序能做100种不同的事情时会发生什么?我不想花整整半小时来逐项测试,而且有时我会遗忘真正需要测试的那一个东西。那是一场噩梦。
这时测试和测试自动化登场了。
我把测试当做是文档。这是我对代码预期效果的文档。测试告诉我,我(或我之前的人)如何期望代码来工作,以及他们认为事情会出错的地方。
所以,当我现在编写测试时,我会记住这一点:
演示如何使用我正在测试的类/函数/系统。
展示出所有我认为可能会出错的内容。
上述的一个必然结果是,在大多数情况下,我测试的是行为,而不是实现。(点击此处查看我在Google浴室休息期间听来的一个例子:https://testing.googleblog.com/2013/08/testing-on-toilet-test-behavior-not.html)
我在#2中漏掉的东西就是bug的来源。
因此,每当我发现一个bug时,我都会确保代码修复程序有相应的测试(称为回归测试)来记录信息:这是另一种可能出错的方法。
但是,仅仅编写这些测试并不能提高代码质量,需要实际编写代码。但是我从阅读测试中获得的见解能帮助我写更好的代码。
这是测试的整体图景。
但是,这不是唯一一种要做的测试。接下来就是部署环境登场的地方。
你可能有完美的单元测试,但如果没有进行系统测试,则会发生以下情况:
这锁好使(吗?)
对于经过良好测试的代码也是如此:如果你的机器上没有所需的库,则会崩溃。
首先是你用来开发的机器(所有“它在我的机器上能正常工作!”这类meme(梗)的来源)。
其次是你用来测试的机器(可能与你用来开发的机器相同)。
最后,有你用来部署的机器(请不要让它与你用来开发的机器相同)
如果测试和部署机器之间的环境不匹配,你就麻烦了。这就是部署环境的用武之地。
我们的机器上有本地开发,它位于docker中。
我们有一个开发环境,其中机器安装了一组库(和开发工具),我们在上面安装在这些库上编写的代码。其他依赖系统的所有测试都可以在这里进行。
然后是beta / stage环境,它与生产环境完全一样。
最后,生产环境,它们是运行代码并为实际客户提供服务的机器。
目的是尝试捕获单元和系统测试发现不了的bug。例如,请求和响应系统之间的API不匹配。
我想个人项目或小公司的情况会有很大不同。并非每个人都有资源来部署自己的基础设施。但是,这个想法对于AWS和Azure等云提供商的服务也适用。
你可以为开发和生产设置单独的集群。AWS ECS使用docker镜像进行部署,因此各环境之间相对一致。棘手的一点是其他AWS服务之间的集成。你是否从正确的环境中调用了正确的端点?
你甚至可以更进一步:为其他AWS服务下载备用容器映像,并使用docker-compose设置本地完整环境。这样能加速反馈循环
等我启动自己的业余项目以后,我可能会在这方面有更多的经验。
降低风险(derisking)
Derisking是一门通过你所部署的代码来降低风险的艺术。
可以采取哪些措施来降低灾难发生的风险呢?
如果这是一个新的突破性变化,当出现问题时,如何保证最小程度的损失?
“我们不需要对所有这些新变化都进行全系统部署。”哦,等等,真的吗?我当时怎么一点也没想到!
设计
我为什么要把设计放在写代码和测试这两项之后呢?好吧,设计可能是首要问题,但如果我还没有在现在这个环境中编码和测试过,我可能不会像现在这样擅长设计一个尊重环境特性的系统。
在设计一个系统时有很多事情值得考虑。
使用数量是多少
有多少用户?预期的增长是多少?(这将转化为多少个数据库行)
未来的失误可能是什么?
我需要把它转换成一份名为“收集需求”的整洁的清单。今年我这方面做的还不够多,这是我明年在彭博社要解决的问题。
这个过程有点违背敏捷——在开始实施之前你能设计到什么程度呢?这是一个平衡——而且你要选择什么时间做什么事情。什么时候该埋头苦干,什么时候该后退一步?
当然,收集需求并不是全部。我认为将开发过程包含在设计中也是有好处的。比如
本地开发将如何运作?
我们将如何打包和部署?
我们如何进行端到端测试?
我们将如何对这项新服务进行压力测试?
我们将如何管理秘密?
CI / CD集成?
我们最近为BNEF开发了一个新的检索系统。做这方面的工作很棒。我必须设计本地开发,了解DPKG(打包和部署),并与秘密部署搏斗。
谁能想到把秘密部署到生产中居然会那么棘手?
你不能把它们放在代码中,不然任何人都可以看到它们了。
把它们作为环境变量,就像12 factor app那样?这是个好主意。你要怎么把它们放在那里?(每次机器启动时访问PROD机器来填充环境变量都很痛苦)
部署为秘密文件?文件来自哪里?它是如何填充的?
我们不想手动地去做事情。
最后,我们使用了具有角色访问控制的数据库(只有我们和我们的机器可以与数据库通信)。我们的代码在启动时从这个数据库获取秘密。这在开发,beta和生产中都有很好的复现,各自的数据库中都有秘密。
同样,如果你用的是AWS等云提供商提供的服务,情况可能会有很大不同。你不必多考虑秘密。获取你的角色帐户,在UI中输入秘密,你的代码将在需要时找到它们。这样简化了不少东西,挺酷的——但我很高兴自己有前面的经验可以来欣赏它的简洁性。
设计时考虑到维护
设计系统令人兴奋。而维护呢?就不怎么样了。
我的维护经历让我想到了这个问题。系统是为何以及如何退化的?
首先是不弃用旧的,反而总是添加更多新的东西。倾向于添加而不是删除。(让你想起某人了吗?)
其次是设计时总想着最终目标。一个不断发展着去做不是自己本应做的事情的系统,表现必然没有那些从一开始就目标明确的系统好。这是采取后退一步的方法,而不是马上上手。
我现在知道至少三种降低系统退化速度的方法。
保持业务逻辑和基础架构分离:通常退化的是基础架构——使用量增加,框架变得过时,零日漏洞的出现等等。
围绕维护来构建流程。对新的部分和旧的部分进行相同的更新。这可以防止新旧之间的差异,并使整个代码保持“现代”。
确保你一直在修剪所有不需要的/旧的东西。
部署
我是将功能捆绑在一起好呢,还是逐个部署好呢?
根据当前的流程,如果上面这个问题的答案是将功能捆绑在一起,则会出现问题。
那么该问的则是,为什么要将功能捆绑在一起?
部署是否需要花费太多时间?
Code review会变得更加不容易吗?
无论是出于什么原因,这都是解决问题的瓶颈所在。
关于功能捆绑,我知道至少存在两个问题。
1、如果其中一个有bug,就会自发阻止一个功能。
2、这样做违背了降低风险的原则,或者说是增加了出错的风险。
然后,无论你选择哪种部署流程,你总是希望你的机器像牛一样,而不是像宠物一样。它们并不珍贵。你确切知道每台机器上运行的是什么,以及如何在它坏掉的时候重新创建一个出来。当一台机器坏掉时,你不会感到沮丧,你只需要启动一台新机器。你豢养它们,而不是抚养它们。
当有地方出错时
当出现问题时(问题必然会出现的),黄金法则就是尽量减少对客户的影响。
当出现问题时,我的自然反应是去解决问题。事实证明,这不是最优解决方案。
首先要做的是回滚,而不是修复出错的地方,即使“改一行代码就行”。回到之前的工作状态。这是让客户恢复工作版本的最快方式。
然后我再去看看出了什么问题,并修复这些bug。
集群中的“borked”机器也是如此——先将其下线,标记为不可用,然后再尝试找出机器出了什么毛病。
我发现有一点很奇怪,那就是我的自然倾向和本能反应竟然与最佳解决方案大相径庭。
我认为这种本能也让我走上了解决bug的漫长道路。有时,我觉得它不work,就是因为我写的代码出了问题,而且我会深入研究我写的每一行代码。像深度优先搜索那样。
最后发现是配置更改导致时,也就是说,我没有事先启用该功能,这让我很生气。我在改bug方面做得远远达不到最优。
从那时起,我的启发式方法就是在深度优先搜索之前进行广度优先搜索,以摆脱顶级节点。我可以使用当前资源确认什么?
机器开启了吗?
是否装好了正确的代码?
配置到位了吗?
<代码特定配置>,就像代码中的路由是否正确?
架构版本是否正确?
然后,进入代码部分。
我们以为是nginx没有在机器上正确安装好,但最后发现,只是配置被设置了false。
当然,我不需要一直这样做。有时,仅仅error提示就足以将搜索空间缩减而直指到我的代码。
当我无法弄清楚这个问题的时候,我会尽量将代码的改动保持在最低限度。改动的地方越少,我就能越快地研究真正的问题。将推理跳跃保持在最低限度。
我现在还会记下那些花了我1个多小时才解决的bug:我漏掉了什么?通常是因为我忘记检查一些愚蠢的小事,比如设置路由,确保架构版本和服务版本匹配等等。这是使我对当前使用的技术栈熟悉起来的另一个步骤,不过还有一样东西只能靠经验培养——能弄清楚事情为什么不work的直觉。
战争故事
这是调整参数或玩弄统计数据,以及修复根本原因这两者之间的一段舞蹈。
如果没有战争故事的部分,这篇文章怎么能够说是完整呢?我喜欢阅读它们,现在我至少有一个故事想分享一下。
这是个关于搜索和SQLAlchemy的传说。在BNEF,我们有很多分析师来撰写研究报告。每当报告发布时,我们都会收到一条消息。每当我们收到消息时,我们都会通过SQLAlchemy进入我们的数据库,获取我们需要的所有东西,将其转换,然后将它发送到我们的solr实例进行索引。就在这时,奇怪的AF bug发生了。
每天早上,连接到数据库都会失败,显示error“MYSQL服务器已经消失。”有时候,下午也是如此。机器在下午转动,所以我首先检查的就是这个。不,机器转动时从未发生过错误。我们全天向数据库发出数千个请求,没有一个失败的。那么,这个非常低的负载触发怎么会失败呢?
哦,也许是因为我们没有在事务结束后关闭会话?所以,如果是相同的会话,并且下一个请求在很长一段时间后出现,我们就超时了,服务器就消失了。去看一眼代码,果然,我们在每次读取时使用上下文管理器,在__exit __()上调用session.close()。
用一整天时间来排查所有可能的故障,一无所获,第二天早上上班,机缘巧合之下找到了原因。像往常一样,那天早上也报错了,一秒后,有其他三个索引请求成功了。这符合有一个会话没有被正确关闭的所有表现。后面的故事你已经知道了。
除非你使用的是NullPool,否则SQLAlchemy的mysql语言中的Session.close()不会关闭基础数据库连接。是的,后来就把这里修好了。
好玩的是,这个bug的发生仅仅是因为,我们没有选择在晚上或午餐时间发布研究报告。这里还有另一个教训——stack overflow上的大部分答案(我当然有事先Google过了!)是调整会话超时时间,或者是调整控制每个SQL语句可以发送的数据量的参数。那些回答对我来说都讲不通,因为它们与根本性的问题几乎无关。我检查过查询大小是否在限制范围内,以确保关闭会话时不会发生超时。
我们可以通过将会话超时的值增加到8小时而不是原来的1小时来“修复”此bug。这似乎也可以解决这个问题,直到遇到下一次工作日放假——第二天早上的第一份研究报告将会失败。
这是调整参数或玩弄统计数据,以及修复根本原因这两者之间的一段舞蹈。
监控
这是我之前从未想过要做的事情。平心而论,在全职写代码之前,我从未维护过系统。我仅仅是建造它们,用了一个星期然后开始下一个项目。
通过使用两个系统,一个具有良好的监控功能,另一个则不具备,这让我懂得了监控的重要性。如果我都不知道它们的存在,我就无法修复bug。最糟糕的感受之一就是从客户那里知道有bug出现。“我在做什么?!我连我自己的系统出了什么问题都不知道?“
我认为监控由三个组件构成——日志记录,度量和警报。
代码写的日志就像人类日志一样,是一个渐进的过程。
找出可能需要监视的内容,记录这些内容,然后运行系统。随着时间的推移,你会发现一些bug,但你还没有充足的信息来解决它们。这是增强日志记录的好时机——你的代码中漏掉了什么?
我认为,你自然而然地就会知道哪些东西是值得记录的。这位高级软件工程师和我的记录之间有很大不同。我认为请求—响应日志就足够了,而他有很多度量,比如查询执行时间,代码所做的一些特定内部调用,以及何时轮换日志,所有这些都已整理出来。
在没有日志的情况下进行debug几乎是不可能的——如果你连系统所处的状态都不知道,又谈何重新创建一个出来?
度量标准可以从日志中产生,也可以在代码中独立存在。(例如将事件发送到AWS CloudWatch和Grafana)。你可以自己决定度量并在代码运行时把那些数字发送出去。
在一个好的监控系统中,警报是将所有内容整合在一起的粘合剂。如果一个度量是当前参与生产的机器数量,当这个数字下降到50%时,将是一个很严重的警报——你就知道出问题了。
失败计数超过某个阈值?是的,另一个警报。
由于知道如果出错了那么警报就会叫醒我,所以我在晚上能安然入睡。(等等,什么?)
这暗示了另一种需要培养的习惯。当你修复bug时,你不仅仅是专注于如何修复这个bug,而且要思考,为什么没有早点弄清楚?警报设置到位吗?如何更好地监控以防止类似问题再次出现?
我还没弄明白如何监控用户界面。只测试组件是否到位不足以让我知道哪里出错了。这通常是客户告诉我们的——哪里看起来有点不对劲。
结论
在过去的一年里,我学到了很多东西。我很庆幸自己刚开始工作时就决定要写这篇文章。有了这篇文章作参照,我能够更好地体会到自己成长了多少。我希望你也可以从这里得到一些启发。
我也非常幸运能够身处一支优秀的团队中——我们写代码很多,欢声笑语也很多,我们能从头开始设计系统,并与很多其他团队合作。
今年,我坐在两个高级开发人员旁边。今年会怎么样呢,让我们拭目以待!感谢团队!
那些优秀的工程师,他们自己设计的系统更健壮,更容易被他人理解。这具有乘数效应,可以使同事们在他们设计的系统上更快更可靠地开发——如何构建良好的软件
我尚不确定的事情
我还没有悟到软件工程的真谛。因此,本节提醒我:还有很多东西需要学习!如果我的方向正确,明年这个list应该会变得更长。
1、从抽象还是实现的角度思考?
2、我应该对如何做事有强烈意见吗?也许是因为以前吃过亏?我以前做过的工作是否为自己赢得了话语权?
3、开发工作流程。如果因紧急情况或事件需要改变工作方式——那么这个流程是否会被破坏?它需要被修理好吗?
4、utils(你放置随机东西的文件夹,不放在这里的话,你不知道该放在哪里)是代码味道(code smell)吗?
5、如何处理代码和工作流的文档?
6、如何监控UI才能知道什么时候出问题了?
7、花时间设计完美的API /代码合同,以及自己写出代码并反复迭代选出最优的那个之间,哪一种更好?
8、简单的方式vs正确的方式?我不觉得正确的方法永远是优越的。
9、自己做事vs教那些不会的人如何做事。前者完成速度快,后者意味着你以后就很少需要自己亲自动手了。
10、当重构和防止巨大问题时:“如果我先改变了所有的测试,那么我会看到我有52个文件需要修改,这显然太大了,但是我先去管代码而不是测试吧。”分开处理值得吗?
11、在降低风险(derisking)方面做进一步探索。有哪些策略可以降低项目的风险?
12、收集需求的有效方法有哪些?
13、如何降低系统退化率?
感谢Hemanth Kumar Veeranki阅读本文。
1、很多事情上都是这样。你会骑自行车吗?你能教会别人吗?告诉他们你做的每一个确切步骤?↩
2、这并不意味着使用名称x(),y(),z()来写代码,而只是将它们视为x(),y(),z()。不要假设你所看到的就是一切。(WYSIATI)
3、经典的切斯特顿的围栏效应。(Chesterton’s fence) ↩
4、我不会再这样做了。一旦你进入自动化测试阶段,你就永远回不去了?↩
5、有这样一种论调一发不可收拾,那就是通过对可能出错的一切进行一百万次测试。据我所知,情况现在还并非如此。↩
6、我有一阵子没有这样做过了,所以不确定查找/构建AWS特定的docker镜像的难易度。↩
7、你所处的环境可能决定着你的技术栈。
原文:https://neilkakkar.com/things-I-learnt-from-a-senior-dev.html
☞分析了 9 万条程序员招聘数据,Python 薪资竟反超 Java?
☞Unix 激荡 50 年:驱动 Android、iOS 的操作系统是如何从失败开始的?