programming 2025年1月1日

Tidy First

整理优先:软件设计的躬行实践
书籍封面

Tidy First

作者:Kent Beck 出版日期:2023-11-28 出版社:O'Reilly Media
该书旨在帮助程序员改进软件设计,核心思想是“优先整理”(Tidy First),即在进行大的代码修改之前,先进行小的代码重构(称为“整理”),以简化代码结构,降低修改难度。书中详细介绍了多种“整理”技巧,并探讨了软件设计背后的理论,包括耦合、内聚、时间价值和期权等经济学概念,最终目的是提升程序员的工作效率和幸福感,并促进团队协作。

这本书的作者是谁?肯特·贝克 (Kent Beck) 。肯特·贝克 (Kent Beck) 是谁? 他是最早研究软件开发的模式和重构的人之一,软件工程领域泰斗,敏捷开发的开创者之一,更是极限编程和测试驱动开发的创始人,同时还是JUnit 的作者,

大家熟知的《重构》这本书中文版上的作者写的是 Martin Fowler,但是实际上英文的作者是 Martin Fowler with contributions by Kent Beck,也就是说肯特·贝克也是这本书的作者之一。

《重构》这本书前言中写道:

后来,这个项目重新启动,几乎从头开始编写整个系统,Kent Beck受邀做了顾问。他做了几件迥异以往的事,其中最重要的一件就是坚持以持续不断的重构行为来整理代码。这个团队效能的提升,以及重构在其中扮演的角色,启发了我撰写本书的第1版,如此一来我就能够把Kent和其他一些人已经学会的“以重构方式改进软件质量”的知识,传播给所有读者。

我之所以介绍这么多,其实是你想告诉大家,肯特·贝克是一个非常有影响力的人,尤其是在软件工程领域,他的书肯定是值得一读的。

肯特·贝克最新推出的这本书叫做《整理优先:软件设计的躬行实践》,英文名叫《Tidy First: Software Design in Practice》,这本书的出版日期是 2023 年 11 月 28 日,出版社是 O’Reilly Media。

有点遗憾呀,这本书在亚马逊上评分只有 4.5 星, 这本书并没有达到《设计模式》、《重构》这两本书的高度,有人说这本书出的太仓促,才100页左右并没有深入探测,也有人说这本书并没有谈论太多新的东西。 尤其后半部分(第四部分)花了十几章在介绍他的这个理论,有点走火入魔了,一线的程序员更喜欢前面实战型的技术,管理者也不关心后面的这些理论。也许是这本书不火的原因,相比而言,《设计模式》、《重构》慢慢都是干货。

不管怎样,我们都有自己的判断,让我们来看看这本书的内容吧,或许对我们的编程生涯有所帮助。

“Tidy First”(整理优先)是由 Kent Beck 主要推广的一个理念。它强调通过小型、渐进的改进来提升代码的可读性和可维护性,主要关注那些不太可能引发问题的**“小而精致的重构”**。相比之下,“重构”(refactoring)是指在不改变代码外部行为的前提下对代码进行重组的更广泛实践,可能包括较大的结构性变更和小型整理工作。这两者的核心区别在于,“先整理"优先考虑让代码更清晰易懂,然后才着手进行更大规模的功能性改动。

主要区别

  • 范围:“先整理"专注于小型的表面改进,如变量重命名、添加注释和改善格式,而重构则可能涉及更大规模的结构性变更,如代码重组、函数提取或算法优化。
  • 目的:“先整理"的首要目标是立即改善代码的可读性和可维护性,而重构则致力于通过解决潜在的结构性问题来提升代码质量。
  • 对功能的影响:“先整理"通常不会改变代码的功能,而重构可能会在保持整体功能的同时修改代码行为。

为什么选择"先整理”

  • **更容易采用:**相比大规模的重构工作,小型渐进式的改变更不容易引起争议,更容易获得开发者和项目经理的认可。
  • **主动维护:**定期整理代码有助于防止技术债务的积累,使未来的改动更加容易。
  • **更好的理解:**格式一致、整洁的代码更容易让开发者理解和使用。

第一部分:引言

  • 软件设计的核心目标:帮助技术人员在工作中感到安全。软件设计既是强大的工具,如果运用得当,可以减轻世界的痛苦;反之,则会成为压迫的工具,阻碍软件开发效率。
  • 软件设计是一种人际关系:本书是关于程序员与自身关系的软件设计三部曲的第一部。整理(tidying)是微小的重构,改变结构以使行为更容易改变。
  • 本书的受众:本书面向程序员、首席开发人员、实践型软件架构师和技术经理。它不依赖于任何特定的编程语言,所有开发人员都可以应用书中的概念。
  • 本书的目标:通过本书,读者将理解系统行为的改变和结构的改变之间的根本区别。读者也将能够通过先整理(tidy first)或后整理(tidy after)来改善自己的编程体验,开始以小的、安全的步骤进行大的更改。
  • 本书的结构:本书包括引言、整理(tidyings)、管理整理和理论四个部分。重点在于从小处着手,逐步掌握软件设计。

第二部分:整理(Tidyings)

  • 核心概念:整理类似于微型的重构,目的是使代码的结构更易于修改,从而更容易改变其行为。
  • 何时进行整理:当整理能够使行为的改变更容易时,就应该进行整理。

第一章:卫语句(Guard Clause)

  • 问题:嵌套的条件语句使代码难以阅读和理解。
  • 解决方案:将嵌套的条件语句转换为卫语句,提前返回,简化代码的逻辑。
  • 示例
    if (condition)
      if (not other condition)
        ...some code...
    整理为:
    if (not condition) return
    if (other condition) return
    ...some code...
  • 注意事项
    • 不要过度使用卫语句,过多的卫语句也会使代码难以阅读。
    • 只有当代码结构完全匹配提示时才进行卫语句整理。

第二章:死代码(Dead Code)

  • 问题:未执行的代码会增加代码的复杂性。
  • 解决方案:直接删除未执行的代码。
  • 注意事项
    • 删除死代码可能让人感觉奇怪,但版本控制可以确保代码不会永久丢失。
    • 在删除可疑的死代码之前,先记录其使用情况,并在生产环境中观察一段时间。
    • 每次删除的代码量要小,以便在需要时可以轻松回滚。

第三章:标准化对称性(Normalize Symmetries)

  • 问题:由于不同的人在不同的时间以不同的方式解决相同的问题,导致代码不一致,难以阅读。
  • 解决方案:选择一种标准方式,将不一致的代码转换为标准形式。
  • 示例:对于延迟初始化的变量,可能存在多种写法,应该选择一种统一的写法。
  • 目标:通过一致性减少读者的困惑。

第四章:新接口,旧实现(New Interface, Old Implementation)

  • 问题:现有接口难以使用或令人困惑。
  • 解决方案:实现一个你希望调用的新接口,并通过调用旧接口来实现新接口。
  • 意义:这是微观尺度的软件设计本质。通过这种方式,更容易做出行为上的改变。

第五章:阅读顺序(Reading Order)

  • 问题:代码的阅读顺序可能不符合读者理解逻辑,重要细节可能出现在文件末尾。
  • 解决方案:按照读者期望的顺序重新排列代码。
  • 原则:你是读者,所以你知道如何排列代码。
  • 注意事项
    • 不要在重新排序的同时应用其他整理。
    • 注意语言对声明顺序的敏感性。

第六章:内聚顺序(Cohesion Order)

  • 问题:为了做出行为更改,可能需要修改分散在代码中的多个位置。
  • 解决方案:重新排列代码,使需要更改的元素相邻。
  • 适用范围:适用于文件中的例程,目录中的文件,甚至是跨仓库的代码。
  • 目的:增加内聚性,使行为更改更容易。

第七章:将声明和初始化放在一起(Move Declaration & Initialization Together)

  • 问题:变量的声明和初始化可能分开,使代码难以阅读。
  • 解决方案:将初始化移动到声明附近。
  • 考虑因素
    • 变量的声明和初始化应该在它们被使用之前。
    • 必须维护变量之间的数据依赖关系。

第八章:解释变量(Explaining Variable)

  • 问题:复杂的表达式难以理解。
  • 解决方案:将子表达式提取到以表达式意图命名的变量中。
  • 目的:提高代码的可读性和可维护性。

