我不遵循的五个软件最佳实践
星期五下午5点。孩子们一个小时后就要去踢足球了,冰箱里空空如也,而杂货不会自己买回来。洗衣堆积如山,人力资源部关于“裤子是工作服”的电话也打得你烦不胜烦。于是你收拾好笔记本电脑,准备出门开始周末,暂时改变一下生活中的疯狂节奏。
你正要推开门走向自由,这时你的经理冲了过来。一切都乱套了。仪表盘上全是红色图标,网页服务器像在为MLB训练一样不断抛出错误,而你已经连续熬了48小时,看到首席SRE正用两根中指比划着辞职的姿势走向自己的车。
尽管没有开发者希望过这样的生活,但许多人在职业生涯中至少经历过一次。为了确保这样的混乱留在过去,他们从战斗的伤痕中总结出了集体智慧。我通过近十年的专业开发经验积累了一些智慧,同时也从同事那里学到了更多。
但现在,我第一次在个人项目中将这些智慧抛诸脑后。原来,为了实现自我满足的个人项目与旨在盈利的企业项目有着不同的目的。这为每个代码库创造了不同的激励机制,而业余项目的激励机制会使许多维持企业项目可持续性的开发实践失效。
那么,让我们谈谈规则,以及如何打破它们。
1. 在分支上进行修改,而不是在主分支上
让多人同时在同一个代码库上工作是很困难的。当山姆在更新显示在屏幕上的代码时,珍妮正在更新山姆的代码读取以在监视器上显示图形的数据模型。两位开发人员最终不得不同时修改相同的文件,因此需要一种方法来确保这些修改不会相互覆盖。对于这两位开发者来说,显而易见的解决方案是“优胜者为王”,即胜者获得添加更改的权利,而败者则面临更大的问题。
遗憾的是,我们生活在文明社会,因此只能依赖版本控制系统。
在传统版本控制系统中,存在一个包含主代码版本的代码仓库。开发者从主版本中提取代码,并在基于主版本的临时版本(通常称为分支)中进行修改。这些分支会被合并在一起,这一过程可能涉及人工审核和繁琐的清理工作,但仍优于两名开发者在同一组文件上相互覆盖修改。这一传统衍生出一条铁律:绝不能直接修改主代码库。
然而,这一原则基于多人同时修改同一代码的假设。目前,我是Web Drones的唯一维护者。这意味着没有其他人与我同时进行修改,因此不存在与我产生冲突修改的情况,也就无需通过“决斗”来解决分歧。在项目主版本的分支上开发并将其合并回主版本,最终与直接将更改写入主版本完全相同。
我曾有几次在分支上写入特别大的更改,但我认为这是流程上的问题。我了解自己,如果我同时开始多件事,没有一件会完成。
2. 运用你熟悉的技术,并逐步引入新技术
每个人都认识那种人。你知道的,就是那个开玩笑说要用Arch的人,但实际上他只是在装模作样,因为那些真正了解技术的人对他投以鄙夷的目光。我们暂且称他为格雷格。
尽管格雷格可能很烦人,但他非常聪明。因为他愿意使用最新的技术,他知道很多东西。在一个唯一不变的就是变化的领域,格雷格最终知道解决任何问题的最佳技术,并能用它来快速解决任何抛给他的问题。
许多人曾经历过格雷格将最新技术应用于项目,并因此懊恼到想用头撞桌,直到头痛欲裂。格雷格引入了一系列缺陷,彻底破坏了其他人对代码库进行修改的能力。究竟发生了什么?
这里有两个问题需要考虑。其一是软件只有经过测试才能正常运行,而测试的最佳方式是让大量终端用户使用产品并遇到其边界案例。这并非否定质量保证(QA)的必要性,但从数量上讲,终端用户数量必须始终多于测试人员,且代码库存在的时间越长,终端用户就越有可能遇到更多边界案例。因此,新软件有更少的时间让用户发现 bug(开发人员随后修复)。这意味着如果你使用的是最新版本,你将需要做更多自愿的 QA 工作,而不是发布你的代码。
另一个问题是,人类是习惯的动物,他们会快速且可预测地使用他们已经熟悉的东西。改变程序员的工作流程会带来巨大的成本和风险,而这种成本在尝试跟上新技术和趋势时必须得到缓解。
在实践中,平衡稳定性和过时性的最简单方法是逐步将新技术添加到项目中,这保持了当前工作的连贯性。
那么,当项目基础设施中某项核心组件(例如编程语言)开始积累缺陷、技术债务,甚至危及开发者的理智时,该如何应对?如何在不引发ffi/序列化IPC噩梦的情况下,分阶段引入如此核心的编程语言?也许你正在学习一些全新的技术,几乎没有经过实战检验。追踪一个来自多态宏单子机制的奇怪v0.0.1编译器 bug 需要多长时间?这种机制在构思时比实现时有趣得多。这些问题没有好的答案,你必须接受在学习和开发最新技术时会频繁失败。
幸运的是,有一些地方可以学习大型复杂的技术,而无需承担失败的风险。这就是为什么我决定在托管游戏的服务器上学习。同样,当 Greg 的 Arch 构建在自身更新的重量下崩溃时,他不会有付费客户来起诉他;如果有人在使用 Web Drones 时遇到问题,他们也不会失去超过几个小时的游戏访问权限。
我一直在模仿格雷格,深入探索自己一无所知的全新技术,享受其带来的好处并承受其带来的后果。在整个职业生涯中,我一直使用Python编写软件,但随着其近三十五年的技术债务不断积累,我感到越来越沮丧。因此,我转而使用Go语言。没有“Hello World”教程,没有二十行代码的项目来学习 goroutine 的工作原理,只需快速搜索如何编写 OpenAPI 规范,再搜索如何在该规范上运行代码生成器,然后就是无尽的探索之路。如果我面临截止日期,以这种方式工作,我肯定会筋疲力尽,放弃,然后花一个月时间自怨自艾,幻想自己去农场工作会更好。但没有风险和压力,这无疑是我学习如此庞大编程语言过程中最令人满足的方式之一。事后想想,我本该抓住机会变得更疯狂一些,转而学习Elixir、Clojure,甚至Haskell。总有下次机会。
3. 努力实现70%代码覆盖率的自动化测试套件
你是否曾问过开发者如何确保他们的代码正常运行?如果运气好,开发者会告诉你他们的代码已通过TLA等形式验证软件严格证明正确。然而,大多数人会告诉你他们进行了测试。不过,测试是重复、枯燥且无聊的。这使得开发者更可能在测试的彻底性上偷工减料,而人为操作必然伴随人为错误的风险。出于这些原因,大多数开发人员会编写应用程序代码,然后再编写额外的代码,其唯一目的是自动测试应用程序。这类似于机械工程师在主桥上建造第二座桥来测试其承重能力。或者类似的情况。我不是机械工程师。
无论如何,评估测试代码对应用程序代码的测试彻底性至关重要。如果你在设计软件时只考虑了理想的执行路径,那么一旦软件离开你的掌控,它就会崩溃,因为新用户会将其置于超越现有最伟大的洛夫克拉夫特式想象的诡异而神秘的考验之中。衡量测试彻底性的常见方法是使用覆盖率工具,统计测试执行的每一行代码。这通常以执行行数与总代码行数的比率表示,并以百分比形式列出。虽然写出覆盖代码库每一行的测试(100%覆盖率)看似吸引人,但在实践中这会导致代码复杂化,而100%覆盖率并不值得增加这种复杂性。没有固定的数字来规定代码库需要被测试覆盖的程度,但根据我的经验,70%是一个合理且被接受的数字。
在Web Drones中,我只测试了两个函数,测试覆盖率约为10%。为什么?
编写测试需要时间。这是我本可以用来推广项目、撰写此类文章,以及最重要的是根据玩家反馈调整游戏的宝贵时间。测试那些最终会被弃用的代码所花费的时间,就是浪费时间。
考虑到这一点,为什么我决定只测试这10%?我最终编写了两个包含多个边界情况的复杂函数。此外,这些函数实现的功能也基于时间的流逝。让这些函数接受我提供的任何时间对象,并用我想要的任何时间间隔编写单元测试,比手动测试这些间隔要容易得多。对这两段代码进行单元测试最终节省的时间,比手动测试所需的时间要多。
这并不是说我在编写Web Drones时没有考虑软件质量。如上所述,我使用了oapi-codegen的严格服务器生成功能,该功能利用Go的类型系统确保我的代码与OpenAPI规范中定义的输入输出完全匹配。我还在云端部署了一个无服务器函数,每分钟对服务器进行一次心跳检测,并通过Grafana仪表盘实现了一个小型监控套件。
4. 使用CI/CD
对着垃圾桶大火吐口水是没有用的,但如果你逐一对着一万支蜡烛吐口水呢?慢慢地,你就能把垃圾桶大火的火焰从蜡烛上吹走。这就是 CI/CD 的基本思路。你持续将小规模更改集成到代码库中,运行代码操作如代码检查、编译步骤、自动化测试,然后提交给人工审核。经过这些步骤后,更改以类似的连续性形式部署。如果做得好的话,问题最终会变得小而可控,容易回滚,而不是无法维护的灾难。
我在Web Drones中有CI,但目前没有CD。这是一个手动过程,原因与我的基础设施托管方式有关。
实现CI/CD的方式多种多样,但最快的搭建管道方法是使用云托管服务如Github Actions。对于Web Drones这样的小型项目,这非常适合。然而,我的基础设施预算仅限于我衣柜里积灰的两台树莓派。我可以使用Github Actions进行持续集成,但如果想将代码持续部署到公寓里的物理服务器,就必须开放互联网对硬件代码的修改权限。确保这一过程的安全性所需的时间和风险,远高于手动从Github容器注册表拉取更改。
5. 使用测试工具测量代码覆盖率
关于上述测试讨论。我得坦白,我从未真正设置过覆盖率工具。相反,我采用了经久不衰的“凭空估算10%”的方法。最坏的情况是什么?Web Drones那位不存在的CEO因我未提供报告来安抚他那些不存在的股东而解雇我?这是我的项目,如果我想看到一条上升的曲线,我可以编写一个计数器并将其集成到 Grafana 中。
–
在当今这个充满压力、要求频繁且快速发布的世界中,开发人员优先考虑编写高质量软件至关重要。然而,我们必须记住,质量是一个主观术语,其存在于软件被编写的原因的语境中。我撰写Web Drones的目的是以一种能激发他人加入的方式探索创造力的乐趣,这与试图盈利的典型目标大相径庭。
你也许感兴趣的:
- 20年程序员分享经验20条编程经验
- 日志分析工具 GoAccess 配置详细教程
- 用一个奇招检测并让Chromium机器人爬虫崩溃(机器人爬虫讨厌这个!)
- Rust 比 C 更快吗?
- 为什么 C++ 认为我的类是可复制构造的,而实际上它无法被复制构造?
- SVG 网站图标(favicon)的实际应用
- 这是 JavaScript 吗?
- 如何处理 Rust 依赖项
- 为什么 2025/05/28 和 2025-05-28 在 JavaScript 中是不同的日子?
- 在 Rust 中写入未初始化的缓冲区
你对本文的反应是: