编写易于删除和调试的代码



易于调试的代码不会使您感到愚蠢。 调试具有隐藏行为,错误处理差,不确定性,结构不足或过分结构化或在更改过程中的代码更加困难。 在足够大的项目中,最终会得到无法理解的代码。

如果项目相对较旧,那么您可能会碰到根本忘记的代码,如果不是提交日志,您会发誓这些行不是您编写的。 随着项目的发展,记住不同代码段的功能变得更加困难。 如果代码不执行它似乎正在做的事情,那么情况将会恶化。 而且,当您需要更改不了解的代码时,必须认真弄清楚:调试。

编写易于调试的代码的能力始于您不记得以前写过的任何东西。

规则0:好的代码包含明显的错误。


广泛使用的技术的供应商声称“写清晰代码”是指“写清晰代码”。 问题在于“纯度”的程度与上下文高度相关。 可以在系统中对纯代码进行硬编码,有时还会编写一些肮脏的技巧,以便轻松禁用它。 有时代码被认为是干净的,因为所有污垢都被推到了某个地方。 好的代码不一定是干净的。

清洁度表示开发人员对此代码所感到的自尊(或羞耻)程度,而不是维护或更改的难易程度。 最好给我们一个无聊的代码,而不是一个干净的代码,显而易见的变化是:我发现,如果水果挂得足够低并且易于采摘,人们会更愿意修改代码库。 最好的代码可能是您刚刚查看并立即了解其工作方式的代码。

  • 不会试图看起来丑陋或看起来有趣的代码。
  • 错误明显且行为明确的代码,不同于没有明显错误且行为不明确的代码。
  • 与追求完美的代码相反,在其中记录的代码并不理想。
  • 代码具有如此明显的行为,以至于任何开发人员都可以想出多种不同的方式来修改此代码。

有时,代码太讨厌了,以至于使它变得更干净的任何尝试只会加剧这种情况。 在不了解其行为后果的情况下编写代码也可以视为调用方便维护的代码的一种习惯。

我不想说干净的代码是不好的,但是有时候对干净的渴望更像是在地毯下扫垃圾。 调试代码不一定是干净的。 充斥着检查或错误处理的代码很少可读。

规则1:计算机中始终存在问题


计算机有问题,程序在上次运行期间崩溃。

在尝试做某事之前,应用程序必须首先确保它从已知的,良好的,安全的状态开始。 有时,根本没有状态副本,因为用户删除了状态或升级了计算机。 该程序在上次运行时崩溃,而且也有悖于第一次运行时崩溃。

例如,在读取或写入文件状态时,可能会出现以下问题:

  • 该文件丢失。
  • 该文件已损坏。
  • 旧版本或较新的文件。
  • 对文件的最后更改未完成
  • 文件系统对您说谎。

这些问题并不是新问题;自古以来(1970-01-01),数据库就遇到了这些问题。 使用类似SQLite的方法将有助于解决许多类似的问题,但是如果程序在最后一次执行时崩溃,则代码可能会处理错误的数据和/或使用错误的方式。

例如,对于预定程序,此列表中的内容将发生:

  • 由于夏令时,该程序将在一小时内启动两次。
  • 该程序将启动两次,因为操作员已经忘记它已经在运行。
  • 由于可用磁盘空间不足或神秘的云或网络问题,该程序将延迟启动。
  • 该程序将运行一个多小时,这可能导致以后对该程序的调用延迟。
  • 该程序将在一天的错误时间启动。
  • 该程序将不可避免地在某个边界时间之前(例如,午夜,月末或年末)执行,并且由于计算错误而失败。

创建可持续性软件始于编写认为上次性能下降的软件,如果您不知道该怎么办,则会崩溃。 抛出异常并以“这不应该发生”的方式留下评论的最好的事情是,当这种情况不可避免地发生时,您将有一个调试代码的先机。

该程序甚至没有义务从故障中恢复,它足以使其放弃并且不会使情况恶化。 生成异常的小检查可以节省数周的侦听时间,而简单的锁定文件可以节省数小时的备份恢复时间。

易于调试的代码是:

  • 在执行要求的代码之前检查一切是否正常的代码;
  • 使返回已知状态并重试的代码变得容易
  • 以及具有导致错误尽早发生的安全级别的代码。

规则2:您的程序与自己抗争


Google历史上最大的DoS攻击来自我们自己(因为我们的系统非常庞大)。 尽管有时会有人尝试测试我们的力量,但我们仍然比其他人更能伤害自己。

这适用于我们所有的系统。

长期游戏工程师Astrid Atkinson

程序总是在上一次运行时崩溃;始终没有足够的处理器,内存或磁盘空间。 所有工作人员都在排空队列,每个人都在尝试重复失败且过时的请求,并且所有服务器在垃圾收集期间同时暂停。 该系统不仅在崩溃,而且还在不断地试图破坏自身。

极大的困难甚至可能导致检查系统。

实施服务器操作检查可能很容易,但前提是它不处理请求。 如果您不检查连续运行时间,则该程序可能介于两次检查之间。 运行状况检查也可能触发错误:我必须编写导致系统崩溃的检查,并由他们进行保护。 两次,相差三个月。

错误处理代码将不可避免地导致发现更多需要处理的错误,其中许多错误是由错误处理本身引起的。 同样,性能优化通常是系统瓶颈的原因。 很好地在一个选项卡中使用的应用程序会变成一个问题,并以20个副本启动。

另一个示例:管道中的工作程序运行得太快,并且在管道的下一部分访问它之前消耗了可用的内存。 这可以与交通拥堵相提并论:它们是由于速度提高而产生的,结果交通拥堵向相反的方向发展。 因此,优化通常会以某些神秘的方式生成高负载或重负载的系统。
换句话说:系统越快,其承受的压力就越大,如果您不允许系统稍微抵消作用,那么如果系统破裂,也不要感到惊讶。

对抗是系统的一种反馈形式。 该程序易于调试,使用户参与了反馈循环,使您可以查看系统内的所有行为,包括随机,有意,期望和不期望的行为。 您可以轻松检查此类代码,查看并了解其发生的更改。

规则3:如果您现在不明确,请稍后再调试


换句话说,您应该很容易跟踪程序中的变量并了解正在发生的情况。 对于噩梦线性代数的任何例程,您都应努力使程序的状态尽可能明显。 这意味着在程序中间,您无法更改变量的用途,因为将一个变量用于两个不同的用途是致命的罪过。

这也意味着您需要谨慎地避免半谓词问题,切勿使用单个值( count )来表示一对值( booleancount )。 有必要避免为结果返回正数,如果没有匹配项,则同时返回-1 。 事实是,在需要诸如“ 0, but true ”之类的情况下,您很容易发现自己(这正是Perl 5中的功能); 或者创建的代码难以与系统的其他部分结合使用(对于程序的下一部分, -1可能不是错误,而是有效的输入值)。

除了将一个变量用于两个目的外,建议不要将两个变量用于同一目的,尤其是布尔值时。 我并不是要说使用两个数字来存储范围是不好的,但是使用布尔数表示程序状态通常是被屏蔽的状态机。

当状态没有从上到下传递时,也就是在情节周期的情况下,最好为状态提供自己的变量并清除逻辑。 如果对象内部有一组布尔值,则将其替换为一个名为state的变量,并使用enum(或在必要时使用字符串)。 if表达式看起来像if state == name ,而不是if bad_name && !alternate_option

即使您创建了显式的状态机,也有可能会造成混淆:有时,代码中可能包含两个隐藏的状态机。 一旦折磨我编写HTTP代理,直到我明确了每台计算机,跟踪了连接状态并分别解析了它。 当将两个状态机组合为一个状态机时,可能很难添加新状态或确切了解某物应具有的状态。

与创建不需要调试的代码相比,更多的是关于轻松调试。 如果您列出了正确的状态列表,那么丢弃不正确的状态而不会意外丢失一两个状态会更加容易。

规则4:随机行为是预期行为。


当您不了解数据结构的功能时,这些知识空白将由用户填补:任何有意或无意的代码行为最终都将依赖于某种东西。 许多流行的编程语言都支持可迭代的哈希表,并且在大多数情况下,哈希表在插入后仍保持顺序。

在某些语言中,哈希表的行为满足了大多数用户的期望,并按添加键的顺序对其进行迭代。 换句话说,哈希表在每次迭代时都以不同的顺序返回键。 在这种情况下,一些用户抱怨该行为不够随机。

不幸的是,程序中的任何随机性源最终都将用于统计模拟,甚至更糟的是-加密; 并且将使用任何排序源进行排序。

在数据库中,某些标识符比其他标识符包含更多的信息。 通过创建表,开发人员可以在不同类型的主键之间进行选择。 正确的选择是UUID,或与它没有区别的东西。 其他选择的缺点是它们可以公开订购和标识信息。 也就是说,不仅a == b ,而且a <= b ,其他选项也意味着自动递增键。

当使用自动增量键时,数据库为表中的每一行分配一个数字,在插入新行时将其加1。 排序很模糊:人们不知道数据的哪一部分是规范的。 换句话说,您是按键还是按时间戳排序? 与哈希表一样,人们自己会选择正确的答案。 另一个问题是用户可以使用其他键轻松预测相邻记录。

但是,任何企图超过UUID的尝试都会失败:我们已经尝试使用邮政编码,电话号码和IP地址,每次失败都可悲。 UUID可能不会使您的代码更容易调试,但是更少的随机行为意味着更少的麻烦。

从键中,您不仅可以提取有关订购的信息。 如果在数据库中基于其他字段创建密钥,那么人们将丢弃数据并从密钥中恢复数据。 并且会出现两个问题:当程序的状态存储在多个位置时,副本之间会很容易产生分歧。 如果您不确定哪一个需要更改或哪个已更改,则同步它们将更加困难。

无论您允许用户做什么,他们都会这样做。 编写易于调试的代码意味着思考滥用它的方式,以及人们通常如何与之交互。

规则5:调试是一项社会任务,首先是一项技术任务。


当项目分为组件和系统时,查找错误可能会困难得多。 通过了解问题是如何产生的,您可以协调不同部分的更改以纠正行为。 在大型项目中修复错误并不需要太多地寻找它们就可以说服人们这些错误的存在或存在的可能性。
该软件中存在错误,因为没有人完全确定谁对什么负责。 也就是说,在什么都不写的情况下调试代码变得更加困难,您必须询问Slack中的所有内容,并且只有一位专家来之前没人回答。

这可以通过计划,工具,过程和文档来解决。

规划是摆脱保持联系,事件管理结构压力的一种方法。 这些计划使您可以通知买家,释放已经联系了太久的人员,还可以跟踪问题并进行更改以降低未来的风险。 工具-一种减少执行某些工作的要求的方法,以便其他开发人员可以更轻松地访问它。 流程是从单个参与者中删除管理功能并将其传递给团队的一种方式。

人员和交互方式将发生变化,但是随着团队的转变,流程和工具将保持不变。 并不是说一个比另一个重要,而是一个设计用来支持另一个的变化。 该过程还可用于从团队中删除控制功能。 这并不总是好事或坏事,但是总会有某种过程,即使没有阐明。 记录下来的行为是让其他人更改此过程的第一步。

文档不仅仅是文本文件。 这是一种转移责任,您如何使人们工作,如何向受到这些变更影响的人员报告变更的方法。 与编写代码时相比,编写文档需要更多的同理心和更多的技能:没有简单的编译器标志或类型检查,并且可以轻松编写很多单词而无需编写任何文档。

没有文档,就无法期望别人做出明智的决定,甚至不能同意使用该软件的后果。 没有文档,工具或流程,就无法分担维护负担,或者至少不能替代现在正在解决问题的人员。

促进调试的愿望不仅适用于代码本身,而且适用于与代码相关的过程,这有助于了解您需要进入谁的皮肤来修复代码。

易于调试的代码很容易解释。


有一种观点认为,如果您在调试过程中向某人解释了问题,那么您自己就可以理解。 为此,您甚至不需要别人,主要是迫使自己从头开始解释情况,解释播放顺序。 通常这足以做出正确的决定。

如果只是。 有时,当我们寻求帮助时,我们不问需要什么。 这是如此普遍,以至于被称为XY问题:“ 如何获取文件名的最后三个字母? ?? 不,我的意思是扩张 。”

我们以我们理解的解决方案来谈论问题,并且以我们担心的后果来谈论解决方案。 调试是难以理解的意外结果和替代解决方案;它要求程序员最困难:承认他理解错误。

原来这不是编译器错误。

Source: https://habr.com/ru/post/zh-CN412693/


All Articles