第九章:解释常量(Explaining Constant)

  • 问题:代码中出现未知的数字或重复的字符串常量。
  • 解决方案:创建符号常量,并用符号替换字面常量。
  • 目的:提高代码的可读性和可维护性。
  • 注意事项
    • 同一个字面量在不同地方可能含义不同,需要注意。

第十章:显式参数(Explicit Parameters)

  • 问题:例程使用隐式参数,例如从全局变量或映射中获取数据。
  • 解决方案:将参数显式传递给例程。
  • 示例:将从 map 中获取参数改为显式传递参数。
  • 目的:提高代码的可读性、可测试性和可分析性。

第十一章:分块语句(Chunk Statements)

  • 问题:代码块过长,难以理解。
  • 解决方案:在代码块之间添加空行,进行分块。
  • 意义:这是最简单的整理方式,体现了“不要把软件设计看得太重要而不敢做”的理念。

第十二章:提取助手函数(Extract Helper)

  • 问题:例程中的代码块具有明显的用途,并与例程的其余部分交互有限。
  • 解决方案:将代码块提取为助手函数,并根据用途而不是如何工作的命名。
  • 特殊情况:如果要更改较大程序中的几行代码,则提取这些行作为助手函数,更改助手中的行,然后再将助手内联回调用例程(通常会发现自己喜欢助手并保留它)。
  • 目的:提高代码的可读性和可重用性。

第十三章:一堆(One Pile)

  • 问题:代码被分割成许多小块,但分割的方式阻碍了理解。
  • 解决方案:将代码尽可能多地内联到一大块中,然后再进行整理。
  • 症状:长而重复的参数列表、重复的代码、助手例程命名不佳、共享的可变数据结构。
  • 目的:通过将代码聚集在一起,更容易理解其结构。

第十四章:解释注释(Explaining Comment)

  • 问题:代码的意图不明显。
  • 解决方案:添加解释代码意图的注释。
  • 原则:只写下代码中不明显的内容。站在未来读者或你 15 分钟前的角度思考,你会希望知道什么。
  • 示例:在文件开头添加注释,说明该文件的用途。
  • 最佳实践:发现缺陷时立即添加注释。

第十五章:删除冗余注释(Delete Redundant Comment)

  • 问题:注释与代码重复,没有提供额外的信息。
  • 解决方案:删除与代码重复的注释。
  • 核心观点:代码的目的是向其他程序员解释你想要计算机做什么,注释和代码为作者和未来的读者呈现不同的权衡。
  • 原则
    • 如果注释是完全多余的,则删除它。
    • 要以使注释完全多余为目标进行整理。

第三部分:管理

  • 整理是技术人员的自我关怀:整理是软件设计,它关注你、你与代码的关系以及最终你与自己的关系。
  • 整理是一个需要管理的流程:仅仅能够识别和应用整理并不意味着你已经掌握了整理。本书的标题是“先整理?”,重点在于问号。这承认了并非所有情况下都应该整理。
  • 整理和代码审查:整理应该放在单独的拉取请求(PR)中,每个PR包含尽可能少的整理。
  • 分离整理和行为变更:整理属于结构上的变化,而行为变更属于功能上的变化,应该分开处理。
  • 整理的节奏
    • 整理是为了使将来的行为变更更容易。
    • 整理通常在几分钟到一个小时内完成。
  • 解开纠结
    • 当你发现自己陷入了整理和变更的混乱时,请尝试丢弃进行中的工作并重新开始,先整理。
    • 尽早意识到需要解开纠结,可以减小工作量。

第十六章:分离整理(Separate Tidying)

  • 问题:将整理和行为变更混合在同一个PR中,会导致PR过长,难以审查。
  • 解决方案:将整理放在单独的PR中,每个PR尽可能少地包含整理。
  • 目标:使代码审查更容易,鼓励更小的、更集中的PR。
  • 评审延迟:代码评审的延迟会影响你如何分割PR,快速的评审会鼓励更小的 PR。
  • 实验:在信任和文化强大的团队中,可以尝试不要求对整理PR进行审查。

