重构大师-一-
重构大师(一)
干净的代码
重构的主要目的是对抗技术债务。它将混乱转变为干净的代码和简单的设计。
不错!但究竟什么是干净的代码呢?以下是一些特点:
干净的代码对其他程序员来说是显而易见的。
我并不是在说超级复杂的算法。糟糕的变量命名、臃肿的类和方法、神秘数字——这些都会使代码显得混乱且难以理解。
干净的代码不包含重复。
每次你需要在重复代码中进行更改时,你必须记得对每个实例做出相同的更改。这增加了认知负担,并减慢了进度。
干净的代码包含最少数量的类和其他可变部分。
更少的代码意味着头脑中需要记住的内容更少。更少的代码意味着更少的维护。更少的代码意味着更少的错误。代码是负担,保持简短和简单。
干净的代码通过所有测试。
当你的测试通过率只有 95%时,你就知道你的代码不够干净。当你的测试覆盖率为 0%时,你知道你完蛋了。
干净的代码更容易维护,也更便宜!
干净的代码
重构的主要目的是对抗技术债务。它将混乱转化为干净的代码和简单的设计。
不错!那么,干净的代码到底是什么呢?以下是它的一些特征:
干净的代码对其他程序员来说是显而易见的。
我并不是在谈论超级复杂的算法。糟糕的变量命名、臃肿的类和方法、魔法数字——这些都会使代码显得杂乱且难以理解。
干净的代码不包含重复。
每次你需要更改重复代码时,都必须记得在每个实例中做相同的更改。这增加了认知负担,并减慢了进度。
干净的代码包含最少的类和其他可变部分。
更少的代码意味着头脑中需要记住的东西更少。更少的代码意味着更少的维护。更少的代码意味着更少的错误。代码是一种负担,保持简短和简单。
干净的代码通过所有测试。
当你的测试只有 95%通过时,你就知道你的代码不干净。当你的测试覆盖率为 0%时,你就知道你完蛋了。
干净的代码更容易且更便宜维护!
技术债务
每个人都尽力从头开始编写优秀的代码。可能没有程序员故意编写不干净的代码来损害项目。但在什么情况下,干净的代码变得不干净?
关于不干净代码的“技术债务”隐喻最初由沃德·坎宁汉姆提出。
如果你从银行贷款,这允许你更快地进行购买。你需要额外支付加快过程的费用——你不仅要偿还本金,还要支付额外的利息。不用说,你甚至可能积累如此多的利息,以至于利息总额超过你的总收入,使得完全偿还变得不可能。
同样的事情也可能发生在代码中。你可以暂时加速,而不为新功能编写测试,但这将逐渐每天减缓你的进度,直到你最终通过编写测试来偿还债务。
技术债务的原因
业务压力
有时商业环境可能迫使你在功能完全完成之前推出功能。在这种情况下,代码中将出现补丁和应急措施,以隐藏项目未完成的部分。
对技术债务后果缺乏理解
有时你的雇主可能不理解技术债务的“利息”,因为它随着债务的积累而减缓开发进度。这可能使得团队难以投入时间进行重构,因为管理层看不到其价值。
未能抵制组件之间的严格一致性
此时,项目看起来像一个整体,而不是各个模块的产品。在这种情况下,对项目某一部分的任何更改都会影响其他部分。团队开发变得更加困难,因为很难隔离个别成员的工作。
缺乏测试
缺乏即时反馈会鼓励快速但冒险的变通方案或应急措施。在最糟糕的情况下,这些更改会在未经任何先前测试的情况下直接实施并部署到生产环境中。后果可能是灾难性的。例如,一个看似无害的热修复可能会向数千名客户发送奇怪的测试电子邮件,甚至更糟,冲掉或损坏整个数据库。
缺乏文档
这会减缓新成员加入项目的速度,如果关键人员离开项目,可能会使开发陷入停滞。
团队成员之间缺乏互动
如果知识库没有在公司内部分布,人们最终将会以过时的理解来处理项目中的流程和信息。当初级开发人员受到错误的培训时,这种情况可能会加剧。
长期在多个分支中同时开发
这可能导致技术债务的累积,当更改合并时,技术债务会增加。孤立进行的更改越多,累计的技术债务就越大。
延迟重构
项目的需求不断变化,某些时候,代码的部分可能会变得过时、繁琐,必须重新设计以满足新需求。
另一方面,项目的程序员每天都在编写与过时部分配合的新代码。因此,重构延迟越久,未来需要重新修改的依赖代码就会越多。
缺乏合规性监控
当项目中的每个人都按照自己的方式编写代码时(即与上一个项目相同的方式),就会发生这种情况。
无能
这发生在开发者根本不知道如何编写体面的代码时。
何时重构
三次原则
-
当你第一次做某件事时,只需完成它。
-
当你第二次做类似的事情时,虽然感到不适,但还是要照做。
-
当你第三次做某件事时,开始重构。
当添加新功能时
-
重构有助于你理解他人的代码。如果需要处理别人的肮脏代码,尝试先进行重构。干净的代码更容易理解。你不仅会为自己改善它,还会为之后使用它的人提升体验。
-
重构使得添加新功能变得更加容易。在干净的代码中进行更改要简单得多。
当修复错误时
代码中的错误就像现实生活中的错误:它们藏在代码最黑暗、最肮脏的地方。清理代码,错误几乎会自行暴露。
管理者欣赏主动重构,因为这消除了之后需要进行特殊重构任务的必要性。快乐的老板会让程序员也开心!
在代码审查期间
代码审查可能是整理代码的最后机会,之后它就会公开发布。
最好与作者配对进行此类审查。这样,你可以快速修复简单问题,并评估修复更困难问题的时间。
如何重构
重构应作为一系列小改动进行,每次改动使现有代码稍微变得更好,同时保持程序正常运行。
正确重构的清单
*#### 代码应该变得更干净。
如果重构后代码依然不整洁……好吧,我很抱歉,但你刚刚浪费了一个小时的生命。试着找出原因。
当你不再进行小改动的重构,而是将大量重构混合为一次大改动时,这种情况经常发生。因此,特别是在有时间限制的情况下,很容易失去理智。
但这也可能发生在处理极其糟糕的代码时。无论你改进什么,整体代码依然是一场灾难。
在这种情况下,值得考虑完全重写代码的某些部分。但在此之前,你应该编写测试并留出充足的时间。否则,你最终会得到我们在第一段中讨论的那种结果。
在重构期间不应创建新功能。
*不要混合重构与新功能的直接开发。尽量将这两个过程分开,至少在单个提交的范围内。
所有现有测试在重构后必须通过。
重构后测试可能崩溃的两种情况是:
-
你在重构过程中犯了错误。 这显而易见:去修复这个错误。
-
你的测试过于底层。 例如,你在测试类的私有方法。
在这种情况下,问题出在测试上。你可以重构测试本身,或编写一整套新的高层测试。避免这种情况的一个好方法是编写 BDD 风格的测试。****
重构目录
原文:refactoringguru.cn/refactoring/catalog
代码异味
原文:refactoringguru.cn/refactoring/smells
臃肿的代码
臃肿的代码是指那些膨胀到如此巨大的程度,以至于难以处理的代码、方法和类。通常这些坏味道不会立即显现,而是随着程序的发展而逐渐积累(尤其是当没有人努力去消除它们时)。
长方法
一个方法包含过多的代码行。一般来说,任何超过十行的方法都应该让你开始提出问题。
大型类
一个类包含许多字段/方法/代码行。
原始痴迷
-
对于简单任务使用原始类型而不是小对象(例如货币、范围、电话号码的特殊字符串等)。
-
使用常量来表示编码信息(例如常量
USER_ADMIN_ROLE = 1
用于表示具有管理员权限的用户)。 -
使用字符串常量作为数据数组中的字段名。
长参数列表
方法的参数超过三或四个。
数据块
有时代码的不同部分包含相同的变量组(例如用于连接数据库的参数)。这些代码块应该被转化为它们自己的类。
长方法
症状和征兆
方法包含的代码行数过多。一般来说,任何超过十行的方法都应该引发你的疑问。
问题原因
就像加州旅馆一样,方法总是不断添加内容,但从未删除任何东西。由于编写代码比阅读它更容易,这种“味道”在方法变成丑陋的过大怪物之前往往不会被注意到。
在心理上,创建一个新方法通常比向现有方法添加内容更难:“但这只是两行,没必要为此创建一个完整的方法……”这意味着又添加了一行,然后又一行,最终产生了纠缠的意大利面代码。
处理
一般经验法则是,如果你觉得需要对方法内部的某些内容进行注释,应该将这段代码提取到一个新方法中。即使是一行代码,如果需要解释,也可以并且应该被拆分为一个单独的方法。如果这个方法有一个描述性的名称,没人需要查看代码来了解它的功能。
-
为了减少方法体的长度,使用提取方法。
-
如果局部变量和参数干扰了提取方法,可以使用用查询替换临时变量、引入参数对象或保留整体对象。
-
如果之前的方法都没有帮助,尝试通过用方法对象替换方法将整个方法移动到一个单独的对象。
-
条件运算符和循环是代码可以移动到单独方法的良好线索。对于条件,使用分解条件。如果循环妨碍了代码,尝试提取方法。
回报
-
在所有类型的面向对象代码中,方法简短的类最持久。方法或函数越长,理解和维护的难度就越大。
-
此外,长方法为不必要的重复代码提供了完美的隐藏空间。
性能
增加方法数量会影响性能吗?正如许多人所声称的那样?在几乎所有情况下,影响微乎其微,甚至不值得担忧。
此外,既然你有清晰易懂的代码,如果有必要,你更有可能找到真正有效的方法来重构代码并获得实际的性能提升。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦了阅读?
不奇怪,阅读我们这里所有文本需要 7 小时。
试试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
大型类
征兆与症状
一个类包含许多字段/方法/代码行。
问题原因
类通常开始时很小,但随着程序的增长,它们会变得臃肿。
就像长方法一样,程序员通常发现将新功能放入现有类的心理负担较轻,而不是为该功能创建新类。
处理方案
当一个类承担太多(功能)角色时,考虑将其拆分:
-
提取类有助于当大型类的一部分行为可以分离为独立组件时。
-
提取子类有助于当大型类的一部分行为可以用不同方式实现或在少数情况下使用时。
-
提取接口有助于在需要列出客户端可以使用的操作和行为时。
-
如果大型类负责图形界面,您可以尝试将其部分数据和行为移动到一个独立的领域对象中。在此过程中,可能需要在两个地方存储一些数据的副本,并保持数据的一致性。重复观察数据提供了一种解决方法。
收益
-
对这些类的重构让开发人员不必记住类的大量属性。
-
在许多情况下,将大型类拆分成多个部分可以避免代码和功能的重复。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪阅读我们这里所有文本需要 7 个小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
原始迷恋
征兆和症状
-
为简单任务(例如货币、范围、电话号码的特殊字符串等)使用原始类型而不是小对象。
-
使用常量编码信息(例如常量
USER_ADMIN_ROLE = 1
以指代具有管理员权限的用户)。 -
将字符串常量用作数据数组中的字段名。
问题的原因
像其他大多数代码气味一样,原始迷恋是在脆弱时刻产生的。“只不过是一个存储数据的字段!”程序员说道。创建一个原始字段比创建一个全新的类要简单得多,对吧?于是就这样做了。然后又需要另一个字段,并以同样的方式添加。结果,类变得庞大而笨重。
原始数据类型常常用于“模拟”类型。因此,代替一个单独的数据类型,你拥有一组数字或字符串,形成某个实体允许值的列表。然后通过常量将这些特定数字和字符串赋予易于理解的名称,这就是它们广泛传播的原因。
另一个糟糕的原始使用示例是字段模拟。该类包含一个大型多样数据数组,且将字符串常量(在类中指定)用作获取此数据的数组索引。
处理方法
-
如果你有多种原始字段,可能可以将其中一些逻辑上分组到自己的类中。更好的是,将与这些数据相关的行为也移动到类中。为此任务,请尝试用对象替换数据值。
-
如果原始字段的值用于方法参数,请使用引入参数对象或保留整个对象。
-
当复杂数据被编码在变量中时,请使用用类替换类型代码、用子类替换类型代码或用状态/策略替换类型代码。
-
如果变量中有数组,请使用用对象替换数组。
收益
-
由于使用对象而非原始类型,代码变得更加灵活。
-
更好地理解和组织代码。特定数据的操作在同一地方,而不是分散开来。再也不用猜测这些奇怪常量的原因以及它们为何在数组中。
-
更容易找到重复代码。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读?
难怪,阅读我们这里所有的文本需要 7 小时。
试试我们的交互式重构课程。这是一种不那么乏味的学习新知识的方法。
让我们看看…
长参数列表
征兆与症状
方法的参数超过三到四个。
问题的原因
在将几种类型的算法合并到一个方法中后,可能会出现长参数列表。长列表可能是为了控制将运行哪个算法以及如何运行而创建的。
长参数列表也可能是为了使类之间更独立而产生的副产品。例如,创建方法所需特定对象的代码已从方法中移动到调用该方法的代码中,但创建的对象作为参数传递给方法。因此,原始类不再了解对象之间的关系,依赖性减少。但如果创建多个这样的对象,每个对象都将需要自己的参数,这就意味着更长的参数列表。
随着列表的增长,这样的列表很难理解,变得矛盾且难以使用。方法可以使用其自身对象的数据,而不是长长的参数列表。如果当前对象不包含所有必要数据,可以将另一个对象(将获取必要数据的对象)作为方法参数传递。
处理
-
检查传递给参数的值。如果某些参数只是另一个对象的方法调用的结果,使用用方法调用替换参数。该对象可以放在其自身类的字段中,或作为方法参数传递。
-
与其将来自另一个对象的一组数据作为参数传递,不如将对象本身传递给方法,使用保留整个对象。
-
但如果这些参数来自不同的来源,可以通过引入参数对象将它们作为一个单一参数对象传递。
回报
-
更可读、更简洁的代码。
-
重构可能会揭示先前未注意到的重复代码。
何时忽略
- 如果这样做会导致类之间的不必要依赖,请不要去掉参数。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
毫不奇怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更不乏味的学习新知识的方法。
让我们看看…
数据簇
征兆和症状
有时代码的不同部分包含相同的变量组(例如连接到数据库的参数)。这些簇应该转换为它们自己的类。
问题原因
这些数据组通常是由于程序结构不良或“复制粘贴编程”造成的。
如果你想确认某些数据是否是数据簇,只需删除其中一个数据值,看看其他值是否仍然有意义。如果不是,这表明这组变量应该合并为一个对象。
处理
-
如果重复的数据构成类的字段,请使用提取类将字段移动到它们自己的类中。
-
如果相同的数据簇作为方法参数传递,请使用引入参数对象将它们设定为一个类。
-
如果一些数据被传递给其他方法,考虑将整个数据对象传递给方法,而不仅仅是单个字段。保持整个对象将对此有所帮助。
-
查看这些字段使用的代码。将这段代码移动到数据类中可能是个好主意。
收益
-
改善了代码的理解和组织。对特定数据的操作现在集中在一个地方,而不是在代码中随意分散。
-
减少代码大小。
何时忽略
- 在方法的参数中传递整个对象,而不是仅传递其值(基本类型),可能会在两个类之间创建不必要的依赖关系。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读这里所有文本需要 7 小时。
尝试我们的互动重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看…
面向对象的滥用者
所有这些异味都是对面向对象编程原则的不完整或不正确的应用。
开关语句
你有一个复杂的 switch
操作符或一系列 if
语句。
临时字段
临时字段仅在特定情况下获得其值(因此对象需要它们)。在这些情况下之外,它们是空的。
拒绝遗赠
如果一个子类只使用从父类继承的一部分方法和属性,则层次结构就不正确。未使用的方法可能会被忽略或重新定义,从而引发异常。
具有不同接口的替代类
两个类执行相同的功能,但方法名称不同。
switch
语句
征兆与症状
您有一个复杂的switch
操作符或一系列if
语句。
问题的原因
switch
和case
操作符的相对少用是面向对象代码的一个标志。单个switch
的代码往往会分散在程序的不同位置。当添加一个新条件时,你必须找到所有的switch
代码并进行修改。
一般来说,当你看到switch
时,你应该考虑多态性。
处理
-
为了将
switch
隔离并放入正确的类中,您可能需要提取方法,然后移动方法。 -
如果
switch
基于类型代码,例如当程序的运行模式被切换时,请使用用子类替换类型代码或用状态/策略替换类型代码。 -
在指定继承结构后,请使用用多态性替换条件。
-
如果操作符中的条件不太多,且它们都以不同参数调用同一方法,那么多态性将是多余的。在这种情况下,您可以将该方法拆分为多个较小的方法,使用用显式方法替换参数,并相应地更改
switch
。 -
如果条件选项之一为
null
,请使用引入空对象。
收益
- 改善代码组织。
何时忽略
-
当
switch
操作符执行简单操作时,没有必要进行代码更改。 -
switch
操作符通常由工厂设计模式(工厂方法或抽象工厂)使用,以选择一个创建的类。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
我们来看看…
临时字段
征兆和症状
临时字段的值(因此被对象所需)仅在特定情况下存在。在这些情况下之外,它们是空的。
问题的原因
通常,临时字段是为了在需要大量输入的算法中使用而创建的。因此,程序员决定在类中为这些数据创建字段,而不是在方法中创建大量参数。这些字段仅在算法中使用,其余时间未被使用。
这种代码很难理解。您期望在对象字段中看到数据,但由于某种原因,它们几乎总是空的。
处理
-
临时字段及其操作的所有代码可以通过提取类放入一个单独的类中。换句话说,您正在创建一个方法对象,达到与执行用方法对象替换方法相同的效果。
-
引入空对象并将其整合进替代用于检查临时字段值是否存在的条件代码中。
收益
- 更好的代码清晰度和组织性。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里的所有文本需要 7 个小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
拒绝遗产
征兆与症状
如果子类仅使用从父类继承的一部分方法和属性,层次结构就会失衡。未使用的方法可能被忽略或重新定义并引发异常。
问题的原因
有人仅仅因为想重用超类中的代码而激励自己创建类之间的继承关系。但超类和子类完全不同。
处理方法
-
如果继承没有意义,而子类与超类真的毫无关联,就应消除继承,转而使用用委托替代继承。
-
如果继承是合适的,请在子类中去掉不必要的字段和方法。从父类中提取子类所需的所有字段和方法,放入新的超类,并让两个类都从它继承(提取超类)。
回报
- 改善代码的清晰度和组织性。您将不再疑惑为什么
Dog
类继承自Chair
类(尽管它们都有四条腿)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读书累了吗?
难怪,这里所有文本的阅读时间需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
拥有不同接口的替代类
原文:
refactoringguru.cn/smells/alternative-classes-with-different-interfaces
症状与体征
两个类执行相同的功能,但方法名称不同。
问题的原因
创建其中一个类的程序员可能并不知道已经存在一个功能等效的类。
治疗
尝试用一个共同的分母来描述类的接口:
-
重命名方法 s,使它们在所有替代类中保持一致。
-
移动方法、添加参数和参数化方法,使方法的签名和实现保持一致。
-
如果类的功能只有部分重复,尝试使用提取超类。在这种情况下,现有类将成为子类。
-
在你确定使用哪种治疗方法并实施后,你可能能够删除其中一个类。
收益
-
你消除了不必要的重复代码,使得结果代码更加精简。
-
代码变得更加可读和易懂(你不再需要猜测创建第二个类的原因,而它执行与第一个类完全相同的功能)。
何时忽略
- 有时合并类是不可能的,或者困难到毫无意义。例如,当替代类在不同的库中,每个库都有自己的类版本时。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读得累了吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
我们来看看…
更改防止器
这些坏味道意味着如果你需要在代码中的一个地方进行更改,你也必须在其他地方进行许多更改。因此,程序开发变得更加复杂和昂贵。
发散变化
当你对一个类进行更改时,你会发现自己不得不更改许多不相关的方法。例如,添加新产品类型时,你必须更改查找、显示和订购产品的方法。
霰弹手术
进行任何修改都要求你对许多不同的类进行许多小的更改。
并行继承层次
每当你为一个类创建子类时,你会发现自己需要为另一个类创建子类。
分歧变更
原文:
refactoringguru.cn/smells/divergent-change
分歧变更类似于霰弹手术,但实际上是相反的异味。分歧变更是指对单个类进行许多更改。霰弹手术指的是对多个类同时进行单个更改。
征兆与症状
当你修改一个类时,发现需要改变许多无关的方法。例如,添加新产品类型时,你需要更改查找、显示和订购产品的方法。
问题原因
这些分歧修改通常是由于糟糕的程序结构或“复制粘贴编程”造成的。
治疗
-
通过提取类拆分类的行为。
-
如果不同的类有相同的行为,您可能希望通过继承来合并这些类(提取超类和提取子类)。
收益
-
改善代码组织。
-
减少代码重复。
-
简化支持。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读?
难怪,阅读这里所有文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看……
散弹手术
原文:
refactoringguru.cn/smells/shotgun-surgery
散弹手术与 Divergent Change 相似,但实际上是相反的臭味。Divergent Change是指对单个类进行许多更改。散弹手术则是指对多个类同时进行单一更改。
征兆与症状
进行任何修改都需要你对许多不同的类进行许多小更改。
问题原因
单一责任被分割到大量类中。这可能发生在对 Divergent Change 的过度应用之后。
处理方法
-
使用移动方法和移动字段将现有类行为移动到单一类中。如果没有适合的类,请创建一个新的。
-
如果将代码移动到同一类使原来的类几乎为空,请尝试通过内联类消除这些现在多余的类。
收益
-
更好的组织。
-
更少的代码重复。
-
更容易维护。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读得累了吗?
难怪,阅读我们这里所有文本需要 7 小时。
尝试我们的交互式重构课程,它提供了更少乏味的学习新内容的方法。
我们来看…
并行继承层级
原文:
refactoringguru.cn/smells/parallel-inheritance-hierarchies
标志与症状
每当你为一个类创建子类时,你会发现自己需要为另一个类创建子类。
问题的原因
一切都很好,只要层级保持较小。但随着新类的添加,进行更改变得越来越困难。
处理方法
- 你可以通过两个步骤来去重并行类层级。首先,让一个层级的实例引用另一个层级的实例。然后,使用 移动方法 和 移动字段 来移除被引用类中的层级。
收益
-
减少代码重复。
-
可以改善代码的组织。
何时忽略
- 有时,拥有并行类层级只是为了避免程序架构中更大的混乱。如果你发现去重层级的尝试产生了更丑陋的代码,那就退出,撤回你所有的更改,习惯那段代码。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的方式来学习新知识。
我们来看看…
可有可无的元素
可有可无的元素是指那些毫无意义和不必要的东西,其缺失将使代码更简洁、更高效、更易理解。
评论
一个方法充满了解释性注释。
重复代码
两个代码片段看起来几乎一模一样。
懒惰类
理解和维护类总是需要时间和金钱。因此,如果一个类没有足够的价值来吸引你的注意,它应该被删除。
数据类
数据类是指仅包含字段和用于访问这些字段的简单方法(getter 和 setter)的类。这些类只是其他类使用的数据容器,不包含任何额外的功能,无法独立对其拥有的数据进行操作。
死代码
一个变量、参数、字段、方法或类不再被使用(通常是因为它已经过时)。
推测性通用性
存在未使用的类、方法、字段或参数。
注释
迹象与症状
方法中充满了解释性注释。
问题原因
注释通常是出于最好的意图而创建的,当作者意识到自己的代码不够直观或明显时。在这种情况下,注释就像是遮盖鱼腥味代码气味的除臭剂,这些代码是可以改进的。
最好的注释是方法或类的好名称。
如果你觉得没有注释就无法理解某段代码,请尝试改变代码结构,使得注释变得不必要。
处理方法
-
如果注释旨在解释复杂的表达式,则应使用提取变量将表达式拆分为易于理解的子表达式。
-
如果注释解释了一段代码,则可以通过提取方法将该部分代码转换为单独的方法。新方法的名称通常可以直接取自注释文本。
-
如果一个方法已经被提取,但仍然需要注释来解释该方法的作用,请给该方法一个自解释的名称。为此请使用重命名方法。
-
如果需要对系统正常工作所必需的状态进行规则断言,请使用引入断言。
收益
- 代码变得更加直观和明显。
何时忽略
注释有时是有用的:
-
当解释为什么以某种特定方式实现某个功能时。
-
当解释复杂算法时(当尝试了所有简化算法的方法但仍然无法奏效时)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
毫不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看……
重复代码
迹象和症状
两段代码片段看起来几乎相同。
问题的原因
重复代码通常发生在多个程序员同时在同一个程序的不同部分工作时。由于他们在处理不同的任务,他们可能不知道同事已经编写了可以满足自己需求的类似代码。
还有更微妙的重复,当代码的特定部分看起来不同但实际上执行相同的工作。这种重复可能难以发现和修复。
有时重复是故意的。当急于赶工期而现有代码“差不多合适”时,初级程序员可能会忍不住复制粘贴相关代码。在某些情况下,程序员只是懒得整理代码。
处理方法
-
如果在同一类中的两个或多个方法中发现相同代码:请使用提取方法,并在两个地方调用新方法。
-
如果在同一层次的两个子类中发现相同代码:
-
对于两个类都使用提取方法,然后对在方法中使用的字段使用提升字段。
-
如果重复代码位于构造函数内部,请使用提升构造函数主体。
-
如果重复代码相似但并不完全相同,请使用表单模板方法。
-
如果两个方法执行相同的操作但使用不同的算法,请选择最佳算法并应用替换算法。
-
-
如果在两个不同的类中发现重复代码:
-
如果类不属于一个层次结构,请使用提取超类创建一个单一的超类,以维护这些类的所有先前功能。
-
如果创建超类困难或不可能,请在一个类中使用提取类,并在另一个类中使用新组件。
-
-
如果存在大量条件表达式并且执行相同的代码(仅在条件上有所不同),请使用合并条件表达式将这些操作合并为一个条件,并使用提取方法将条件放入一个易于理解的单独方法中。
-
如果在条件表达式的所有分支中执行相同的代码:请使用合并重复条件片段将相同的代码放置在条件树外。
收益
-
合并重复代码简化了代码结构并使其更短。
-
简化 + 短小 = 更易于简化和更便宜的代码支持。
何时忽略
- 在非常少见的情况下,合并两个相同的代码片段可能会使代码变得不够直观和明显。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不难理解,阅读我们这里所有的文本需要 7 小时。
尝试我们的互动重构课程。这是一种不那么乏味的学习新知识的方法。
让我们看看…
懒惰类
症状与体征
理解和维护类总是需要时间和金钱。因此,如果一个类没有足够的价值来吸引你的注意,它就应该被删除。
问题的原因
也许某个类被设计得非常完整,但经过一些重构后变得异常简小。
或者它可能是为了支持从未完成的未来开发工作而设计的。
处理方法
-
几乎无用的组件应该进行内联类处理。
-
对于功能较少的子类,尝试合并层次结构。
收益
-
减少代码大小。
-
更轻松的维护。
何时忽略
- 有时,懒惰类被创建是为了界定未来开发的意图,在这种情况下,尽量在代码的清晰度和简洁性之间保持平衡。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读书读累了吗?
难怪,阅读我们这里的所有文本需要 7 个小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
数据类
迹象与症状
数据类是指仅包含字段和访问这些字段的简单方法(获取器和设置器)的类。这些类只是其他类使用的数据容器。这些类不包含任何额外功能,无法独立操作它们所拥有的数据。
问题原因
当一个新创建的类仅包含少数公共字段(甚至可能只有少量的获取器/设置器)时,这是一件很正常的事情。但对象真正的强大之处在于它们可以包含对其数据的行为类型或操作。
处理方法
-
如果一个类包含公共字段,请使用封装字段将其隐藏,以确保访问只能通过获取器和设置器进行。
-
对于存储在集合(如数组)中的数据,请使用封装集合。
-
审查使用该类的客户端代码。在其中,你可能会发现更适合放在数据类本身的功能。如果是这样,使用移动方法和提取方法将此功能迁移到数据类。
-
在类中充满经过深思熟虑的方法后,你可能希望去掉那些提供过于广泛访问类数据的旧数据访问方法。为此,使用移除设置方法和隐藏方法可能会有所帮助。
收益
-
提高代码的理解和组织。对特定数据的操作现在集中在一个地方,而不是随意散布在代码中。
-
帮助你发现客户端代码的重复。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
死代码
迹象和症状
一个变量、参数、字段、方法或类不再被使用(通常是因为它已过时)。
问题的原因
当软件需求发生变化或进行了修正时,没有人有时间清理旧代码。
这样的代码也可能出现在复杂条件中,当某个分支变得不可达(由于错误或其他情况)。
处理
找到死代码的最快方法是使用一个好的IDE。
-
删除未使用的代码和不需要的文件。
-
对于不必要的类,如果使用了子类或超类,可以应用内联类或折叠层次。
-
要删除不需要的参数,请使用移除参数。
收益
-
减少代码大小。
-
更简单的支持。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦了阅读?
不奇怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
投机通用性
征兆和症状
有一个未使用的类、方法、字段或参数。
问题的原因
有时代码是“以防万一”创建的,以支持预期未来将实现的功能。但实际上,这些功能并未实现,导致代码变得难以理解和维护。
处理
-
要移除未使用的抽象类,请尝试折叠层次结构。
-
不必要的功能委托给另一个类可以通过内联类来消除。
-
未使用的方法?使用内联方法来消除它们。
-
带有未使用参数的方法应通过移除参数进行检查。
-
未使用的字段可以直接删除。
收益
-
更精简的代码。
-
更易于支持。
何时忽略
-
如果您正在开发框架,创建在框架本身中未使用的功能是完全合理的,只要该功能是框架用户所需的。
-
在删除元素之前,请确保它们未在单元测试中使用。这种情况发生在测试需要某种方式从类中获取特定内部信息或执行特殊测试相关操作时。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读得疲惫了吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
耦合器
这个组中的所有气味都导致类之间的过度耦合,或者展示了如果耦合被过度委托所替代会发生什么。
一个方法访问另一个对象的数据多于它自己数据的访问。
一个类使用另一个类的内部字段和方法。
在代码中,你会看到一系列类似于$a->b()->c()->d()
的调用。
如果一个类只执行一个操作,将工作委托给另一个类,那么它存在的意义是什么?
特征嫉妒
征兆和症状
一个方法访问其他对象的数据多于其自身的数据。
问题的原因
这种异味可能在字段移动到数据类后出现。如果是这种情况,您可能还想将对数据的操作移到此类中。
处理方法
基本规则是,如果事物同时变化,应该将它们放在同一个地方。通常使用这些数据的函数与数据一起被修改(尽管可能有例外)。
-
如果一个方法显然应该移动到另一个地方,使用移动方法。
-
如果只有方法的一部分访问另一个对象的数据,使用提取方法将相关部分移走。
-
如果一个方法使用来自多个其他类的函数,首先确定哪个类包含大部分使用的数据。然后将该方法放入此类中,连同其他数据。或者,使用提取方法将该方法拆分成可以放置在不同类中的几个部分。
回报
-
更少的代码重复(如果数据处理代码放在一个中心位置)。
-
更好的代码组织(处理数据的方法与实际数据相邻)。
何时忽略
- 有时行为被故意与持有数据的类分开。通常的好处是能够动态改变行为(参见策略、访问者及其他模式)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
不适当的亲密关系
征兆和症状
一个类使用另一个类的内部字段和方法。
问题的原因
密切关注那些在一起花费过多时间的类。好的类应该尽可能少地了解彼此。这种类更容易维护和重用。
处理
-
最简单的解决方案是使用移动方法和移动字段,将一个类的部分移动到使用这些部分的类中。但只有在第一个类确实不需要这些部分时,这种方法才有效。
-
另一种解决方案是对该类使用提取类和隐藏委托,以使代码关系“正式”。
-
如果类之间是相互依赖的,您应该使用将双向关联更改为单向。
-
如果这种“亲密”关系存在于子类和父类之间,请考虑用继承替代委托。
回报
-
改进代码组织。
-
简化支持和代码重用。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
难怪,阅读我们这里所有的文本需要 7 个小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
消息链
迹象和症状
在代码中,你会看到一系列类似于$a->b()->c()->d()
的调用。
问题原因
消息链发生在一个客户端请求另一个对象时,该对象又请求另一个对象,依此类推。这些链意味着客户端依赖于类结构中的导航。这些关系的任何变化都需要修改客户端。
治疗
-
要删除消息链,请使用隐藏委托。
-
有时候,思考最终对象的用途更为重要。也许使用提取方法将此功能移至链的开头,通过使用移动方法会更有意义。
收益
-
减少链中类之间的依赖关系。
-
减少冗长代码的数量。
何时忽略
- 过于激进的委托隐藏可能导致代码中难以看到实际功能所在。这是另一个说法,避免中间人气味也同样重要。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,这里所有的文本需要花费 7 小时阅读。
尝试我们的交互式重构课程,它提供了一种不那么繁琐的学习新知识的方法。
让我们看看…
中介
迹象和症状
如果一个类仅执行一个操作,将工作委派给另一个类,那么它存在的意义是什么?
问题原因
这种气味可能是过度消除消息链的结果。
在其他情况下,这可能是由于一个类的有用工作逐渐转移到其他类而导致的。这个类就像一个空壳,除了委派外什么也不做。
处理
- 如果大多数方法的类都委派给另一个类,那么就应该移除中介。
收益
- 代码更简洁。
何时忽略
不要删除出于某种原因创建的中介:
-
可能添加了中介以避免类之间的依赖关系。
-
一些设计模式故意创建中介(例如 代理 或 装饰器)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
我们来看一下…
其他气味
以下是一些不属于任何广泛类别的气味。
不完整的库类
迟早,库将无法满足用户需求。解决这个问题的唯一方法——更改库——通常是不可能的,因为库是只读的。
不完整的类库
征兆和症状
图书馆迟早会停止满足用户需求。解决这个问题的唯一方案——更改图书馆——往往是不可能的,因为图书馆是只读的。
问题原因
图书馆的作者没有提供您需要的功能,或者拒绝实施这些功能。
处理方法
-
要向库类引入一些方法,请使用引入外部方法。
-
对于类库中的重大更改,请使用引入本地扩展。
收益
- 减少代码重复(与其从头创建自己的库,不如依赖现有库)。
何时忽略
- 扩展一个库可能会产生额外的工作,如果对库的更改涉及代码更改。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种更轻松的学习新知识的方法。
让我们看看…
重构技术
原文:refactoringguru.cn/refactoring/techniques
组合方法
原文:
refactoringguru.cn/refactoring/techniques/composing-methods
大部分重构工作都专注于正确构建方法。在大多数情况下,过长的方法是万恶之源。这些方法内部代码的多变性掩盖了执行逻辑,使方法极难理解,甚至更难修改。
这一组重构技术简化了方法,消除了代码重复,为未来改进铺平了道路。
提取方法
问题: 你有一个可以归类在一起的代码片段。
解决方案: 将这段代码移动到一个新的单独方法(或函数)中,并用对该方法的调用替换旧代码。
内联方法
问题: 当方法体比方法本身更明显时,使用此技巧。
解决方案: 用方法的内容替换对该方法的调用,并删除该方法本身。
提取变量
问题: 你有一个难以理解的表达式。
解决方案: 将表达式的结果或其部分放在自解释的单独变量中。
内联临时变量
问题: 你有一个临时变量,它只被赋值为一个简单表达式的结果,没其他用途。
解决方案: 用表达式本身替换对变量的引用。
用查询替换临时变量
问题: 你将表达式的结果放在一个局部变量中,以便在代码中后续使用。
解决方案: 将整个表达式移动到一个单独的方法中,并从中返回结果。查询该方法而不是使用变量。如有必要,将新方法合并到其他方法中。
拆分临时变量
问题: 你有一个局部变量,用于存储方法内部的各种中间值(循环变量除外)。
解决方案: 为不同的值使用不同的变量。每个变量应该只负责一个特定的事物。
删除参数赋值
问题: 某个值被分配给方法体内的参数。
解决方案: 使用局部变量代替参数。
用方法对象替换方法
问题: 你有一个长方法,其中局部变量交织在一起,以至于你无法应用提取方法。
解决方案: 将方法转换为一个单独的类,使局部变量成为该类的字段。然后可以将该方法拆分为同一类中的多个方法。
替代算法
问题: 所以你想用一个新算法替换现有算法?
解决方案: 用一个新算法替换实现算法的方法体。
提取方法
问题
你有一个可以组合在一起的代码片段。
解决方案
将这段代码移动到一个单独的新方法(或函数)中,并用对该方法的调用替换旧代码。
之前
void printOwing() {
printBanner();
// Print details.
System.out.println("name: " + name);
System.out.println("amount: " + getOutstanding());
}
之后
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails(double outstanding) {
System.out.println("name: " + name);
System.out.println("amount: " + outstanding);
}
之前
void PrintOwing()
{
this.PrintBanner();
// Print details.
Console.WriteLine("name: " + this.name);
Console.WriteLine("amount: " + this.GetOutstanding());
}
之后
void PrintOwing()
{
this.PrintBanner();
this.PrintDetails();
}
void PrintDetails()
{
Console.WriteLine("name: " + this.name);
Console.WriteLine("amount: " + this.GetOutstanding());
}
之前
function printOwing() {
$this->printBanner();
// Print details.
print("name: " . $this->name);
print("amount " . $this->getOutstanding());
}
之后
function printOwing() {
$this->printBanner();
$this->printDetails($this->getOutstanding());
}
function printDetails($outstanding) {
print("name: " . $this->name);
print("amount " . $outstanding);
}
之前
def printOwing(self):
self.printBanner()
# print details
print("name:", self.name)
print("amount:", self.getOutstanding())
之后
def printOwing(self):
self.printBanner()
self.printDetails(self.getOutstanding())
def printDetails(self, outstanding):
print("name:", self.name)
print("amount:", outstanding)
之前
printOwing(): void {
printBanner();
// Print details.
console.log("name: " + name);
console.log("amount: " + getOutstanding());
}
之后
printOwing(): void {
printBanner();
printDetails(getOutstanding());
}
printDetails(outstanding: number): void {
console.log("name: " + name);
console.log("amount: " + outstanding);
}
为什么重构
方法中的行数越多,越难以弄清楚该方法的功能。这是进行这种重构的主要原因。
除了消除代码中的粗糙边缘,提取方法也是许多其他重构方法中的一步。
好处
-
更可读的代码!务必给新方法一个描述其目的的名称:
createOrder()
、renderCustomerInfo()
等。 -
更少的代码重复。通常,方法中的代码可以在程序的其他地方重复使用。因此,你可以用对新方法的调用替换重复代码。
-
隔离代码的独立部分,这意味着出错的可能性较小(例如,如果错误地修改了变量)。
如何重构
-
创建一个新方法,并以使其目的显而易见的方式命名。
-
将相关的代码片段复制到你的新方法中。从旧位置删除该片段,并在其中放置对新方法的调用。
找到这个代码片段中使用的所有变量。如果它们在片段内部声明并且不在外部使用,则可以保持不变——它们将成为新方法的局部变量。
-
如果变量是在你要提取的代码之前声明的,你需要将这些变量传递给新方法的参数,以便使用它们之前包含的值。有时通过使用用查询替换临时变量来去掉这些变量更为简单。
-
如果你看到在提取的代码中局部变量以某种方式发生了变化,这可能意味着这个变化的值在你的主方法中之后会被需要。请仔细检查!如果确实如此,将这个变量的值返回给主方法以保持一切正常。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读腻了吗?
难怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种更轻松的学习新知识的方法。
我们来看看…
内联方法
问题
当方法体比方法本身更明显时,可以使用此技术。
解决方案
用方法的内容替换对该方法的调用,并删除该方法本身。
之前
class PizzaDelivery {
// ...
int getRating() {
return moreThanFiveLateDeliveries() ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() {
return numberOfLateDeliveries > 5;
}
}
之后
class PizzaDelivery {
// ...
int getRating() {
return numberOfLateDeliveries > 5 ? 2 : 1;
}
}
之前
class PizzaDelivery
{
// ...
int GetRating()
{
return MoreThanFiveLateDeliveries() ? 2 : 1;
}
bool MoreThanFiveLateDeliveries()
{
return numberOfLateDeliveries > 5;
}
}
之后
class PizzaDelivery
{
// ...
int GetRating()
{
return numberOfLateDeliveries > 5 ? 2 : 1;
}
}
之前
function getRating() {
return ($this->moreThanFiveLateDeliveries()) ? 2 : 1;
}
function moreThanFiveLateDeliveries() {
return $this->numberOfLateDeliveries > 5;
}
之后
function getRating() {
return ($this->numberOfLateDeliveries > 5) ? 2 : 1;
}
之前
class PizzaDelivery:
# ...
def getRating(self):
return 2 if self.moreThanFiveLateDeliveries() else 1
def moreThanFiveLateDeliveries(self):
return self.numberOfLateDeliveries > 5
之后
class PizzaDelivery:
# ...
def getRating(self):
return 2 if self.numberOfLateDeliveries > 5 else 1
之前
class PizzaDelivery {
// ...
getRating(): number {
return moreThanFiveLateDeliveries() ? 2 : 1;
}
moreThanFiveLateDeliveries(): boolean {
return numberOfLateDeliveries > 5;
}
}
之后
class PizzaDelivery {
// ...
getRating(): number {
return numberOfLateDeliveries > 5 ? 2 : 1;
}
}
为什么要重构
一种方法简单地委托给另一种方法。就其本身而言,这种委托没有问题。但当有很多这样的方式时,它们会变成一个令人困惑的纠缠,难以理清。
通常方法最初并不太短,但随着程序的变化而变短。因此,不要害羞,尽量删除那些已经过时的方法。
益处
- 通过减少不必要的方法数量,你可以使代码更简洁明了。
如何重构
-
确保该方法在子类中没有被重新定义。如果该方法被重新定义,请避免使用此技术。
-
找到对该方法的所有调用。用方法的内容替换这些调用。
-
删除该方法。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦了阅读?
毫无疑问,阅读我们这里所有的文本需要 7 个小时。
尝试我们的交互式重构课程。它提供了一种更不乏味的学习新知识的方法。
让我们看看…
提取变量
问题
你有一个难以理解的表达式。
解决方案
将表达式或其部分的结果放在自解释的单独变量中。
之前
void renderBanner() {
if ((platform.toUpperCase().indexOf("MAC") > -1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
wasInitialized() && resize > 0 )
{
// do something
}
}
之后
void renderBanner() {
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIE = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIE && wasInitialized() && wasResized) {
// do something
}
}
之前
void RenderBanner()
{
if ((platform.ToUpper().IndexOf("MAC") > -1) &&
(browser.ToUpper().IndexOf("IE") > -1) &&
wasInitialized() && resize > 0 )
{
// do something
}
}
之后
void RenderBanner()
{
readonly bool isMacOs = platform.ToUpper().IndexOf("MAC") > -1;
readonly bool isIE = browser.ToUpper().IndexOf("IE") > -1;
readonly bool wasResized = resize > 0;
if (isMacOs && isIE && wasInitialized() && wasResized)
{
// do something
}
}
之前
if (($platform->toUpperCase()->indexOf("MAC") > -1) &&
($browser->toUpperCase()->indexOf("IE") > -1) &&
$this->wasInitialized() && $this->resize > 0)
{
// do something
}
之后
$isMacOs = $platform->toUpperCase()->indexOf("MAC") > -1;
$isIE = $browser->toUpperCase()->indexOf("IE") > -1;
$wasResized = $this->resize > 0;
if ($isMacOs && $isIE && $this->wasInitialized() && $wasResized) {
// do something
}
之前
def renderBanner(self):
if (self.platform.toUpperCase().indexOf("MAC") > -1) and \
(self.browser.toUpperCase().indexOf("IE") > -1) and \
self.wasInitialized() and (self.resize > 0):
# do something
之后
def renderBanner(self):
isMacOs = self.platform.toUpperCase().indexOf("MAC") > -1
isIE = self.browser.toUpperCase().indexOf("IE") > -1
wasResized = self.resize > 0
if isMacOs and isIE and self.wasInitialized() and wasResized:
# do something
之前
renderBanner(): void {
if ((platform.toUpperCase().indexOf("MAC") > -1) &&
(browser.toUpperCase().indexOf("IE") > -1) &&
wasInitialized() && resize > 0 )
{
// do something
}
}
之后
renderBanner(): void {
const isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
const isIE = browser.toUpperCase().indexOf("IE") > -1;
const wasResized = resize > 0;
if (isMacOs && isIE && wasInitialized() && wasResized) {
// do something
}
}
为什么重构
提取变量的主要原因是使复杂表达式更易于理解,通过将其划分为中间部分。这些可以是:
-
C 语言中
if()
运算符的条件或?:
运算符的一部分 -
一个没有中间结果的长算术表达式
-
长多部分行
如果你看到提取的表达式在代码的其他地方被使用,提取变量可能是执行 提取方法 的第一步。
好处
- 更易读的代码!尝试给提取的变量命名,以清晰地表明变量的目的。提高可读性,减少冗长的注释。选择像
customerTaxValue
、cityUnemploymentRate
、clientSalutationString
等名称。
缺点
-
你的代码中存在更多变量。但这被代码的可读性所抵消。
-
在重构条件表达式时,请记住编译器很可能会优化它,以最小化计算结果值所需的计算量。假设你有以下表达式
if (a() || b()) ...
。如果方法a
返回true
,程序将不会调用方法b
,因为结果值仍然是true
,无论b
返回什么值。然而,如果你将该表达式的部分提取到变量中,两个方法将始终被调用,这可能会影响程序的性能,特别是如果这些方法进行了一些重负载的工作。
如何重构
-
在相关表达式之前插入新行并在此声明新变量。将复杂表达式的一部分赋值给该变量。
-
用新变量替换表达式的那部分。
-
对表达式中的所有复杂部分重复此过程。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦了阅读?
不奇怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看…
内联临时
问题
你有一个临时变量,它的值是简单表达式的结果,仅此而已。
解决方案
用表达式本身替换对变量的引用。
之前
boolean hasDiscount(Order order) {
double basePrice = order.basePrice();
return basePrice > 1000;
}
之后
boolean hasDiscount(Order order) {
return order.basePrice() > 1000;
}
之前
bool HasDiscount(Order order)
{
double basePrice = order.BasePrice();
return basePrice > 1000;
}
之后
bool HasDiscount(Order order)
{
return order.BasePrice() > 1000;
}
之前
$basePrice = $anOrder->basePrice();
return $basePrice > 1000;
之后
return $anOrder->basePrice() > 1000;
之前
def hasDiscount(order):
basePrice = order.basePrice()
return basePrice > 1000
之后
def hasDiscount(order):
return order.basePrice() > 1000
之前
hasDiscount(order: Order): boolean {
let basePrice: number = order.basePrice();
return basePrice > 1000;
}
之后
hasDiscount(order: Order): boolean {
return order.basePrice() > 1000;
}
为什么重构
内联局部变量几乎总是用作用查询替换临时变量的一部分,或者为提取方法铺平道路。
优点
- 这种重构技术本身几乎没有好处。然而,如果变量被赋值为方法的结果,通过去掉不必要的变量,可以稍微提高程序的可读性。
缺点
- 有时,看似无用的临时变量用于缓存重复使用的昂贵操作的结果。因此,在使用这种重构技术之前,请确保简单性不会以牺牲性能为代价。
如何重构
-
找到所有使用该变量的地方。用赋值给它的表达式替代变量。
-
删除变量的声明和赋值行。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读得累了吗?
难怪,阅读我们这里所有文本需要 7 小时。
尝试我们的互动重构课程,它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
用查询替换临时变量
问题
你将表达式的结果放入一个局部变量,以便在代码中后续使用。
解决方案
将整个表达式移动到一个单独的方法中并返回结果。从方法中查询,而不是使用变量。如有必要,将新方法融入其他方法中。
之前
double calculateTotal() {
double basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
}
之后
double calculateTotal() {
if (basePrice() > 1000) {
return basePrice() * 0.95;
}
else {
return basePrice() * 0.98;
}
}
double basePrice() {
return quantity * itemPrice;
}
之前
double CalculateTotal()
{
double basePrice = quantity * itemPrice;
if (basePrice > 1000)
{
return basePrice * 0.95;
}
else
{
return basePrice * 0.98;
}
}
之后
double CalculateTotal()
{
if (BasePrice() > 1000)
{
return BasePrice() * 0.95;
}
else
{
return BasePrice() * 0.98;
}
}
double BasePrice()
{
return quantity * itemPrice;
}
之前
$basePrice = $this->quantity * $this->itemPrice;
if ($basePrice > 1000) {
return $basePrice * 0.95;
} else {
return $basePrice * 0.98;
}
之后
if ($this->basePrice() > 1000) {
return $this->basePrice() * 0.95;
} else {
return $this->basePrice() * 0.98;
}
...
function basePrice() {
return $this->quantity * $this->itemPrice;
}
之前
def calculateTotal():
basePrice = quantity * itemPrice
if basePrice > 1000:
return basePrice * 0.95
else:
return basePrice * 0.98
之后
def calculateTotal():
if basePrice() > 1000:
return basePrice() * 0.95
else:
return basePrice() * 0.98
def basePrice():
return quantity * itemPrice
之前
calculateTotal(): number {
let basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
}
else {
return basePrice * 0.98;
}
}
之后
calculateTotal(): number {
if (basePrice() > 1000) {
return basePrice() * 0.95;
}
else {
return basePrice() * 0.98;
}
}
basePrice(): number {
return quantity * itemPrice;
}
为什么重构
这种重构可以为将提取方法应用于一个非常长的方法的一部分奠定基础。
同样的表达式有时也可能在其他方法中出现,这也是考虑创建公共方法的一个原因。
好处
-
代码可读性。理解方法
getTax()
的目的比理解orderPrice() * 0.2
这行代码要容易得多。 -
通过去重实现更精简的代码,尤其是在被替换的行在多个方法中使用时。
了解一下
性能
这种重构可能会引发这样一个问题:这种方法是否可能导致性能下降。诚实的回答是:是的,因为结果代码可能因查询新方法而受到影响。但在今天快速的 CPU 和优秀的编译器面前,这种负担几乎总是微不足道的。相比之下,可读代码和在程序代码的其他地方重用该方法的能力——得益于这种重构方法——是非常显著的好处。
然而,如果你的临时变量用于缓存一个真正耗时的表达式的结果,你可能想在将表达式提取到新方法后停止重构。
如何重构
-
确保在方法中变量只被赋值一次。如果不是,请使用拆分临时变量以确保该变量仅用于存储表达式的结果。
-
使用提取方法将感兴趣的表达式放入一个新方法中。确保该方法只返回一个值,并且不改变对象的状态。如果该方法影响对象的可见状态,请使用将查询与修改分开。
-
用对新方法的查询替换变量。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不足为奇,阅读我们这里所有文本需要 7 个小时。
尝试我们的交互式重构课程。它提供了一种更不乏味的学习新知识的方法。
让我们来看看…
拆分临时变量
问题
你有一个局部变量,用于在方法内部存储各种中间值(除了循环变量)。
解决方案
对于不同的值,使用不同的变量。每个变量应只负责一件特定的事情。
之前
double temp = 2 * (height + width);
System.out.println(temp);
temp = height * width;
System.out.println(temp);
之后
final double perimeter = 2 * (height + width);
System.out.println(perimeter);
final double area = height * width;
System.out.println(area);
之前
double temp = 2 * (height + width);
Console.WriteLine(temp);
temp = height * width;
Console.WriteLine(temp);
之后
readonly double perimeter = 2 * (height + width);
Console.WriteLine(perimeter);
readonly double area = height * width;
Console.WriteLine(area);
之前
$temp = 2 * ($this->height + $this->width);
echo $temp;
$temp = $this->height * $this->width;
echo $temp;
之后
$perimeter = 2 * ($this->height + $this->width);
echo $perimeter;
$area = $this->height * $this->width;
echo $area;
之前
temp = 2 * (height + width)
print(temp)
temp = height * width
print(temp)
之后
perimeter = 2 * (height + width)
print(perimeter)
area = height * width
print(area)
之前
let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);
之后
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);
为什么要重构
如果你在一个函数内部减少变量的数量,并将它们用于各种无关的目的,当你需要更改包含变量的代码时,你一定会遇到问题。你必须重新检查每个变量使用的案例,以确保使用了正确的值。
好处
-
程序代码的每个组件应仅负责一件事。这使得维护代码变得更加容易,因为你可以轻松替换任何特定的部分,而不必担心意外效果。
-
代码变得更加易读。如果一个变量在很久以前匆忙创建,它可能有一个没有任何说明的名称:
k
、a2
、value
等。但你可以通过为新变量命名一个易于理解、自解释的名称来解决这个问题。这些名称可能类似于customerTaxValue
、cityUnemploymentRate
、clientSalutationString
等。 -
如果你预计将来会使用提取方法,那么这种重构技术是很有用的。
如何重构
-
找到代码中变量被赋值的第一个地方。在这里,你应该用一个与所赋值对应的名称重命名变量。
-
在使用该变量值的地方使用新名称替代旧名称。
-
在变量被赋不同值的地方根据需要重复操作。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读累了吗?
难怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
2022-10-20 数据科学 IPython 笔记本 7.10 组合数据集:合并和连接
2022-10-20 【与神对话】和【零极限】系列完整书单