第十七章:链式整理(Chaining)

  • 核心思想:整理就像薯片,你吃了一片就会想吃下一片。管理不断整理的冲动是整理的关键技巧。
  • 整理的链式效应
    • 卫语句后,条件可能需要提取为解释助手或解释变量。
    • 删除死代码后,可以更容易地按照阅读顺序或内聚顺序对代码进行排序。
    • 标准化对称性后,可以更容易地对并行代码进行分组。
    • 创建新接口后,你需要逐步迁移所有调用者。
    • 建立阅读顺序后,可能会看到标准化对称性的机会。
    • 为了内聚顺序分组的元素是提取到子元素的候选对象。
    • 解释变量右侧的赋值表达式是解释助手的候选对象。
    • 提取解释常量会引导到内聚顺序。
    • 显式参数后,可以将一组参数分组到一个对象中并将代码移入该对象。
    • 你可以用解释注释来开始每个块,你也可以把一个块提取为一个解释助手。
    • 完成一堆后,可以使用分块语句,添加解释注释并提取助手进行整理。
    • 如果可能,请通过引入解释变量,解释常量或解释助手将注释中的信息移动到代码中。
    • 消除冗余注释的干扰可以帮助你看到更好的阅读顺序或看到显式参数的机会。
  • 重点:你的工作是为自己和你的团队在现在和未来取得成功做好准备。

第十八章:批次大小(Batch Sizes)

  • 核心问题:在集成和部署之前,应该进行多少整理。
  • 考虑因素
    • 为了支持下一个行为变更,需要进行多少整理。
    • 多少整理容易集成和部署。
  • 权衡
    • 太少的整理批次会导致整合困难。
    • 太多的整理批次会增加碰撞、交互和投机。
  • 解决方案:减少审查的成本,鼓励更小的整理批次。
  • 信任和文化:在信任和文化强大的团队中,整理不需要审查。

第十九章:节奏(Rhythm)

  • 重点:管理整理的节奏是管理整理的艺术的一部分。
  • 软件设计的规模:本书讨论的是个人影响力的软件设计,时间尺度通常是几分钟到一小时。
  • 整理的聚类效应:行为变更往往聚集在代码中,整理也倾向于聚集在这些地方。
  • 持续的改进:即使刚开始你整理了很多,很快你就会发现自己想在已经整理好的代码中进行行为变更。

第二十章:解开纠结(Getting Untangled)

  • 问题:你在修改代码行为时,发现并应用了许多整理,最后陷入整理和变更的混乱中。
  • 不可取的选项
    • 直接发布,不礼貌且容易出错。
    • 将整理和更改解开为单独的PR,工作量大。
  • 可取的选项:丢弃进行中的工作并重新开始,先整理,这会让提交链更连贯。
  • 沉没成本谬误:不要因为已经取得了一些进展而害怕丢弃工作。

第二十一章:先,后,稍后,永不(First, After, Later, Never)

  • 整理的时机:关于行为变更,整理的时机有以下四种选择:先整理、后整理、稍后整理和永不整理。
  • 永不整理(Never)
    • 当代码的行为永远不会改变时,可以选择永不整理。
    • 这种情况很少见。
  • 稍后整理(Later)
    • 当你没有足够的时间或者精力,可以将整理放到稍后进行。
    • 创建一个“乐趣列表”,记录需要稍后整理的内容。
    • 稍后整理可以减少混乱带来的影响,并帮助你更好地理解设计。
    • 在没有精力处理新功能时,整理也是一种很好的选择。
  • 后整理(After)
    • 当你需要在同一区域再次更改行为时,后整理是一种合理的选择。
    • 如果等待到下次再整理的成本更高,那么在现在整理可能更有意义。
    • 如果整理的成本与行为变更的成本大致成比例,则可以考虑后整理。
  • 先整理(First)
    • 当整理能够立即带来收益时(例如,改进代码的理解或降低行为变更的成本),应该优先考虑先整理。
    • 当你知道要整理什么以及如何整理时,先整理。
  • 总结
    • 永不整理:当你永远不会更改此代码时,或者当你无法通过改进设计来学习时。
    • 稍后整理:当你有一大批没有立即回报的整理时,或者当完成整理时会获得最终回报时,或者你可以在小批量中整理时。
    • 后整理:当等待到下次再整理的成本更高时,或者如果你不整理会感觉没有完成感时。
    • 先整理:当它会立即获得回报(提高理解或降低行为变更的成本)时,并且你知道要整理什么和如何整理时。

第四部分:理论

  • 理论的目的:理论不能说服人,但可以优化应用。理解理论可以帮助你在需要根据推测做出决定时提高你的判断力。
  • 软件设计中的永恒问题
    • 何时开始做出软件设计决策?
    • 何时停止做出软件设计决策并着手更改系统的行为?
    • 如何做出下一个决定?
  • 理论可以让你建设性地与同行意见不合:当你们的根本目标不同时,理论框架可以帮助你们达成共识,并互相学习。

第二十二章:有益地关联元素(Beneficially Relating Elements)

  • 软件设计的定义:有益地关联元素。
  • 元素
    • 元素具有边界,你知道它们从哪里开始和结束。
    • 元素包含子元素。
  • 关联
    • 元素之间存在关系,例如调用、发布、监听和引用.
  • 有益地
    • 设计是通过创建中间元素来使元素彼此受益,从而使系统更容易更改。
  • 软件设计师能做的事情
    • 创建和删除元素
    • 创建和删除关系
    • 增加关系的益处。
  • 系统结构:系统结构包括元素层次结构,元素之间的关系以及这些关系带来的好处。

第二十三章:结构和行为(Structure & Behavior)

  • 软件创造价值的两种方式
    • 它今天的功能(行为)。
    • 它明天可以实现的新功能(可选性)。
  • 行为
    • 可以通过输入/输出对和不变量来描述。
    • 行为创造价值,让计算机在几秒钟内完成人们手工需要花费大量时间才能完成的事情。
  • 结构
    • 结构创建可选性,使系统更容易扩展和维护。
    • 结构与系统的行为无关,但它会影响更改系统的难易程度。
  • 挑战
    • 结构的可见性不如行为,难以评估结构方面的投入是否足够。
  • 重点:结构更改和行为更改都是创造价值的方式,但它们本质上是不同的。

第二十四章:经济学:时间价值和可选性(Economics: Time Value & Optionality)

  • 金钱的本质:金钱代表“冻结的欲望”,它本身具有时间和风险的价值。
  • 关于金钱的两个重要原则
    • 今天的钱比明天的钱更有价值,所以要尽早赚钱,延迟花费。
    • 在混乱的情况下,选择比事物更好,因此要在不确定性中创造选择。
  • 软件设计:软件设计是协调“尽早赚钱/延迟花费”和“创造选择而不是事物”的原则的一种方式。

第二十五章:今天的一美元 > 明天的一美元 (A Dollar Today > A Dollar Tomorrow)

  • 金钱的价值取决于:时间、确定性。
  • 贴现现金流
    • 如果今天给你一美元,你可以用它来购买你想要的东西,或者你可以用它进行投资,从而在以后给你带来更多的钱。
    • 明天的一美元不如今天的一美元有价值。
  • 如何评估软件系统
    • 将软件系统建模为一组现金流,每个现金流都与一个日期相关联。
  • 时间价值:鼓励后整理而不是先整理,因为我们会在之后赚钱,而不是花费。

第二十六章:可选性(Options)

  • 可选性的价值
    • “我接下来可以实现的行为”本身具有价值,甚至在实现之前。
    • “我接下来可以实现的行为”越多,价值就越高。
    • 对价值的预测越不确定,选择的价值就越大。
  • 软件设计和可选性
    • 软件设计是为变更做准备,而行为变更则是故事中的土豆。
    • 我们今天做的设计是我们为“明天购买”行为变更的“选择权”而支付的溢价。
  • 重要原则
    • 潜在的行为变更的价值越不稳定越好。
    • 开发时间越长越好。
    • 未来开发成本越低越好。
    • 为创造一个选项而做的设计工作越少越好。

第二十七章:可选性与现金流(Options Versus Cash Flows)

  • 经济上的拉锯战:贴现现金流告诉我们尽早赚钱并减少支出。另一方面,期权告诉我们现在花钱是为了以后赚更多的钱。
  • 何时先整理?
    • 当整理的成本加上整理后进行行为变更的成本小于不整理进行行为变更的成本时。
    • 即使短期经济不鼓励,你可能仍然想要先整理。你需要评估整理所创建的选项的价值。
    • 你甚至可能只是为了让后续的行为变更更令人愉快而先整理。
  • 重要技能
    • 习惯于意识到影响软件设计时间和范围的因素。
    • 在与直接同事和更远的同事打交道时练习关系技巧。

第二十八章:可逆的结构变更(Reversible Structure Changes)

  • 结构变更与行为变更的区别:结构变更是可逆的,而行为变更通常是不可逆的。
  • 可逆决策的处理:由于没有太大的避免错误的价值,因此不应在避免错误上投入太多。
  • 代码审查的弊端:代码审查过程没有区分可逆的和不可逆的更改。
  • 使不可逆的决策可逆:例如通过逐步实现,可以使用功能标志等。
  • 理想主义的思维方式:避免犯错是不现实的,因此需要接受和利用可逆性的价值。

第二十九章:耦合(Coupling)

  • 耦合的定义:如果更改一个元素需要更改其他元素,则两个元素在特定更改方面是耦合的。
  • 耦合的要素
    • 两个元素是耦合的,必须指定是哪方面的变化。
    • 并非所有的耦合都值得担心。只有当变化真实发生时,耦合才会带来问题。
  • 耦合如何驱动软件成本
    • 1-N:一个元素可以通过更改与其他任意数量的元素耦合。
    • 级联:一个变更可以触发另一个变更,从而导致连锁反应。
  • 耦合的影响
    • 耦合会增加软件的维护成本。
    • “复杂”意味着更改具有意外的后果。
  • 何时先整理?:当你发现混乱的更改与耦合有关时,可以考虑通过整理减少耦合。

第三十章:康斯坦丁的等价性(Constantine’s Equivalence)

  • 软件成本的主要组成部分:软件成本大约等于更改软件的成本。
  • 变化成本
    • 并非所有变化都是相同的。某些变化比其他变化成本更高。
    • 最昂贵的行为变更总共比所有最不昂贵的行为变更花费更多。
  • 耦合与软件成本
    • 昂贵的变更成本来自耦合。
    • 软件成本与耦合近似相等。
    • 为了降低软件的成本,我们必须降低耦合。

第三十一章:耦合与解耦(Coupling Versus Decoupling)

  • 为什么会有耦合?
    • 为了尽早实现收入和延迟支出,可能选择快速实现有耦合的代码。
    • 某些耦合是不可避免的,或者是直到现在才成为问题。
  • 面临的选择:你今天面临的选择是支付耦合的成本还是支付解耦的成本。
  • 一个案例
    • 一个简单的通信协议,包括发送和接收功能。
    • 使用接口定义语言,可以实现解耦。
    • 尽管减少了一种耦合,但可能会出现其他耦合。
  • 权衡:耦合的成本与解耦的成本之间存在权衡。
  • 总结:你可以在这个连续体的任何地方支付耦合的成本或支付解耦的成本。

第三十二章:内聚(Cohesion)

  • 内聚的含义
    • 耦合的元素应该是同一个包含元素的子元素。
    • 不耦合的元素应该放在其他地方。
  • 提高内聚的两种方式
    • 将耦合的元素捆绑到它们自己的子元素中。
    • 将不耦合的元素移动到其他地方。
  • 提取助手:提取助手函数是“提取内聚子元素”的一种形式。
  • 注意:在不完整的信息下工作,不要大幅重新安排,一次移动一个元素。

第三十三章:结论(Conclusion)

  • 决定是否先整理的因素
    • 成本:整理会使成本更小,更晚或可能性更低吗?
    • 收入:整理会使收入更大,更早或可能性更高吗?
    • 耦合:整理会使我需要更改的元素更少吗?
    • 内聚:整理会使我需要更改的元素在更小,更集中的范围内吗?
    • 自身:整理会给你的编程带来平静、满足和快乐吗?
  • 不要过度整理:整理是为了支持下一个行为变更,而不是为了整理而整理。
  • 软件设计是一种团队行为:要为下一个挑战(变革者和等待变革的人之间的关系)做好准备。