重构大师-一-
重构大师(一)
干净的代码
重构的主要目的是对抗技术债务。它将混乱转变为干净的代码和简单的设计。
不错!但究竟什么是干净的代码呢?以下是一些特点:
干净的代码对其他程序员来说是显而易见的。
我并不是在说超级复杂的算法。糟糕的变量命名、臃肿的类和方法、神秘数字——这些都会使代码显得混乱且难以理解。
干净的代码不包含重复。
每次你需要在重复代码中进行更改时,你必须记得对每个实例做出相同的更改。这增加了认知负担,并减慢了进度。
干净的代码包含最少数量的类和其他可变部分。
更少的代码意味着头脑中需要记住的内容更少。更少的代码意味着更少的维护。更少的代码意味着更少的错误。代码是负担,保持简短和简单。
干净的代码通过所有测试。
当你的测试通过率只有 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 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
移除对参数的赋值
问题
一些值在方法体内被赋给参数。
解决方案
使用局部变量代替参数。
之前
int discount(int inputVal, int quantity) {
if (quantity > 50) {
inputVal -= 2;
}
// ...
}
之后
int discount(int inputVal, int quantity) {
int result = inputVal;
if (quantity > 50) {
result -= 2;
}
// ...
}
之前
int Discount(int inputVal, int quantity)
{
if (quantity > 50)
{
inputVal -= 2;
}
// ...
}
之后
int Discount(int inputVal, int quantity)
{
int result = inputVal;
if (quantity > 50)
{
result -= 2;
}
// ...
}
之前
function discount($inputVal, $quantity) {
if ($quantity > 50) {
$inputVal -= 2;
}
...
之后
function discount($inputVal, $quantity) {
$result = $inputVal;
if ($quantity > 50) {
$result -= 2;
}
...
之前
def discount(inputVal, quantity):
if quantity > 50:
inputVal -= 2
# ...
之后
def discount(inputVal, quantity):
result = inputVal
if quantity > 50:
result -= 2
# ...
之前
discount(inputVal: number, quantity: number): number {
if (quantity > 50) {
inputVal -= 2;
}
// ...
}
之后
discount(inputVal: number, quantity: number): number {
let result = inputVal;
if (quantity > 50) {
result -= 2;
}
// ...
}
为什么要重构
这个重构的原因与拆分临时变量相同,但在这种情况下我们处理的是参数,而不是局部变量。
首先,如果参数通过引用传递,那么在方法内部更改参数值后,该值会传递给请求调用此方法的参数。这个过程往往是偶然发生的,导致不幸的后果。即使在你的编程语言中通常是通过值(而不是通过引用)传递参数,这种编码怪癖可能会让不习惯的人感到困惑。
其次,将不同值多次赋给单一参数,使你很难知道在任何特定时间点参数中应该包含什么数据。如果参数及其内容有文档记录,但实际值可能与方法内部的预期不同,问题会更加严重。
好处
-
程序的每个元素应只负责一件事。这使得今后的代码维护变得更加容易,因为你可以安全地替换代码而不会产生副作用。
-
这个重构有助于将重复代码提取到单独的方法。
如何重构
-
创建一个局部变量并赋予参数的初始值。
-
在此行之后的所有方法代码中,将参数替换为新的局部变量。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读?
难怪,这里所有文本的阅读需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
我们来看……
用方法对象替换方法
问题
你有一个很长的方法,其中的局部变量交织在一起,以至于无法应用提取方法。
解决方案
将方法转变为一个单独的类,以便局部变量成为类的字段。然后你可以在同一个类中将方法拆分为几个方法。
之前
class Order {
// ...
public double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// Perform long computation.
}
}
之后
class Order {
// ...
public double price() {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order) {
// Copy relevant information from the
// order object.
}
public double compute() {
// Perform long computation.
}
}
之前
public class Order
{
// ...
public double Price()
{
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// Perform long computation.
}
}
之后
public class Order
{
// ...
public double Price()
{
return new PriceCalculator(this).Compute();
}
}
public class PriceCalculator
{
private double primaryBasePrice;
private double secondaryBasePrice;
private double tertiaryBasePrice;
public PriceCalculator(Order order)
{
// Copy relevant information from the
// order object.
}
public double Compute()
{
// Perform long computation.
}
}
之前
class Order {
// ...
public function price() {
$primaryBasePrice = 10;
$secondaryBasePrice = 20;
$tertiaryBasePrice = 30;
// Perform long computation.
}
}
之后
class Order {
// ...
public function price() {
return (new PriceCalculator($this))->compute();
}
}
class PriceCalculator {
private $primaryBasePrice;
private $secondaryBasePrice;
private $tertiaryBasePrice;
public function __construct(Order $order) {
// Copy relevant information from the
// order object.
}
public function compute() {
// Perform long computation.
}
}
之前
class Order:
# ...
def price(self):
primaryBasePrice = 0
secondaryBasePrice = 0
tertiaryBasePrice = 0
# Perform long computation.
之后
class Order:
# ...
def price(self):
return PriceCalculator(self).compute()
class PriceCalculator:
def __init__(self, order):
self._primaryBasePrice = 0
self._secondaryBasePrice = 0
self._tertiaryBasePrice = 0
# Copy relevant information from the
# order object.
def compute(self):
# Perform long computation.
之前
class Order {
// ...
price(): number {
let primaryBasePrice;
let secondaryBasePrice;
let tertiaryBasePrice;
// Perform long computation.
}
}
之后
class Order {
// ...
price(): number {
return new PriceCalculator(this).compute();
}
}
class PriceCalculator {
private _primaryBasePrice: number;
private _secondaryBasePrice: number;
private _tertiaryBasePrice: number;
constructor(order: Order) {
// Copy relevant information from the
// order object.
}
compute(): number {
// Perform long computation.
}
}
为什么重构
一个方法太长,无法分离,因为局部变量交织在一起,很难彼此隔离。
第一步是将整个方法隔离到一个单独的类中,并将其局部变量转换为类的字段。
首先,这可以在类级别上隔离问题。其次,它为将一个庞大且笨重的方法拆分成几个小方法铺平了道路,而这些小方法与原始类的目的并不相符。
好处
- 将一个长方法隔离到自己的类中,可以防止方法膨胀。同时,这也允许在类内部将其拆分为子方法,而不会用工具方法污染原始类。
缺点
- 新增一个类,增加了程序的整体复杂性。
如何重构
-
创建一个新类。根据你要重构的方法的目的命名它。
-
在新类中创建一个私有字段,用于存储对之前方法所在类的实例的引用。如果需要,可以用来从原始类中获取所需数据。
-
为方法中的每个局部变量创建一个单独的私有字段。
-
创建一个构造函数,接受方法中所有局部变量的值作为参数,并初始化相应的私有字段。
-
声明主方法并将原始方法的代码复制到其中,用私有字段替换局部变量。
-
通过创建一个方法对象并调用其主方法,替换原始类中原始方法的主体。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读累了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
我们来看看…
替代算法
问题
所以你想用一个新的算法替换现有的算法吗?
解决方案
用新算法替换实现算法的方法主体。
之前
String foundPerson(String[] people){
for (int i = 0; i < people.length; i++) {
if (people[i].equals("Don")){
return "Don";
}
if (people[i].equals("John")){
return "John";
}
if (people[i].equals("Kent")){
return "Kent";
}
}
return "";
}
之后
String foundPerson(String[] people){
List candidates =
Arrays.asList(new String[] {"Don", "John", "Kent"});
for (int i=0; i < people.length; i++) {
if (candidates.contains(people[i])) {
return people[i];
}
}
return "";
}
之前
string FoundPerson(string[] people)
{
for (int i = 0; i < people.Length; i++)
{
if (people[i].Equals("Don"))
{
return "Don";
}
if (people[i].Equals("John"))
{
return "John";
}
if (people[i].Equals("Kent"))
{
return "Kent";
}
}
return String.Empty;
}
之前
string FoundPerson(string[] people)
{
List<string> candidates = new List<string>() {"Don", "John", "Kent"};
for (int i = 0; i < people.Length; i++)
{
if (candidates.Contains(people[i]))
{
return people[i];
}
}
return String.Empty;
}
之前
function foundPerson(array $people){
for ($i = 0; $i < count($people); $i++) {
if ($people[$i] === "Don") {
return "Don";
}
if ($people[$i] === "John") {
return "John";
}
if ($people[$i] === "Kent") {
return "Kent";
}
}
return "";
}
之后
function foundPerson(array $people){
foreach (["Don", "John", "Kent"] as $needle) {
$id = array_search($needle, $people, true);
if ($id !== false) {
return $people[$id];
}
}
return "";
}
之前
def foundPerson(people):
for i in range(len(people)):
if people[i] == "Don":
return "Don"
if people[i] == "John":
return "John"
if people[i] == "Kent":
return "Kent"
return ""
之后
def foundPerson(people):
candidates = ["Don", "John", "Kent"]
return people if people in candidates else ""
之前
foundPerson(people: string[]): string{
for (let person of people) {
if (person.equals("Don")){
return "Don";
}
if (person.equals("John")){
return "John";
}
if (person.equals("Kent")){
return "Kent";
}
}
return "";
}
之后
foundPerson(people: string[]): string{
let candidates = ["Don", "John", "Kent"];
for (let person of people) {
if (candidates.includes(person)) {
return person;
}
}
return "";
}
为什么重构
-
渐进式重构并不是改进程序的唯一方法。有时一个方法存在太多问题,以至于拆除该方法并重新开始更为简单。而且也许你找到了一种更简单、更高效的算法。如果是这样,你应该简单地用新算法替换旧算法。
-
随着时间的推移,你的算法可能会被纳入一个知名的库或框架中,而你想要摆脱独立实现,以简化维护。
-
你的程序的需求可能会发生重大变化,以至于你现有的算法无法用于该任务。
如何重构
-
确保你已尽可能简化现有算法。使用提取方法将不重要的代码移动到其他方法中。你算法中的移动部分越少,更容易替换。
-
在一个新方法中创建你的新算法。用新算法替换旧算法,然后开始测试程序。
-
如果结果不匹配,请返回旧实现并比较结果。找出差异的原因。虽然原因往往是旧算法中的错误,但更可能是新算法中的某些部分未能正常工作。
-
当所有测试成功完成后,彻底删除旧算法!
</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/moving-features-between-objects
即使你在不同类之间分配功能的方式不尽完美,仍然有希望。
这些重构技术展示了如何安全地在类之间移动功能,创建新类,并隐藏实现细节以避免公开访问。
移动方法
问题: 一个方法在另一个类中的使用频率超过其自身类中的使用频率。
解决方案: 在使用该方法最多的类中创建一个新方法,然后将代码从旧方法移动到那里。将原方法的代码转变为对另一个类中新方法的引用,或者完全删除它。
移动字段
问题: 一个字段在另一个类中的使用频率超过其自身类中的使用频率。
解决方案: 在新类中创建一个字段,并将所有使用旧字段的用户重定向到它。
提取类
问题: 当一个类完成两个类的工作时,会导致尴尬。
解决方案: 相反,创建一个新类,并将负责相关功能的字段和方法放入其中。
内联类
问题: 一个类几乎什么都不做,并且不负责任何事情,也没有计划额外的职责。
解决方案: 将类中的所有特性移到另一个类中。
隐藏委托
问题: 客户从对象А的字段或方法获取对象 B。然后客户调用对象 B 的方法。
解决方案: 在类 A 中创建一个新方法,将调用委托给对象 B。现在客户端对类 B 没有了解或依赖。
去掉中介
问题: 一个类有太多方法,仅仅将请求委托给其他对象。
解决方案: 删除这些方法,并强制客户端直接调用最终方法。
引入外部方法
问题: 一个工具类不包含你需要的方法,而你无法将该方法添加到类中。
解决方案: 将该方法添加到客户端类,并将工具类的对象作为参数传递给它。
引入本地扩展
问题: 一个工具类不包含你需要的一些方法。但你无法将这些方法添加到类中。
解决方案: 创建一个包含这些方法的新类,并使其成为工具类的子类或包装类。
移动方法
问题
一个方法在另一个类中使用得比在它自己类中多。
解决方案
在使用该方法最多的类中创建一个新方法,然后将旧方法中的代码移动到那里。将原方法的代码转换为对另一个类中新方法的引用,或者完全删除它。
之前!移动方法 - 之前之后!移动方法 - 之后
为什么重构
-
你想把一个方法移动到一个包含该方法使用的大部分数据的类中。这使得类的内部更加一致。
-
你想移动一个方法,以减少或消除调用该方法的类对其所在类的依赖。如果调用类已经依赖于你计划将方法移动到的类,这可能会很有用。这减少了类之间的依赖性。
如何重构
-
验证旧方法在其类中使用的所有特性。将它们一起移动可能是个好主意。一般来说,如果某个特性仅被考虑中的方法使用,你肯定应该将其移动。如果该特性也被其他方法使用,你也应该同时移动这些方法。有时移动大量方法比在不同类之间建立关系要容易得多。
确保该方法没有在超类和子类中声明。如果是这样,你要么必须停止移动,要么需要在接收类中实现一种多态,以确保方法在捐赠类之间的不同功能。
-
在接收类中声明新方法。你可能想给这个方法一个更适合它的新名称。
-
决定你将如何引用接收类。你可能已经有一个返回适当对象的字段或方法,但如果没有,你需要写一个新方法或字段来存储接收类的对象。
现在你有了引用接收对象的方法以及它的类中的新方法。掌握这些后,你可以将旧方法转换为对新方法的引用。
-
看看:你能完全删除旧方法吗?如果可以,请在所有使用旧方法的地方放置对新方法的引用。
</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 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
隐藏委托
问题
客户端从对象 A 的字段或方法中获取对象 B。然后客户端调用对象 B 的方法。
解决方案
在类 A 中创建一个新方法,将调用委托给对象 B。现在客户端对类 B 并不了解,也不依赖于类 B。
在之前!在之后!
为什么重构
首先,让我们看看术语:
-
服务器是客户端可以直接访问的对象。
-
委托是包含客户端所需功能的最终对象。
当客户端从另一个对象请求一个对象时,就会出现调用链,然后第二个对象请求另一个对象,以此类推。这些调用序列使客户端参与到类结构的导航中。这些相互关系的任何变化都需要在客户端进行相应的更改。
优势
- 将委托隐藏于客户端。客户端代码越少需要了解对象之间关系的细节,对程序的修改就越容易。
缺点
- 如果你需要创建过多的委托方法,服务器类有可能成为一个不必要的中介,导致过多的中介者。
如何重构
-
为每个被客户端调用的委托类方法,在服务器类中创建一个方法,将调用委托给委托类。
-
更改客户端代码,使其调用服务器类的方法。
-
如果你的更改使客户端不再需要委托类,你可以从服务器类中移除对委托类的访问方法(最初用于获取委托类的方法)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
去掉中介
问题
一个类有太多方法只是简单地委托给其他对象。
解决方案
删除这些方法,并强迫客户端直接调用最终方法。
之前之后
为什么要重构
为了描述这个技术,我们将使用隐藏委托中的术语:
-
服务器是客户端可以直接访问的对象。
-
委托是包含客户端所需功能的最终对象。
有两种类型的问题:
-
服务器类本身不执行任何操作,仅仅增加了不必要的复杂性。在这种情况下,考虑一下这个类是否真的需要。
-
每当向委托添加新功能时,您需要在服务器类中为其创建一个委托方法。如果进行了大量更改,这将相当繁琐。
如何重构
-
创建一个获取委托类对象的 getter,以便从服务器类对象访问。
-
在服务器类中用对委托类方法的直接调用替换对委托方法的调用。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里的所有文本需要 7 个小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
引入外部方法
问题
实用类不包含你需要的方法,你不能将方法添加到该类中。
解决方案
将方法添加到客户端类中,并将实用类的对象作为参数传递给它。
之前
class Report {
// ...
void sendReport() {
Date nextDay = new Date(previousEnd.getYear(),
previousEnd.getMonth(), previousEnd.getDate() + 1);
// ...
}
}
之后
class Report {
// ...
void sendReport() {
Date newStart = nextDay(previousEnd);
// ...
}
private static Date nextDay(Date arg) {
return new Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1);
}
}
之前
class Report
{
// ...
void SendReport()
{
DateTime nextDay = previousEnd.AddDays(1);
// ...
}
}
之后
class Report
{
// ...
void SendReport()
{
DateTime nextDay = NextDay(previousEnd);
// ...
}
private static DateTime NextDay(DateTime date)
{
return date.AddDays(1);
}
}
之前
class Report {
// ...
public function sendReport() {
$previousDate = clone $this->previousDate;
$paymentDate = $previousDate->modify("+7 days");
// ...
}
}
之后
class Report {
// ...
public function sendReport() {
$paymentDate = self::nextWeek($this->previousDate);
// ...
}
/**
* Foreign method. Should be in Date.
*/
private static function nextWeek(DateTime $arg) {
$previousDate = clone $arg;
return $previousDate->modify("+7 days");
}
}
之前
class Report:
# ...
def sendReport(self):
nextDay = Date(self.previousEnd.getYear(),
self.previousEnd.getMonth(), self.previousEnd.getDate() + 1)
# ...
之后
class Report:
# ...
def sendReport(self):
newStart = self._nextDay(self.previousEnd)
# ...
def _nextDay(self, arg):
return Date(arg.getYear(), arg.getMonth(), arg.getDate() + 1)
之前
class Report {
// ...
sendReport(): void {
let nextDay: Date = new Date(previousEnd.getYear(),
previousEnd.getMonth(), previousEnd.getDate() + 1);
// ...
}
}
之后
class Report {
// ...
sendReport() {
let newStart: Date = nextDay(previousEnd);
// ...
}
private static nextDay(arg: Date): Date {
return new Date(arg.getFullYear(), arg.getMonth(), arg.getDate() + 1);
}
}
为什么要重构
你有代码使用某个类的数据和方法。你意识到代码在该类的新方法中看起来和工作得会更好。但你无法将方法添加到类中,因为,例如,该类位于第三方库中。
当你想将代码移到方法中时,如果代码在程序的不同地方重复多次,这种重构会带来很大的回报。
由于你将实用类的对象传递给新方法的参数,你可以访问其所有字段。在方法内部,你可以做几乎所有你想做的事情,就像该方法是实用类的一部分一样。
好处
- 消除代码重复。如果你的代码在多个地方重复,可以用方法调用替换这些代码片段。即使考虑到外部方法位于次优位置,这种做法也优于重复。
缺点
- 在客户端类中有实用类的方法并不总是对维护代码的人来说是清晰的。如果该方法可以在其他类中使用,你可以通过为实用类创建一个包装器并将方法放在那里来获益。当有多个这样的实用方法时,这也会很有帮助。引入本地扩展可以帮助解决这个问题。
如何重构
-
在客户端类中创建一个新方法。
-
在此方法中创建一个参数,以传递实用类的对象。如果可以从客户端类中获得该对象,则不必创建这样的参数。
-
将相关代码片段提取到此方法中,并用方法调用替换它们。
-
一定要在方法的注释中保留外部方法标签,并建议如果将来可能的话,将该方法放入实用类中。这将使未来维护软件的人更容易理解该方法为何位于此特定类中。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
引入本地扩展
问题
一个工具类不包含你需要的一些方法。但你无法将这些方法添加到类中。
解决方案
创建一个包含方法的新类,并使其成为工具类的子类或包装类。
前后
为什么重构
你正在使用的类没有你需要的方法。更糟糕的是,你无法添加这些方法(例如,因为这些类在第三方库中)。有两种解决方案:
-
从相关类创建一个子类,包含方法并从父类继承其他一切。这样更简单,但有时会受到工具类本身的阻碍(由于
final
)。 -
创建一个包装类,包含所有新方法,并在其他地方委托给工具类的相关对象。此方法工作量较大,因为你不仅需要代码来维护包装器与工具对象之间的关系,还需要大量简单的委托方法,以模拟工具类的公共接口。
好处
- 通过将附加方法移到单独的扩展类(包装类或子类)中,可以避免使客户端类充满不合适的代码。程序组件更加连贯,也更易于重用。
如何重构
-
创建一个新的扩展类:
-
选项 A:使其成为工具类的子类。
-
选项 B:如果你决定创建一个包装器,请在其中创建一个字段以存储将进行委托的工具类对象。使用此选项时,你还需要创建重复工具类公共方法的简单委托方法。
-
-
创建一个构造函数,使用工具类构造函数的参数。
-
还可以创建一个替代的“转换”构造函数,仅将原始类的对象作为参数。这将帮助将扩展替代原始类的对象。
-
在类中创建新的扩展方法。将其他类的外部方法移动到此类中,或者如果其功能已在扩展中存在,则删除外部方法。
-
在需要其功能的地方,用新的扩展类替换对工具类的使用。
</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/organizing-data
这些重构技术有助于数据处理,用丰富的类功能替换原始类型。
另一个重要结果是理顺类之间的关联,使类更加可移植和可重用。
自我封装字段
问题: 你直接访问类内部的私有字段。
解决方案: 为字段创建一个 getter 和 setter,仅通过它们访问字段。
用对象替换数据值
问题: 一个类(或一组类)包含一个数据字段。该字段具有自己的行为和相关数据。
解决方案: 创建一个新类,将旧字段及其行为放入该类中,并在原始类中存储该类的对象。
将值更改为引用
问题: 所以你有许多相同实例的单一类,需要用一个对象替换它们。
解决方案: 将相同的对象转换为单个引用对象。
将引用更改为值
问题: 你有一个引用对象,它太小且不常改变,以至于不值得管理其生命周期。
解决方案: 将其转换为值对象。
用对象替换数组
问题: 你有一个包含各种类型数据的数组。
解决方案: 用一个对象替换数组,该对象将为每个元素具有单独的字段。
重复观察数据
问题: 域数据是否存储在负责 GUI 的类中?
解决方案: 将数据分离到不同的类中,确保领域类与 GUI 之间的连接和同步是个好主意。
将单向关联更改为双向
问题: 你有两个类,它们各自需要使用对方的功能,但它们之间的关联仅是单向的。
解决方案: 将缺失的关联添加到需要它的类中。
将双向关联更改为单向
问题: 你有两个类之间的双向关联,但其中一个类并不使用另一个的功能。
解决方案: 删除未使用的关联。
用符号常量替换魔法数字
问题: 你的代码使用一个有特定含义的数字。
解决方案: 用一个具有可读名称的常量替换这个数字,以解释数字的含义。
封装字段
问题: 你有一个公共字段。
解决方案: 将字段设为私有并为其创建访问方法。
封装集合
问题: 一个类包含一个集合字段,并有简单的 getter 和 setter 用于操作该集合。
解决方案: 使 getter 返回的值为只读,并创建用于添加/删除集合元素的方法。
用类替换类型代码
问题: 一个类有一个包含类型代码的字段。此类型的值未在操作条件中使用,并且不会影响程序的行为。
解决方案: 创建一个新类,使用其对象代替类型代码值。
用子类替换类型代码
问题: 你有一个编码类型直接影响程序行为(此字段的值在条件中触发各种代码)。
解决方案: 为编码类型的每个值创建子类。然后将相关行为从原始类提取到这些子类中。用多态替换控制流代码。
用状态/策略替换类型代码
问题: 你有一个编码类型影响行为,但你无法使用子类来摆脱它。
解决方案: 用状态对象替换类型代码。如果需要用类型代码替换字段值,则“插入”另一个状态对象。
用字段替换子类
问题: 你有子类仅在其(常量返回)方法上有所不同。
解决方案: 用父类中的字段替换方法,并删除子类。
自我封装字段
原文:
refactoringguru.cn/self-encapsulate-field
自我封装与普通的封装字段不同:这里给出的重构技术是在私有字段上执行的。
问题
你在类内部直接访问私有字段。
解决方案
创建一个字段的 getter 和 setter,并仅使用它们来访问该字段。
之前
class Range {
private int low, high;
boolean includes(int arg) {
return arg >= low && arg <= high;
}
}
之后
class Range {
private int low, high;
boolean includes(int arg) {
return arg >= getLow() && arg <= getHigh();
}
int getLow() {
return low;
}
int getHigh() {
return high;
}
}
之前
class Range
{
private int low, high;
bool Includes(int arg)
{
return arg >= low && arg <= high;
}
}
之后
class Range
{
private int low, high;
int Low {
get { return low; }
}
int High {
get { return high; }
}
bool Includes(int arg)
{
return arg >= Low && arg <= High;
}
}
之前
private $low;
private $high;
function includes($arg) {
return $arg >= $this->low && $arg <= $this->high;
}
之后
private $low;
private $high;
function includes($arg) {
return $arg >= $this->getLow() && $arg <= $this->getHigh();
}
function getLow() {
return $this->low;
}
function getHigh() {
return $this->high;
}
之前
class Range {
private low: number
private high: number;
includes(arg: number): boolean {
return arg >= low && arg <= high;
}
}
之后
class Range {
private low: number
private high: number;
includes(arg: number): boolean {
return arg >= getLow() && arg <= getHigh();
}
getLow(): number {
return low;
}
getHigh(): number {
return high;
}
}
为什么重构
有时直接在类内部访问私有字段根本不够灵活。你希望能够在首次查询时初始化字段值,或者在字段的新值分配时对其执行某些操作,或者在子类中以各种方式做到这一点。
优势
-
间接访问字段是通过访问方法(getter 和 setter)对字段进行操作。这种方法比直接访问字段灵活得多。
-
首先,当字段中的数据被设置或接收时,你可以执行复杂的操作。惰性初始化和字段值验证可以很容易地在字段的 getter 和 setter 中实现。
-
第二,更重要的是,你可以在子类中重新定义 getter 和 setter。
-
-
你可以选择不为字段实现 setter。字段值将仅在构造函数中指定,从而使字段在整个对象生命周期内不可更改。
缺点
- 当使用直接访问字段时,代码看起来更简单且更具表现力,尽管灵活性降低。
如何重构
-
为该字段创建一个 getter(和可选的 setter)。它们应为
protected
或public
。 -
查找所有对字段的直接调用,并将它们替换为 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 小时。
尝试我们的互动重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
用对象替换数据值
问题
一个类(或一组类)包含一个数据字段。该字段有自己的行为和相关数据。
解决方案
创建一个新类,将旧字段及其行为放入该类中,并在原始类中存储该类的对象。
之前之后
为什么重构
这种重构基本上是提取类的一个特例。不同之处在于重构的原因。
在提取类中,我们有一个负责不同事务的单一类,我们希望将其责任拆分开来。
用对象替换数据值时,我们有一个原始字段(数字、字符串等),由于程序的发展,这些字段不再简单,现在有了相关的数据和行为。一方面,这些字段本身并不可怕。然而,这些字段和行为的组合可能在多个类中同时存在,造成重复代码。
因此,为此我们创建一个新类,并将字段及相关的数据和行为转移到该类中。
好处
- 改善类内部的关联性。数据和相关行为都在一个类内。
如何重构
在开始重构之前,查看是否有直接引用该字段的地方。如果有,请使用自我封装字段将其隐藏在原始类中。
-
创建一个新类,并将字段及相关的 getter 复制到该类中。此外,创建一个接受字段简单值的构造函数。该类不会有 setter,因为每次发送给原始类的新字段值都会创建一个新的值对象。
-
在原始类中,将字段类型更改为新类。
-
在原始类的 getter 中,调用关联对象的 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 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
用对象替换数组
原文:
refactoringguru.cn/replace-array-with-object
这种重构技术是用对象替换数据值的特殊情况。
问题
你有一个包含各种数据类型的数组。
解决方案
用一个将为每个元素拥有单独字段的对象替换数组。
之前
String[] row = new String[2];
row[0] = "Liverpool";
row[1] = "15";
之后
Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");
之前
string[] row = new string[2];
row[0] = "Liverpool";
row[1] = "15";
之后
Performance row = new Performance();
row.SetName("Liverpool");
row.SetWins("15");
之前
$row = [];
$row[0] = "Liverpool";
$row[1] = 15;
之后
$row = new Performance;
$row->setName("Liverpool");
$row->setWins(15);
之前
row = [None * 2]
row[0] = "Liverpool"
row[1] = "15"
之后
row = Performance()
row.setName("Liverpool")
row.setWins("15")
之前
let row = new Array(2);
row[0] = "Liverpool";
row[1] = "15";
之后
let row = new Performance();
row.setName("Liverpool");
row.setWins("15");
为什么重构
数组是存储数据和单一类型集合的绝佳工具。但如果你像使用邮政箱一样使用数组,把用户名存储在箱子 1 中,把用户地址存储在箱子 14 中,总有一天你会对此感到非常不满。这种方法会导致灾难性的失败,当有人把东西放入错误的“箱子”时,还需要花时间弄清楚哪个数据存储在哪里。
好处
-
在结果类中,你可以放置之前存储在主类或其他地方的所有相关行为。
-
一个类的字段比数组的元素更容易文档化。
如何重构
-
创建一个新类来包含数组中的数据。将数组本身作为公共字段放入类中。
-
在原始类中创建一个字段来存储该类的对象。不要忘记在你初始化数据数组的地方也创建该对象。
-
在新类中,为每个数组元素逐一创建访问方法。给它们起自解释的名称,表明它们的功能。同时,将主代码中对数组元素的每次使用替换为相应的访问方法。
-
当所有元素的访问方法都创建完成后,使数组变为私有。
-
对于数组的每个元素,在类中创建一个私有字段,然后更改访问方法以便使用这个字段而不是数组。
-
当所有数据被移动后,删除数组。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦了阅读?
难怪阅读这里所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更轻松的学习新知识的方法。
让我们看看…
重复观察到的数据
问题
域数据是否存储在负责 GUI 的类中?
解决方案
然后,将数据分离到不同的类中是个好主意,以确保域类和 GUI 之间的连接和同步。
前!重复观察到的数据 - 前后!重复观察到的数据 - 后
为什么重构
你想要为相同的数据提供多个接口视图(例如,你同时有桌面应用和移动应用)。如果未能将 GUI 与域分离,你将很难避免代码重复和大量错误。
好处
-
你将责任分配给业务逻辑类和表示类(参见单一职责原则),这使你的程序更具可读性和可理解性。
-
如果需要添加新的接口视图,创建新的表示类;你不需要触碰业务逻辑的代码(参见开闭原则)。
-
现在不同的人可以同时处理业务逻辑和用户界面。
何时不使用
-
这种重构技术在其经典形式中使用观察者模板进行,但不适用于网页应用程序,因为所有类在对网页服务器的查询之间都会被重建。
-
尽管如此,将业务逻辑提取到单独类中的一般原则对于网页应用也是合理的。但这将根据你的系统设计采用不同的重构技术来实现。
如何重构
-
在GUI 类中隐藏对域数据的直接访问。为此,最好使用自我封装字段。因此,你需要为这些数据创建获取器和设置器。
-
在GUI 类事件的处理程序中,使用设置器来设置新字段值。这将使你能够将这些值传递给相关的域对象。
-
创建一个域类,并将GUI 类中的必要字段复制到其中。为所有这些字段创建获取器和设置器。
-
为这两个类创建一个观察者模式:
-
在域类中,创建一个用于存储观察者对象(GUI 对象)的数组,以及注册、删除和通知它们的方法。
-
在GUI 类中,创建一个用于存储对域类的引用的字段,以及
update()
方法,该方法将响应对象的变化并更新GUI 类中的字段值。请注意,值的更新应直接在方法中进行,以避免递归。 -
在GUI 类构造函数中,创建一个域类的实例并将其保存在你创建的字段中。将GUI 对象注册为域对象的观察者。
-
在领域类字段的 setter 中,调用通知观察者的方法(换句话说,在GUI 类中的更新方法),以便将新值传递给 GUI。
-
修改GUI 类字段的 setter,以便它们直接在领域对象中设置新值。注意确保值不是通过领域类的 setter 设置的——否则将导致无限递归。
-
</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/change-unidirectional-association-to-bidirectional
问题
你有两个类需要使用对方的特性,但它们之间的关联仅是单向的。
解决方案
将缺失的关联添加到需要它的类中。
之前!将单向关联更改为双向 - 之前之后!将单向关联更改为双向 - 之后
为什么重构
最初这些类有单向关联。但随着时间的推移,客户端代码需要访问关联的两侧。
好处
- 如果一个类需要反向关联,你可以简单地计算它。但如果这些计算很复杂,最好保留反向关联。
缺点
-
双向关联比单向关联更难实现和维护。
-
双向关联使类相互依赖。使用单向关联时,其中一个可以独立于另一个使用。
如何重构
-
添加一个用于保存反向关联的字段。
-
决定哪个类将是“主导”。这个类将包含创建或更新关联的方法,随着元素的添加或更改,建立类中的关联,并调用建立关联的工具方法。
-
为“非主导”类创建一个建立关联的工具方法。该方法应使用参数中给定的内容来完成字段。给该方法一个明显的名称,以便之后不会用于其他目的。
-
如果控制单向关联的旧方法在“主导”类中,请用来自关联对象的工具方法补充这些方法。
-
如果控制关联的旧方法在“非主导”类中,请在“主导”类中创建这些方法,调用它们并将执行委托给它们。
</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/change-bidirectional-association-to-unidirectional
问题
你在类之间有双向关联,但其中一个类不使用另一个的特性。
解决方案
删除未使用的关联。
之前之后
为什么重构
双向关联通常比单向关联更难维护,需要额外的代码来正确创建和删除相关对象。这使得程序变得更加复杂。
此外,实施不当的双向关联可能会导致垃圾收集问题(反过来会导致未使用对象的内存膨胀)。
示例:垃圾收集器从内存中移除不再被其他对象引用的对象。假设创建了一对对象User
-Order
,使用后被遗弃。但这些对象不会从内存中清除,因为它们仍然互相引用。也就是说,随着编程语言的进步,这个问题变得不那么重要,现在语言会自动识别未使用的对象引用并将其从内存中移除。
还有类之间的相互依赖问题。在双向关联中,两个类必须互相了解,这意味着它们不能单独使用。如果存在许多这样的关联,程序的不同部分变得过于相互依赖,任何一个组件的变化可能会影响其他组件。
好处
-
简化不需要该关系的类。更少的代码意味着更少的代码维护。
-
减少类之间的依赖。独立的类更容易维护,因为对一个类的任何更改只影响该类。
如何重构
-
确保以下条件之一对你的类成立:
-
不使用任何关联。
-
还有另一种获取关联对象的方法,例如通过数据库查询。
-
相关对象可以作为参数传递给使用它的方法。
-
-
根据你的情况,包含与另一个对象关联的字段应该被替换为参数或方法调用,以不同的方式获取该对象。
-
删除将关联对象分配给字段的代码。
-
删除现在未使用的字段。
</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/replace-magic-number-with-symbolic-constant
问题
你的代码使用了一个具有特定含义的数字。
解决方案
用一个具有易读名称的常量替换这个数字,以解释该数字的含义。
之前
double potentialEnergy(double mass, double height) {
return mass * height * 9.81;
}
之后
static final double GRAVITATIONAL_CONSTANT = 9.81;
double potentialEnergy(double mass, double height) {
return mass * height * GRAVITATIONAL_CONSTANT;
}
之前
double PotentialEnergy(double mass, double height)
{
return mass * height * 9.81;
}
之后
const double GRAVITATIONAL_CONSTANT = 9.81;
double PotentialEnergy(double mass, double height)
{
return mass * height * GRAVITATIONAL_CONSTANT;
}
之前
function potentialEnergy($mass, $height) {
return $mass * $height * 9.81;
}
之后
define("GRAVITATIONAL_CONSTANT", 9.81);
function potentialEnergy($mass, $height) {
return $mass * $height * GRAVITATIONAL_CONSTANT;
}
之前
def potentialEnergy(mass, height):
return mass * height * 9.81
之后
GRAVITATIONAL_CONSTANT = 9.81
def potentialEnergy(mass, height):
return mass * height * GRAVITATIONAL_CONSTANT
之前
potentialEnergy(mass: number, height: number): number {
return mass * height * 9.81;
}
之后
static const GRAVITATIONAL_CONSTANT = 9.81;
potentialEnergy(mass: number, height: number): number {
return mass * height * GRAVITATIONAL_CONSTANT;
}
为什么要重构
魔法数字是源代码中遇到的数值,但没有明显的含义。这个“反模式”使得理解程序和重构代码变得更加困难。
当你需要更改这个魔法数字时,会出现更多困难。查找和替换无法解决这个问题:相同的数字可能在不同地方用于不同目的,这意味着你需要验证每一行使用这个数字的代码。
好处
-
符号常量可以作为其值含义的实时文档。
-
更改常量的值比在整个代码库中搜索这个数字要容易得多,且不会意外改变用于其他目的的相同数字。
-
减少代码中对数字或字符串的重复使用。这在值复杂且较长时尤其重要(例如
3.14159
或0xCAFEBABE
)。
重要信息
不是所有的数字都是神奇的。
如果数字的目的很明显,就不需要替换。一个经典例子是:
for (i = 0; i < сount; i++) { ... }
替代方案
-
有时可以用方法调用替换魔法数字。例如,如果你有一个表示集合中元素数量的魔法数字,你不需要在检查集合的最后一个元素时使用它。相反,使用标准方法获取集合长度。
-
魔法数字有时用作类型代码。假设你有两种用户类型,并在一个类中使用数字字段来指定哪个是哪个:管理员为
1
,普通用户为2
。在这种情况下,你应该使用一种重构方法来避免类型代码:
-
用类替换类型代码
-
用子类替换类型代码
-
用状态/策略替换类型代码
-
如何重构
-
声明一个常量并将魔法数字的值赋给它。
-
找到所有魔法数字的提及。
-
对于你找到的每个数字,请仔细检查在这种特定情况下的魔法数字是否与常量的目的相对应。如果是,请用你的常量替换这个数字。这是一个重要步骤,因为相同的数字可能意味着完全不同的事情(并可能用不同的常量替换)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程,它提供了更轻松的学习新知识的方法。
让我们看看……
封装字段
问题
你有一个公共字段。
解决方案
将字段设置为私有,并为其创建访问方法。
之前
class Person {
public String name;
}
之后
class Person {
private String name;
public String getName() {
return name;
}
public void setName(String arg) {
name = arg;
}
}
之前
class Person
{
public string name;
}
之后
class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
之前
public $name;
之后
private $name;
public getName() {
return $this->name;
}
public setName($arg) {
$this->name = $arg;
}
之前
class Person {
name: string;
}
之后
class Person {
private _name: string;
get name() {
return this._name;
}
setName(arg: string): void {
this._name = arg;
}
}
为什么重构
面向对象编程的支柱之一是 封装,即隐蔽对象数据的能力。否则,所有对象都是公共的,其他对象可以在没有任何检查和制衡的情况下获取和修改你的对象的数据!数据与与此数据相关的行为分离,程序部分的模块化受到损害,维护变得复杂。
益处
-
如果组件的数据和行为密切相关并且在代码中的同一位置,那么你维护和开发该组件会更容易。
-
你还可以执行与访问对象字段相关的复杂操作。
何时不使用
-
在某些情况下,由于性能考虑,封装是不明智的。这些情况很少见,但一旦发生,这种情况非常重要。
比如说,你有一个图形编辑器,其中包含具有 x 和 y 坐标的对象。这些字段未来不太可能改变。此外,程序涉及大量不同的对象,这些字段都存在。因此,直接访问坐标字段可以节省大量本来会被调用访问方法占用的 CPU 周期。
作为这种特殊情况的一个例子,Java 中的 Point 类的所有字段都是公共的。
如何重构
-
为字段创建 getter 和 setter。
-
找到字段的所有调用。用 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 个小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
封装集合
问题
一个类包含一个集合字段以及用于操作集合的简单 getter 和 setter。
解决方案
使 getter 返回的值为只读,并创建用于添加/删除集合元素的方法。
BeforeAfter
为什么重构
一个类包含一个字段,该字段包含一个对象集合。这个集合可以是数组、列表、集合或向量。为操作集合创建了正常的 getter 和 setter。
但是,集合应该通过一种与其他数据类型使用的协议略有不同的方式来使用。getter 方法不应该返回集合对象本身,因为这会让客户端在不知情的情况下更改集合内容。此外,这会向客户端显示对象数据的内部结构过多。获取集合元素的方法应该返回一个不允许更改集合或泄露过多结构数据的值。
此外,不应该有将值分配给集合的方法。相反,应该有用于添加和删除元素的操作。通过这种方式,拥有对象可以控制集合元素的添加和删除。
这样的协议恰当地封装了集合,从而最终减少了拥有类与客户端代码之间的关联程度。
好处
-
集合字段被封装在一个类中。当调用 getter 时,它返回集合的副本,这防止了在包含集合的类不知情的情况下意外更改或覆盖集合元素。
-
如果集合元素包含在基本类型内,例如数组,则可以创建更方便的方法来操作集合。
-
如果集合元素包含在非基本容器(标准集合类)中,通过封装集合可以限制对集合不必要的标准方法的访问(例如限制添加新元素)。
如何重构
-
创建用于添加和删除集合元素的方法。这些方法必须接受集合元素作为参数。
-
如果在类构造函数中未完成,则将空集合分配给该字段作为初始值。
-
查找集合字段 setter 的调用。更改 setter,使其使用添加和删除元素的操作,或使这些操作调用客户端代码。
请注意,setter 只能用于用其他元素替换所有集合元素。因此,建议将 setter 名称(重命名方法)更改为replace
。
-
查找所有在调用集合获取器后集合被更改的地方。将代码更改为使用您新的添加和删除元素的方法。
-
更改获取器,使其返回集合的只读表示。
-
检查使用集合的客户端代码,找出在集合类内部看起来更好的代码。
</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/replace-type-code-with-class
什么是类型代码? 类型代码发生在没有单独数据类型时,你有一组数字或字符串,这些值形成某个实体的允许值列表。这些特定的数字和字符串通常通过常量给出可理解的名称,这就是为什么这种类型代码如此常见的原因。
问题
一个类有一个字段包含类型代码。这个类型的值不在操作符条件中使用,也不影响程序的行为。
解决方案
创建一个新类,并使用它的对象来代替类型代码的值。
之前之后
为什么重构
类型代码最常见的原因之一是在与数据库工作时,当数据库中有字段编码了某个复杂概念的数字或字符串。
例如,你有一个User
类,其字段user_role
包含每个用户的访问权限信息,可能是管理员、编辑者或普通用户。因此,在这种情况下,这些信息在字段中分别编码为A
、E
和U
。
这种方法的缺点是什么?字段的设置器通常不检查发送的值,这可能会在某人向这些字段发送意外或错误的值时造成大问题。
此外,这些字段无法进行类型验证。你可以向它们发送任何数字或字符串,这不会被你的 IDE 进行类型检查,甚至允许你的程序运行(然后崩溃)。
好处
-
我们希望将一组原始值(即编码类型的内容)转变为完整的类,从而获得面向对象编程所提供的所有好处。
-
通过用类替换类型代码,我们允许在编程语言层面上对传递给方法和字段的值进行类型提示。
例如,当传递值到方法时,编译器以前无法区分你的数字常量和某个任意数字,但现在当传递不符合指定类型类的数据时,你会在 IDE 中收到错误警告。
-
因此,我们可以将代码移动到类型的类中。如果你需要在整个程序中对类型值进行复杂操作,现在这些代码可以“存在”于一个或多个类型类中。
什么时候不使用
如果编码类型的值在控制流结构(if
、switch
等)中使用,并控制类的行为,你应该使用两种类型代码重构技术之一:
-
用子类替换类型代码
-
用状态/策略替换类型代码
如何重构
-
创建一个新类,并给它一个与编码类型目的相对应的新名称。我们称之为类型类。
-
将包含类型代码的字段复制到类型类中,并将其设为私有。然后为该字段创建一个获取器。该字段的值将仅从构造函数中设置。
-
对于每个编码类型的值,在类型类中创建一个静态方法。它将创建一个对应于此编码类型值的新类型类对象。
-
在原始类中,将编码字段的类型替换为类型类。在构造函数和字段设置器中创建此类型的新对象。更改字段获取器,使其调用类型类获取器。
-
将编码类型的值的任何提及替换为相关类型类静态方法的调用。
-
从原始类中删除编码类型常量。
</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/replace-type-code-with-subclasses
什么是类型代码? 类型代码是指,当你有一组数字或字符串,而不是单独的数据类型时,这些数字或字符串形成某个实体的可允许值列表。通常,这些具体的数字和字符串通过常量被赋予可理解的名称,这也是为何这种类型代码如此常见的原因。
问题
你有一个编码类型,它直接影响程序行为(该字段的值触发条件语句中的各种代码)。
解决方案
为编码类型的每个值创建子类。然后将原类中的相关行为提取到这些子类中。用多态性替换控制流代码。
之前!用子类替换类型代码 - 之前之后!用子类替换类型代码 - 之后
为什么要重构
这种重构技术是对用类替换类型代码的一种更复杂的变体。
与第一个重构方法一样,你有一组简单值,这些值构成字段的所有允许值。虽然这些值通常被指定为常量,并具有可理解的名称,但它们的使用会使你的代码非常容易出错,因为它们在本质上仍然是原始值。例如,你有一个方法接受这些值中的一个作为参数。在某个时刻,方法收到的字符串为小写形式("admin"
),而不是常量USER_TYPE_ADMIN
对应的值"ADMIN"
,这将导致执行与作者(你)原本意图不同的操作。
在这里,我们处理的控制流代码包括条件语句if
、switch
和?:
。换句话说,具有编码值的字段(例如$user->type === self::USER_TYPE_ADMIN
)在这些运算符的条件中被使用。如果在这里使用用类替换类型代码,所有这些控制流结构最好移动到一个负责数据类型的类中。最终,这当然会创建一个非常类似于原来的类型类,但同样存在原有的问题。
好处
-
删除控制流代码。将原类中笨重的
switch
代码移动到适当的子类中。这提高了对单一职责原则的遵循,并使程序整体上更具可读性。 -
如果需要为编码类型添加一个新值,你只需添加一个新的子类,而无需触及现有代码(参见开闭原则)。
-
通过用类替换类型代码,我们为编程语言层面的方法和字段提供了类型提示。这在使用简单的数字或字符串值构成的编码类型时是无法实现的。
何时不使用
-
如果你已经有了类层次结构,这种技术就不适用。在面向对象编程中,你无法通过继承创建双重层次结构。不过,你可以通过组合而非继承来替换类型代码。为此,请使用 用状态/策略替换类型代码。
-
如果类型代码的值在对象创建后可以更改,避免使用此技术。我们必须以某种方式在运行时替换对象本身的类,这是不可能的。不过,在这种情况下,替代方案也是 用状态/策略替换类型代码。
如何重构
-
使用 自封装字段 为包含类型代码的字段创建一个 getter。
-
使超类构造函数为私有。创建一个与超类构造函数具有相同参数的静态工厂方法。它必须包含一个参数,用于接收编码类型的起始值。根据这个参数,工厂方法将创建不同子类的对象。为此,在其代码中必须创建一个大型条件判断,但至少在确实必要时它是唯一的;否则,子类和多态性将会起作用。
-
为编码类型的每个值创建一个唯一的子类。在其中,重定义编码类型的 getter,使其返回对应的编码类型的值。
-
从超类中删除带有类型代码的字段。使其 getter 为抽象。
-
一旦你有了子类,就可以开始将字段和方法从超类移动到相应的子类中(借助 向下推送字段 和 向下推送方法)。
-
当所有可能的内容都已移动后,使用 用多态性替换条件 来彻底摆脱一次性使用类型代码的条件。
</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/replace-type-code-with-state-strategy
什么是类型代码? 类型代码是指,当你不是使用单独的数据类型,而是有一组数字或字符串形成某个实体的允许值列表时。通常,这些特定的数字和字符串通过常量赋予可理解的名称,这就是为什么这种类型代码如此常见的原因。
问题
你有一个影响行为的编码类型,但你无法使用子类来消除它。
解决方案
用状态对象替换类型代码。如果有必要用类型代码替换字段值,可以“插入”另一个状态对象。
在此之前!用状态策略替换类型代码 - 之前之后!用状态策略替换类型代码 - 之后
为什么要重构
你有类型代码,它影响类的行为,因此我们不能使用用类替换类型代码。
类型代码影响类的行为,但由于现有的类层次结构或其他原因,我们无法为编码类型创建子类。这意味着我们不能应用用子类替换类型代码。
好处
-
这种重构技术是解决当具有编码类型的字段在对象生命周期中改变其值的情况的一种方法。在这种情况下,通过替换原始类所引用的状态对象来替换值。
-
如果需要添加一个编码类型的新值,你只需添加一个新的状态子类,而无需更改现有代码(参见开放/封闭原则)。
缺点
- 如果你有一个简单的类型代码案例,但仍然使用这种重构技术,你将会有许多多余(且不需要)的类。
好消息
这种重构技术的实现可以使用两种设计模式之一:状态或策略。无论选择哪种模式,实施方式都是一样的。那么在特定情况下你应该选择哪种模式呢?
如果你试图拆分控制算法选择的条件,请使用策略。
但如果编码类型的每个值不仅负责选择算法,还负责类的整体状态、字段值和许多其他操作,状态更适合这个工作。
如何重构
-
使用自我封装字段为包含类型代码的字段创建一个 getter。
-
创建一个新类,并赋予它一个适合类型代码目的的可理解名称。这个类将扮演状态(或策略)的角色。在其中,创建一个抽象的编码字段 getter。
-
为编码类型的每个值创建状态类的子类。在每个子类中,重定义编码字段的 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 个小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我想想…
简化条件表达式
原文:
refactoringguru.cn/refactoring/techniques/simplifying-conditional-expressions
条件语句的逻辑往往随着时间的推移变得越来越复杂,还有更多技术可以应对这一点。
拆分条件
问题: 你有一个复杂的条件(if-then
/else
或 switch
)。
解决方案: 将条件语句中复杂的部分拆分为单独的方法:条件、then
和 else
。
合并条件表达式
问题: 你有多个条件语句导致相同的结果或操作。
解决方案: 将所有这些条件合并为一个表达式。
合并重复的条件片段
问题: 在条件的所有分支中都可以找到相同的代码。
解决方案: 将代码移出条件语句。
移除控制标志
问题: 你有一个布尔变量,它作为多个布尔表达式的控制标志。
解决方案: 不要使用变量,而是使用 break
、continue
和 return
。
用守卫语句替换嵌套条件
问题: 你有一组嵌套的条件语句,很难确定代码执行的正常流程。
解决方案: 将所有特殊检查和边界情况隔离到单独的子句中,并将它们放在主要检查之前。理想情况下,你应该有一个“扁平”的条件列表,一个接一个。
用多态替换条件
问题: 你有一个条件语句,根据对象类型或属性执行不同的操作。
解决方案: 创建与条件分支相匹配的子类。在这些子类中,创建一个共享方法,并将对应条件分支的代码移动到其中。然后用相关的方法调用替换条件语句。结果是,适当的实现将通过多态性根据对象类来获得。
引入空对象
问题: 由于某些方法返回 null
而不是实际对象,你的代码中有许多对 null
的检查。
解决方案: 不要返回 null
,而是返回一个显示默认行为的空对象。
引入断言
问题: 为了使一段代码正常工作,某些条件或值必须为真。
解决方案: 用具体的断言检查替换这些假设。
分解条件
问题
你有一个复杂的条件(if-then
/else
或switch
)。
解决方案
将条件的复杂部分分解为单独的方法:条件、then
和else
。
之前
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
}
else {
charge = quantity * summerRate;
}
之后
if (isSummer(date)) {
charge = summerCharge(quantity);
}
else {
charge = winterCharge(quantity);
}
之前
if (date < SUMMER_START || date > SUMMER_END)
{
charge = quantity * winterRate + winterServiceCharge;
}
else
{
charge = quantity * summerRate;
}
之后
if (isSummer(date))
{
charge = SummerCharge(quantity);
}
else
{
charge = WinterCharge(quantity);
}
之前
if ($date->before(SUMMER_START) || $date->after(SUMMER_END)) {
$charge = $quantity * $winterRate + $winterServiceCharge;
} else {
$charge = $quantity * $summerRate;
}
之后
if (isSummer($date)) {
$charge = summerCharge($quantity);
} else {
$charge = winterCharge($quantity);
}
之前
if date.before(SUMMER_START) or date.after(SUMMER_END):
charge = quantity * winterRate + winterServiceCharge
else:
charge = quantity * summerRate
之后
if isSummer(date):
charge = summerCharge(quantity)
else:
charge = winterCharge(quantity)
之前
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * winterRate + winterServiceCharge;
}
else {
charge = quantity * summerRate;
}
之后
if (isSummer(date)) {
charge = summerCharge(quantity);
}
else {
charge = winterCharge(quantity);
}
为什么重构
代码越长,理解起来就越困难。当代码充满条件时,事情变得更加难以理解:
-
当你忙于弄清楚
then
块中的代码时,你会忘记相关条件是什么。 -
当你忙于解析
else
时,你会忘记then
中的代码做了什么。
好处
-
通过将条件代码提取到明确命名的方法中,你为将来维护代码的人(比如两个月后的你)简化了工作。
-
这个重构技术也适用于条件中的短表达式。字符串
isSalaryDay()
比用于比较日期的代码要美观且更具描述性。
如何重构
-
通过提取方法将条件提取到单独的方法中。
-
对
then
和else
块重复此过程。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读腻了吗?
不奇怪,阅读我们这里的所有文本需要 7 个小时。
尝试我们的互动重构课程。这提供了一种不那么乏味的学习新知识的方法。
我们来看看…
整合条件表达式
问题
你有多个条件导致相同的结果或动作。
解决方案
将所有这些条件整合到一个表达式中。
之前
double disabilityAmount() {
if (seniority < 2) {
return 0;
}
if (monthsDisabled > 12) {
return 0;
}
if (isPartTime) {
return 0;
}
// Compute the disability amount.
// ...
}
之后
double disabilityAmount() {
if (isNotEligibleForDisability()) {
return 0;
}
// Compute the disability amount.
// ...
}
之前
double DisabilityAmount()
{
if (seniority < 2)
{
return 0;
}
if (monthsDisabled > 12)
{
return 0;
}
if (isPartTime)
{
return 0;
}
// Compute the disability amount.
// ...
}
之后
double DisabilityAmount()
{
if (IsNotEligibleForDisability())
{
return 0;
}
// Compute the disability amount.
// ...
}
之前
function disabilityAmount() {
if ($this->seniority < 2) {
return 0;
}
if ($this->monthsDisabled > 12) {
return 0;
}
if ($this->isPartTime) {
return 0;
}
// compute the disability amount
...
之后
function disabilityAmount() {
if ($this->isNotEligibleForDisability()) {
return 0;
}
// compute the disability amount
...
之前
def disabilityAmount():
if seniority < 2:
return 0
if monthsDisabled > 12:
return 0
if isPartTime:
return 0
# Compute the disability amount.
# ...
之后
def disabilityAmount():
if isNotEligibleForDisability():
return 0
# Compute the disability amount.
# ...
之前
disabilityAmount(): number {
if (seniority < 2) {
return 0;
}
if (monthsDisabled > 12) {
return 0;
}
if (isPartTime) {
return 0;
}
// Compute the disability amount.
// ...
}
之后
disabilityAmount(): number {
if (isNotEligibleForDisability()) {
return 0;
}
// Compute the disability amount.
// ...
}
为什么重构
你的代码包含许多交替的操作符,执行相同的操作。操作符分开的原因并不明确。
整合的主要目的是将条件提取到一个单独的方法中,以获得更大的清晰度。
好处
-
消除了重复的控制流代码。结合多个具有相同“目的地”的条件,有助于表明你只在进行一个复杂的检查,导致一个动作。
-
通过整合所有操作符,你现在可以用一种新的方法将这个复杂表达式隔离开来,其名称解释了条件的目的。
如何重构
在重构之前,确保条件没有任何“副作用”或以其他方式修改某些内容,而只是返回值。副作用可能隐藏在操作符本身内部执行的代码中,例如,当根据条件的结果向变量添加内容时。
-
通过使用
and
和or
将条件整合到一个表达式中。整合时的一般规则是:-
嵌套条件使用
and
连接。 -
连续条件使用
or
连接。
-
-
对操作符条件执行提取方法,并给方法命名以反映表达式的目的。
</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/consolidate-duplicate-conditional-fragments
问题
相同的代码可以在条件的所有分支中找到。
解决方案
将代码移出条件语句。
之前
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.98;
send();
}
之后
if (isSpecialDeal()) {
total = price * 0.95;
}
else {
total = price * 0.98;
}
send();
之前
if (IsSpecialDeal())
{
total = price * 0.95;
Send();
}
else
{
total = price * 0.98;
Send();
}
之后
if (IsSpecialDeal())
{
total = price * 0.95;
}
else
{
total = price * 0.98;
}
Send();
之前
if (isSpecialDeal()) {
$total = $price * 0.95;
send();
} else {
$total = $price * 0.98;
send();
}
之后
if (isSpecialDeal()) {
$total = $price * 0.95;
} else {
$total = $price * 0.98;
}
send();
之前
if isSpecialDeal():
total = price * 0.95
send()
else:
total = price * 0.98
send()
之后
if isSpecialDeal():
total = price * 0.95
else:
total = price * 0.98
send()
之前
if (isSpecialDeal()) {
total = price * 0.95;
send();
}
else {
total = price * 0.98;
send();
}
之后
if (isSpecialDeal()) {
total = price * 0.95;
}
else {
total = price * 0.98;
}
send();
为什么要重构
在条件的所有分支中发现重复代码,通常是条件分支内代码演变的结果。团队开发可能是导致这一现象的因素之一。
好处
- 代码去重。
如何重构
-
如果重复代码位于条件分支的开头,请将代码移到条件语句之前。
-
如果代码在分支的末尾执行,请将其放置在条件语句之后。
-
如果重复代码随机位于分支内部,首先尝试将代码移到分支的开头或结尾,这取决于它是否会改变后续代码的结果。
-
如果合适,并且重复代码超过一行,尝试使用 提取方法。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
移除控制标志
问题
你有一个布尔变量作为多个布尔表达式的控制标志。
解决方案
不要使用变量,使用break
、continue
和return
。
为什么要重构
控制标志可以追溯到古老的编程时代,那时“合格”的程序员总是为他们的函数设置一个入口点(函数声明行)和一个出口点(在函数的最后)。
在现代编程语言中,这种风格的编程已过时,因为我们有特殊的操作符来修改循环和其他复杂结构中的控制流:
-
break
:停止循环。 -
continue
:停止当前循环分支的执行,并在下一个迭代中检查循环条件。 -
return
:停止整个函数的执行并返回其结果(如果在操作符中给出)。
好处
- 控制标志代码通常比使用控制流操作符编写的代码要繁琐得多。
如何重构
-
找到导致退出循环或当前迭代的控制标志的值赋值。
-
如果这是退出循环,则用
break
替换;如果这是退出迭代,则用continue
替换;如果需要从函数返回此值,则用return
替换。 -
删除与控制标志相关的剩余代码和检查。
</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/replace-nested-conditional-with-guard-clauses
问题
您有一组嵌套条件,难以确定代码执行的正常流程。
解决方案
将所有特殊检查和边界情况隔离到单独的子句中,并将它们放在主要检查之前。理想情况下,您应该有一个“扁平”的条件列表,一个接一个。
之前
public double getPayAmount() {
double result;
if (isDead){
result = deadAmount();
}
else {
if (isSeparated){
result = separatedAmount();
}
else {
if (isRetired){
result = retiredAmount();
}
else{
result = normalPayAmount();
}
}
}
return result;
}
之后
public double getPayAmount() {
if (isDead){
return deadAmount();
}
if (isSeparated){
return separatedAmount();
}
if (isRetired){
return retiredAmount();
}
return normalPayAmount();
}
之前
public double GetPayAmount()
{
double result;
if (isDead)
{
result = DeadAmount();
}
else
{
if (isSeparated)
{
result = SeparatedAmount();
}
else
{
if (isRetired)
{
result = RetiredAmount();
}
else
{
result = NormalPayAmount();
}
}
}
return result;
}
之后
public double GetPayAmount()
{
if (isDead)
{
return DeadAmount();
}
if (isSeparated)
{
return SeparatedAmount();
}
if (isRetired)
{
return RetiredAmount();
}
return NormalPayAmount();
}
之前
function getPayAmount() {
if ($this->isDead) {
$result = $this->deadAmount();
} else {
if ($this->isSeparated) {
$result = $this->separatedAmount();
} else {
if ($this->isRetired) {
$result = $this->retiredAmount();
} else {
$result = $this->normalPayAmount();
}
}
}
return $result;
}
之后
function getPayAmount() {
if ($this->isDead) {
return $this->deadAmount();
}
if ($this->isSeparated) {
return $this->separatedAmount();
}
if ($this->isRetired) {
return $this->retiredAmount();
}
return $this->normalPayAmount();
}
之前
def getPayAmount(self):
if self.isDead:
result = deadAmount()
else:
if self.isSeparated:
result = separatedAmount()
else:
if self.isRetired:
result = retiredAmount()
else:
result = normalPayAmount()
return result
之后
def getPayAmount(self):
if self.isDead:
return deadAmount()
if self.isSeparated:
return separatedAmount()
if self.isRetired:
return retiredAmount()
return normalPayAmount()
之前
getPayAmount(): number {
let result: number;
if (isDead){
result = deadAmount();
}
else {
if (isSeparated){
result = separatedAmount();
}
else {
if (isRetired){
result = retiredAmount();
}
else{
result = normalPayAmount();
}
}
}
return result;
}
之后
getPayAmount(): number {
if (isDead){
return deadAmount();
}
if (isSeparated){
return separatedAmount();
}
if (isRetired){
return retiredAmount();
}
return normalPayAmount();
}
为什么要重构
识别“地狱条件”相对简单。每个嵌套层级的缩进形成一支箭头,指向痛苦与困惑的方向:
if () {
if () {
do {
if () {
if () {
if () {
...
}
}
...
}
...
}
while ();
...
}
else {
...
}
}
很难弄清楚每个条件的作用和如何运作,因为代码执行的“正常”流程并不明显。这些条件表明了混乱的演变,每个条件都是作为权宜之计添加的,而没有考虑到优化整体结构。
为简化情况,将特殊情况隔离到单独的条件中,如果保护子句为真,则立即结束执行并返回一个空值。实际上,您在这里的任务是使结构变得扁平。
如何重构
尝试消除代码中的副作用——将查询与修改分离可能对这个目的有帮助。这个解决方案对于下面描述的重组是必要的。
-
将所有导致调用异常或立即返回值的保护子句隔离出来。将这些条件放在方法的开头。
-
在重排完成并且所有测试成功后,查看是否可以使用合并条件表达式来处理导致相同异常或返回值的保护子句。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
难怪,阅读这里所有文本需要 7 个小时。
尝试我们的交互式重构课程。它提供了一种不那么繁琐的学习新知识的方法。
让我们看看…
用多态性替换条件
问题
您有一个根据对象类型或属性执行各种操作的条件。
解决方案
创建与条件分支匹配的子类。在这些子类中,创建一个共享方法,并将相应条件分支的代码移到其中。然后用相关方法调用替换条件。最终通过多态性将获得正确的实现,具体取决于对象类。
之前
class Bird {
// ...
double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException("Should be unreachable");
}
}
之后
abstract class Bird {
// ...
abstract double getSpeed();
}
class European extends Bird {
double getSpeed() {
return getBaseSpeed();
}
}
class African extends Bird {
double getSpeed() {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
double getSpeed() {
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}
// Somewhere in client code
speed = bird.getSpeed();
之前
public class Bird
{
// ...
public double GetSpeed()
{
switch (type)
{
case EUROPEAN:
return GetBaseSpeed();
case AFRICAN:
return GetBaseSpeed() - GetLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return isNailed ? 0 : GetBaseSpeed(voltage);
default:
throw new Exception("Should be unreachable");
}
}
}
之后
public abstract class Bird
{
// ...
public abstract double GetSpeed();
}
class European: Bird
{
public override double GetSpeed()
{
return GetBaseSpeed();
}
}
class African: Bird
{
public override double GetSpeed()
{
return GetBaseSpeed() - GetLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue: Bird
{
public override double GetSpeed()
{
return isNailed ? 0 : GetBaseSpeed(voltage);
}
}
// Somewhere in client code
speed = bird.GetSpeed();
之前
class Bird {
// ...
public function getSpeed() {
switch ($this->type) {
case EUROPEAN:
return $this->getBaseSpeed();
case AFRICAN:
return $this->getBaseSpeed() - $this->getLoadFactor() * $this->numberOfCoconuts;
case NORWEGIAN_BLUE:
return ($this->isNailed) ? 0 : $this->getBaseSpeed($this->voltage);
}
throw new Exception("Should be unreachable");
}
// ...
}
之后
abstract class Bird {
// ...
abstract function getSpeed();
// ...
}
class European extends Bird {
public function getSpeed() {
return $this->getBaseSpeed();
}
}
class African extends Bird {
public function getSpeed() {
return $this->getBaseSpeed() - $this->getLoadFactor() * $this->numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
public function getSpeed() {
return ($this->isNailed) ? 0 : $this->getBaseSpeed($this->voltage);
}
}
// Somewhere in Client code.
$speed = $bird->getSpeed();
之前
class Bird:
# ...
def getSpeed(self):
if self.type == EUROPEAN:
return self.getBaseSpeed()
elif self.type == AFRICAN:
return self.getBaseSpeed() - self.getLoadFactor() * self.numberOfCoconuts
elif self.type == NORWEGIAN_BLUE:
return 0 if self.isNailed else self.getBaseSpeed(self.voltage)
else:
raise Exception("Should be unreachable")
之后
class Bird:
# ...
def getSpeed(self):
pass
class European(Bird):
def getSpeed(self):
return self.getBaseSpeed()
class African(Bird):
def getSpeed(self):
return self.getBaseSpeed() - self.getLoadFactor() * self.numberOfCoconuts
class NorwegianBlue(Bird):
def getSpeed(self):
return 0 if self.isNailed else self.getBaseSpeed(self.voltage)
# Somewhere in client code
speed = bird.getSpeed()
之前
class Bird {
// ...
getSpeed(): number {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new Error("Should be unreachable");
}
}
之后
abstract class Bird {
// ...
abstract getSpeed(): number;
}
class European extends Bird {
getSpeed(): number {
return getBaseSpeed();
}
}
class African extends Bird {
getSpeed(): number {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
getSpeed(): number {
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}
// Somewhere in client code
let speed = bird.getSpeed();
为什么重构
如果您的代码包含根据以下内容执行各种任务的操作符,这种重构技术将有所帮助:
-
对象的类或它实现的接口
-
对象字段的值
-
调用对象方法之一的结果
如果出现新的对象属性或类型,您需要在所有类似条件中搜索并添加代码。因此,如果对象的所有方法中散布着多个条件,这种技术的好处将成倍增加。
好处
-
这种技术遵循告知-不询问原则:与其询问对象的状态并根据此执行操作,不如简单地告诉对象它需要做什么,让它自己决定如何执行。
-
消除重复代码。您摆脱了许多几乎相同的条件。
-
如果需要添加新的执行变体,只需添加一个新子类,而无需修改现有代码(开放/封闭原则)。
如何重构
准备重构
对于这种重构技术,您应该有一个准备好的类层级,包含替代行为。如果没有这样的层级,请创建一个。其他技术将帮助实现这一目标:
-
用子类替换类型代码。将为特定对象属性的所有值创建子类。这种方法简单但灵活性较差,因为您无法为对象的其他属性创建子类。
-
用状态/策略替换类型代码。将为特定对象属性专门创建一个类,并为该属性的每个值从中创建子类。当前类将包含对这种类型对象的引用,并将执行委托给它们。
以下步骤假设您已经创建了层级结构。
重构步骤
-
如果条件在执行其他操作的方法中,请执行提取方法。
-
对于每个层级子类,重定义包含条件的方法,并将相应条件分支的代码复制到该位置。
-
从条件中删除此分支。
-
重复替换直到条件为空。然后删除条件并将方法声明为抽象。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
厌倦阅读了吗?
毫无疑问,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
我们来看…
引入空对象
问题
由于一些方法返回null
而不是实际对象,您的代码中有许多对null
的检查。
解决方案
返回空对象而不是null
,使其表现出默认行为。
之前
if (customer == null) {
plan = BillingPlan.basic();
}
else {
plan = customer.getPlan();
}
之后
class NullCustomer extends Customer {
boolean isNull() {
return true;
}
Plan getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}
// Replace null values with Null-object.
customer = (order.customer != null) ?
order.customer : new NullCustomer();
// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
之前
if (customer == null)
{
plan = BillingPlan.Basic();
}
else
{
plan = customer.GetPlan();
}
之后
public sealed class NullCustomer: Customer
{
public override bool IsNull
{
get { return true; }
}
public override Plan GetPlan()
{
return new NullPlan();
}
// Some other NULL functionality.
}
// Replace null values with Null-object.
customer = order.customer ?? new NullCustomer();
// Use Null-object as if it's normal subclass.
plan = customer.GetPlan();
之前
if ($customer === null) {
$plan = BillingPlan::basic();
} else {
$plan = $customer->getPlan();
}
之后
class NullCustomer extends Customer {
public function isNull() {
return true;
}
public function getPlan() {
return new NullPlan();
}
// Some other NULL functionality.
}
// Replace null values with Null-object.
$customer = ($order->customer !== null) ?
$order->customer :
new NullCustomer;
// Use Null-object as if it's normal subclass.
$plan = $customer->getPlan();
之前
if customer is None:
plan = BillingPlan.basic()
else:
plan = customer.getPlan()
之后
class NullCustomer(Customer):
def isNull(self):
return True
def getPlan(self):
return self.NullPlan()
# Some other NULL functionality.
# Replace null values with Null-object.
customer = order.customer or NullCustomer()
# Use Null-object as if it's normal subclass.
plan = customer.getPlan()
之前
if (customer == null) {
plan = BillingPlan.basic();
}
else {
plan = customer.getPlan();
}
之后
class NullCustomer extends Customer {
isNull(): boolean {
return true;
}
getPlan(): Plan {
return new NullPlan();
}
// Some other NULL functionality.
}
// Replace null values with Null-object.
let customer = (order.customer != null) ?
order.customer : new NullCustomer();
// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
为什么要重构
对null
的多次检查使您的代码变得更长且更丑。
缺点
- 摆脱条件语句的代价是创建另一个新类。
如何重构
-
从相关类创建一个子类,作为空对象的角色。
-
在两个类中创建方法
isNull()
,该方法对空对象返回true
,对实际类返回false
。 -
找到所有可能返回
null
而不是实际对象的地方。更改代码以返回一个空对象。 -
找到所有将实际类的变量与
null
进行比较的地方。用对isNull()
的调用替换这些检查。 -
-
如果原始类的方法在值不等于
null
的条件下运行,请在空类中重新定义这些方法,并将else
部分的代码插入其中。然后可以删除整个条件,差异化的行为将通过多态性实现。 -
如果事情并不简单且方法无法重新定义,请看看是否可以将原本应该在
null
值情况下执行的操作提取到空对象的新方法中。将这些方法替换为else
中的旧代码作为默认操作。
-
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读腻了吗?
难怪,阅读我们这里的所有文本需要 7 个小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
引入断言
问题
为了让一段代码正确工作,某些条件或值必须为真。
解决方案
用具体的断言检查替换这些假设。
之前
double getExpenseLimit() {
// Should have either expense limit or
// a primary project.
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit :
primaryProject.getMemberExpenseLimit();
}
之后
double getExpenseLimit() {
Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit:
primaryProject.getMemberExpenseLimit();
}
之前
double GetExpenseLimit()
{
// Should have either expense limit or
// a primary project.
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit :
primaryProject.GetMemberExpenseLimit();
}
之后
double GetExpenseLimit()
{
Assert.IsTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit:
primaryProject.GetMemberExpenseLimit();
}
之前
function getExpenseLimit() {
// Should have either expense limit or
// a primary project.
return ($this->expenseLimit !== NULL_EXPENSE) ?
$this->expenseLimit:
$this->primaryProject->getMemberExpenseLimit();
}
之后
function getExpenseLimit() {
assert($this->expenseLimit !== NULL_EXPENSE || isset($this->primaryProject));
return ($this->expenseLimit !== NULL_EXPENSE) ?
$this->expenseLimit:
$this->primaryProject->getMemberExpenseLimit();
}
之前
def getExpenseLimit(self):
# Should have either expense limit or
# a primary project.
return self.expenseLimit if self.expenseLimit != NULL_EXPENSE else \
self.primaryProject.getMemberExpenseLimit()
之后
def getExpenseLimit(self):
assert (self.expenseLimit != NULL_EXPENSE) or (self.primaryProject != None)
return self.expenseLimit if (self.expenseLimit != NULL_EXPENSE) else \
self.primaryProject.getMemberExpenseLimit()
之前
getExpenseLimit(): number {
// Should have either expense limit or
// a primary project.
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit:
primaryProject.getMemberExpenseLimit();
}
之后
getExpenseLimit(): number {
// TypeScript and JS doesn't have built-in assertions, so we'll use
// good-old console.error(). You can always extract this into a
// designated assertion function.
if (!(expenseLimit != NULL_EXPENSE ||
(typeof primaryProject !== 'undefined' && primaryProject))) {
console.error("Assertion failed: getExpenseLimit()");
}
return (expenseLimit != NULL_EXPENSE) ?
expenseLimit:
primaryProject.getMemberExpenseLimit();
}
为什么重构
假设一段代码假设了某个对象的当前状态或参数或局部变量的值。通常,这种假设在出现错误时才会失效。
通过添加相应的断言使这些假设变得明显。与方法参数中的类型提示一样,这些断言可以作为代码的实时文档。
作为检查代码需要断言的指导方针,请查看描述特定方法工作条件的注释。
好处
- 如果一个假设不成立,导致代码给出错误结果,那么最好在此之前停止执行,以免造成致命后果和数据损坏。这也意味着在设计测试程序时,你忽略了写必要的测试。
缺点
-
有时候,抛出异常比简单的断言更合适。你可以选择必要的异常类,并让其余代码正确处理它。
-
什么时候异常比简单断言更好?如果异常可以由用户或系统的操作引起,并且你能够处理该异常。另一方面,普通的未命名和未处理的异常基本上等同于简单的断言——你不处理它们,它们是程序错误的结果,这种错误本不该发生。
如何重构
当你看到某个条件被假设时,添加对此条件的断言以确保其正确性。
添加断言不应改变程序的行为。
不要在代码的所有地方过度使用断言。只检查对代码正确运行所必需的条件。如果你的代码即使在某个特定断言为假时仍能正常工作,你可以安全地移除该断言。
</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/simplifying-method-calls
这些技术使方法调用变得更简单、更易理解。这反过来又简化了类之间交互的接口。
重命名方法
问题: 一个方法的名称未能解释该方法的功能。
解决方案: 重命名该方法。
添加参数
问题: 一个方法没有足够的数据来执行某些操作。
解决方案: 创建一个新参数来传递必要的数据。
移除参数
问题: 一个参数在方法体内未被使用。
解决方案: 移除未使用的参数。
将查询与修改分开
问题: 你是否有一个返回值的方法,同时还改变了对象内部的某些内容?
解决方案: 将方法拆分为两个独立的方法。正如你所期待的,其中一个应返回值,另一个则修改对象。
参数化方法
问题: 多个方法执行类似的操作,仅在其内部值、数字或操作上有所不同。
解决方案: 通过使用一个参数来传递必要的特殊值,将这些方法合并。
用显式方法替代参数
问题: 一个方法被分成多个部分,每个部分的执行依赖于一个参数的值。
解决方案: 将方法的各个部分提取到它们自己的方法中,并调用这些方法,而不是原始方法。
保持整个对象
问题: 你从一个对象中获取多个值,然后将它们作为参数传递给一个方法。
解决方案: 相反,尝试传递整个对象。
用方法调用替代参数
问题: 调用查询方法并将其结果作为参数传递给另一个方法,而该方法本可以直接调用查询。
解决方案: 尝试在方法体内放置查询调用,而不是通过参数传递值。
引入参数对象
问题: 你的方法包含一组重复的参数。
解决方案: 用一个对象替代这些参数。
移除设置方法
问题: 字段的值应在创建时设置,并且之后不应改变。
解决方案: 因此,移除设置字段值的方法。
隐藏方法
问题: 一个方法未被其他类使用,或仅在其自己的类层次内使用。
解决方案: 将该方法设置为私有或受保护。
用工厂方法替代构造函数
问题: 你有一个复杂的构造函数,它不仅仅是设置对象字段中的参数值。
解决方案: 创建一个工厂方法,用它替换构造函数调用。
用异常替换错误代码
问题: 一个方法返回一个特殊值来表示错误吗?
解决方案: 改为抛出异常。
用测试替换异常
问题: 你在一个简单测试可以解决的地方抛出异常吗?
解决方案: 用条件测试替换异常。
重命名方法
问题
方法的名称并没有解释该方法的功能。
解决方案
重命名方法。
在此之前!重命名方法 - 之前之后!重命名方法 - 之后
为什么重构
也许这个方法从一开始就命名不当——例如,有人匆忙创建了该方法,并没有认真对待命名。
或者,也许该方法起初命名得当,但随着其功能的增加,方法名称就不再是一个好的描述符。
益处
- 代码可读性。尝试给新方法起一个反映其功能的名称。类似于
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 个小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看…
添加参数
问题
方法没有足够的数据来执行某些操作。
解决方案
创建一个新参数以传递必要的数据。
之前!添加参数 - 之前之后!添加参数 - 之后
为什么重构
您需要对方法进行更改,而这些更改需要添加以前未提供给该方法的信息或数据。
好处
- 在这里的选择是添加一个新参数还是添加一个包含方法所需数据的新私有字段。当您需要一些偶尔或频繁变化的数据,而不必一直将其保留在对象中时,使用参数更为合适。在这种情况下,重构会带来收益。否则,添加一个私有字段,并在调用方法之前用必要的数据填充它。
缺点
-
添加一个新参数总是比删除它容易,这就是为什么参数列表经常膨胀到离谱的大小。这种现象被称为长参数列表。
-
如果您需要添加一个新参数,这有时意味着您的类不包含必要的数据,或者现有参数不包含必要的相关数据。在这两种情况下,最佳解决方案是考虑将数据移动到主类或其他可以在方法内部访问的类中。
如何重构
-
查看该方法是否在超类或子类中定义。如果该方法存在于它们中,您需要在这些类中重复所有步骤。
-
接下来的步骤对保持程序在重构过程中的功能至关重要。通过复制旧方法创建一个新方法,并为其添加必要的参数。用对新方法的调用替换旧方法的代码。您可以将任何值插入到新参数中(例如,对于对象使用
null
,对于数字使用零)。 -
查找所有对旧方法的引用,并将其替换为对新方法的引用。
-
删除旧方法。如果旧方法是公共接口的一部分,则无法删除。在这种情况下,将旧方法标记为已弃用。
</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 个小时。
尝试我们的互动重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看…
用显式方法替换参数
原文:
refactoringguru.cn/replace-parameter-with-explicit-methods
问题
方法被拆分为几个部分,每个部分根据参数的值运行。
解决方案
将方法的各个部分提取到自己的方法中,并调用它们来替代原始方法。
之前
void setValue(String name, int value) {
if (name.equals("height")) {
height = value;
return;
}
if (name.equals("width")) {
width = value;
return;
}
Assert.shouldNeverReachHere();
}
之后
void setHeight(int arg) {
height = arg;
}
void setWidth(int arg) {
width = arg;
}
之前
void SetValue(string name, int value)
{
if (name.Equals("height"))
{
height = value;
return;
}
if (name.Equals("width"))
{
width = value;
return;
}
Assert.Fail();
}
之后
void SetHeight(int arg)
{
height = arg;
}
void SetWidth(int arg)
{
width = arg;
}
之前
function setValue($name, $value) {
if ($name === "height") {
$this->height = $value;
return;
}
if ($name === "width") {
$this->width = $value;
return;
}
assert("Should never reach here");
}
之后
function setHeight($arg) {
$this->height = $arg;
}
function setWidth($arg) {
$this->width = $arg;
}
之前
def output(self, type):
if name == "banner"
# Print the banner.
# ...
if name == "info"
# Print the info.
# ...
之后
def outputBanner(self):
# Print the banner.
# ...
def outputInfo(self):
# Print the info.
# ...
之前
setValue(name: string, value: number): void {
if (name.equals("height")) {
height = value;
return;
}
if (name.equals("width")) {
width = value;
return;
}
}
之后
setHeight(arg: number): void {
height = arg;
}
setWidth(arg: number): number {
width = arg;
}
为什么重构
一个包含依赖参数变体的方法变得庞大。每个分支中都运行非平凡的代码,并且新变体很少被添加。
好处
- 提高代码可读性。理解
startEngine()
的目的要比理解setValue("engineEnabled", true)
容易得多。
何时不使用
- 如果一个方法很少更改且不会添加新变体,则不要用显式方法替换参数。
如何重构
-
对于每种方法变体,创建一个单独的方法。根据主方法中参数的值运行这些方法。
-
找到所有调用原始方法的地方。在这些地方,调用其中一个新的依赖参数的变体。
-
当没有原始方法的调用时,删除它。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读得累吗?
不奇怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看……
保持整个对象
问题
你从一个对象获取多个值,然后将它们作为参数传递给一个方法。
解决方案
相反,试着传递整个对象。
之前
int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
之后
boolean withinPlan = plan.withinRange(daysTempRange);
之前
int low = daysTempRange.GetLow();
int high = daysTempRange.GetHigh();
bool withinPlan = plan.WithinRange(low, high);
之后
bool withinPlan = plan.WithinRange(daysTempRange);
之前
$low = $daysTempRange->getLow();
$high = $daysTempRange->getHigh();
$withinPlan = $plan->withinRange($low, $high);
之后
$withinPlan = $plan->withinRange($daysTempRange);
之前
low = daysTempRange.getLow()
high = daysTempRange.getHigh()
withinPlan = plan.withinRange(low, high)
之后
withinPlan = plan.withinRange(daysTempRange)
之前
let low = daysTempRange.getLow();
let high = daysTempRange.getHigh();
let withinPlan = plan.withinRange(low, high);
之后
let withinPlan = plan.withinRange(daysTempRange);
为什么重构
问题是每次在调用方法之前,未来参数对象的方法都必须被调用。如果这些方法或获取的数据量发生变化,你需要仔细找到程序中十几个这样的地方,并在每个地方实施这些更改。
在应用此重构技术后,获取所有必要数据的代码将存储在一个地方——方法本身。
好处
-
你看到的不是一堆杂乱无章的参数,而是一个具有可理解名称的单一对象。
-
如果方法需要从一个对象中获取更多数据,你不需要重写所有使用该方法的地方——只需在方法内部进行修改。
缺点
- 有时这种转变会导致方法变得不那么灵活:之前方法可以从许多不同来源获取数据,但现在由于重构,我们将其使用限制在只有特定接口的对象。
如何重构
-
在方法中创建一个参数,以获取所需值的对象。
-
现在开始逐一删除方法中的旧参数,用参数对象相关方法的调用替换它们。每替换一个参数后测试程序。
-
从参数对象中删除在方法调用之前的获取器代码。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦阅读了吗?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
用方法调用替换参数
问题
调用查询方法并将其结果作为另一个方法的参数,而那个方法本可以直接调用查询。
解决方案
不要通过参数传递值,尝试在方法体内放置查询调用。
之前
int basePrice = quantity * itemPrice;
double seasonDiscount = this.getSeasonalDiscount();
double fees = this.getFees();
double finalPrice = discountedPrice(basePrice, seasonDiscount, fees);
之后
int basePrice = quantity * itemPrice;
double finalPrice = discountedPrice(basePrice);
之前
int basePrice = quantity * itemPrice;
double seasonDiscount = this.GetSeasonalDiscount();
double fees = this.GetFees();
double finalPrice = DiscountedPrice(basePrice, seasonDiscount, fees);
之后
int basePrice = quantity * itemPrice;
double finalPrice = DiscountedPrice(basePrice);
之前
$basePrice = $this->quantity * $this->itemPrice;
$seasonDiscount = $this->getSeasonalDiscount();
$fees = $this->getFees();
$finalPrice = $this->discountedPrice($basePrice, $seasonDiscount, $fees);
之后
$basePrice = $this->quantity * $this->itemPrice;
$finalPrice = $this->discountedPrice($basePrice);
之前
basePrice = quantity * itemPrice
seasonalDiscount = self.getSeasonalDiscount()
fees = self.getFees()
finalPrice = discountedPrice(basePrice, seasonalDiscount, fees)
之后
basePrice = quantity * itemPrice
finalPrice = discountedPrice(basePrice)
之前
let basePrice = quantity * itemPrice;
const seasonDiscount = this.getSeasonalDiscount();
const fees = this.getFees();
const finalPrice = discountedPrice(basePrice, seasonDiscount, fees);
之后
let basePrice = quantity * itemPrice;
let finalPrice = discountedPrice(basePrice);
为什么要重构
一长串参数很难理解。此外,对这样的方式的调用往往类似于一系列的级联,复杂且令人兴奋的值计算难以导航,但必须传递给方法。因此,如果参数值可以借助某个方法计算,请在方法内部执行此操作,去掉参数。
好处
- 我们去掉不必要的参数,简化方法调用。这些参数通常不是为了当前的项目而创建,而是为了未来可能永远不会到来的需求。
缺点
- 你可能明天需要这个参数以满足其他需求……这会让你重新编写方法。
如何重构
-
确保获取值的代码不使用当前方法的参数,因为它们在另一个方法内部不可用。如果是这样,移动代码就不可能。
-
如果相关代码比单个方法或函数调用更复杂,请使用 提取方法 将这些代码隔离到一个新方法中,使调用更简单。
-
在主方法的代码中,将所有对被替换参数的引用替换为获取值的方法调用。
-
使用 移除参数 来消除现在未使用的参数。
</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 小时。
尝试我们的交互式重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看……
删除设置方法
问题
字段的值应在创建时设置,之后不应更改。
解决方案
因此,删除设置字段值的方法。
之前!删除设置方法 - 之前之后!删除设置方法 - 之后
为什么重构
您希望防止字段值的任何更改。
如何重构
-
字段的值只能在构造函数中更改。如果构造函数中没有设置值的参数,请添加一个。
-
查找所有 setter 调用。
-
如果 setter 调用位于当前类构造函数调用后面,请将其参数移动到构造函数调用中并删除 setter。
-
在构造函数中用直接访问字段替换 setter 调用。
-
-
删除 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 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
用工厂方法替换构造函数
原文:
refactoringguru.cn/replace-constructor-with-factory-method
问题
您有一个复杂的构造函数,除了设置对象字段中的参数值外还做其他事情。
解决方案
创建一个工厂方法,并用它替换构造函数调用。
之前
class Employee {
Employee(int type) {
this.type = type;
}
// ...
}
之后
class Employee {
static Employee create(int type) {
employee = new Employee(type);
// do some heavy lifting.
return employee;
}
// ...
}
之前
public class Employee
{
public Employee(int type)
{
this.type = type;
}
// ...
}
之后
public class Employee
{
public static Employee Create(int type)
{
employee = new Employee(type);
// Do some heavy lifting.
return employee;
}
// ...
}
之前
class Employee {
// ...
public function __construct($type) {
$this->type = $type;
}
// ...
}
之后
class Employee {
// ...
static public function create($type) {
$employee = new Employee($type);
// do some heavy lifting.
return $employee;
}
// ...
}
之前
class Employee {
constructor(type: number) {
this.type = type;
}
// ...
}
之后
class Employee {
static create(type: number): Employee {
let employee = new Employee(type);
// Do some heavy lifting.
return employee;
}
// ...
}
为什么重构
使用此重构技术的最明显原因与用子类替换类型代码有关。
您的代码中之前创建了一个对象,并将编码类型的值传递给它。使用重构方法后,出现了多个子类,您需要根据编码类型的值创建对象。改变原始构造函数以返回子类对象是不可能的,因此我们创建一个静态工厂方法,它将返回所需类的对象,之后用它替换所有原始构造函数的调用。
工厂方法也可以在其他情况下使用,当构造函数无法胜任时。它们在尝试将值更改为引用时可能很重要。它们还可以用于设置超出参数数量和类型的各种创建模式。
优势
-
工厂方法不一定返回调用它的类的对象。通常,这些可以是其子类,基于传递给该方法的参数进行选择。
-
工厂方法可以有一个更好的名称,描述它返回什么以及如何返回,例如
Troops::GetCrew(myTank)
。 -
工厂方法可以返回已经创建的对象,而构造函数总是创建一个新的实例。
如何重构
-
创建一个工厂方法。在其中调用当前构造函数。
-
用对工厂方法的调用替换所有构造函数调用。
-
将构造函数声明为私有。
-
调查构造函数代码,尝试隔离与当前类对象构造无关的代码,将这些代码移动到工厂方法中。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
您的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里的所有文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
用异常替代错误代码
问题
方法返回一个指示错误的特殊值吗?
解决方案
抛出一个异常。
之前
int withdraw(int amount) {
if (amount > _balance) {
return -1;
}
else {
balance -= amount;
return 0;
}
}
之后
void withdraw(int amount) throws BalanceException {
if (amount > _balance) {
throw new BalanceException();
}
balance -= amount;
}
之前
int Withdraw(int amount)
{
if (amount > _balance)
{
return -1;
}
else
{
balance -= amount;
return 0;
}
}
之后
///<exception cref="BalanceException">Thrown when amount > _balance</exception>
void Withdraw(int amount)
{
if (amount > _balance)
{
throw new BalanceException();
}
balance -= amount;
}
之前
function withdraw($amount) {
if ($amount > $this->balance) {
return -1;
} else {
$this->balance -= $amount;
return 0;
}
}
之后
function withdraw($amount) {
if ($amount > $this->balance) {
throw new BalanceException;
}
$this->balance -= $amount;
}
之前
def withdraw(self, amount):
if amount > self.balance:
return -1
else:
self.balance -= amount
return 0
之后
def withdraw(self, amount):
if amount > self.balance:
raise BalanceException()
self.balance -= amount
之前
withdraw(amount: number): number {
if (amount > _balance) {
return -1;
}
else {
balance -= amount;
return 0;
}
}
之后
withdraw(amount: number): void {
if (amount > _balance) {
throw new Error();
}
balance -= amount;
}
为什么重构
返回错误代码是程序设计的过时遗留物。在现代编程中,错误处理由特殊类执行,这些类被称为异常。如果发生问题,你会“抛出”一个错误,然后由其中一个异常处理程序“捕获”它。在正常条件下被忽略的特殊错误处理代码被激活以作出响应。
好处
-
使代码摆脱大量检查各种错误代码的条件语句。异常处理程序是一种更加简洁的方式来区分正常执行路径和异常路径。
-
异常类可以实现自己的方法,从而包含部分错误处理功能(例如发送错误消息)。
-
与异常不同,错误代码不能在构造函数中使用,因为构造函数必须仅返回一个新对象。
缺点
- 异常处理程序可能变成一种类似于 goto 的拐杖。避免这样做!不要使用异常来管理代码执行。异常应仅用于通知错误或关键情况。
如何重构
尝试一次仅为一个错误代码执行这些重构步骤。这将更容易让你将所有重要信息牢记在心,避免错误。
-
查找所有调用返回错误代码的方法,而不是检查错误代码,将其包装在
try
/catch
块中。 -
在方法内部,抛出异常,而不是返回错误代码。
-
更改方法签名,使其包含有关抛出异常的信息(
@throws
部分)。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
读累了吗?
不奇怪,阅读我们这里所有文本需要 7 小时。
尝试我们的互动重构课程。这提供了一种不那么乏味的学习新知识的方法。
让我们看看……
用测试替换异常
问题
你在一个简单测试可以完成工作的地方抛出了异常吗?
解决方案
用条件测试替换异常。
之前
double getValueForPeriod(int periodNumber) {
try {
return values[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
之后
double getValueForPeriod(int periodNumber) {
if (periodNumber >= values.length) {
return 0;
}
return values[periodNumber];
}
之前
double GetValueForPeriod(int periodNumber)
{
try
{
return values[periodNumber];
}
catch (IndexOutOfRangeException e)
{
return 0;
}
}
之后
double GetValueForPeriod(int periodNumber)
{
if (periodNumber >= values.Length)
{
return 0;
}
return values[periodNumber];
}
之前
function getValueForPeriod($periodNumber) {
try {
return $this->values[$periodNumber];
} catch (ArrayIndexOutOfBoundsException $e) {
return 0;
}
}
之后
function getValueForPeriod($periodNumber) {
if ($periodNumber >= count($this->values)) {
return 0;
}
return $this->values[$periodNumber];
}
之前
def getValueForPeriod(periodNumber):
try:
return values[periodNumber]
except IndexError:
return 0
之后
def getValueForPeriod(self, periodNumber):
if periodNumber >= len(self.values):
return 0
return self.values[periodNumber]
之前
getValueForPeriod(periodNumber: number): number {
try {
return values[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
之后
getValueForPeriod(periodNumber: number): number {
if (periodNumber >= values.length) {
return 0;
}
return values[periodNumber];
}
为什么重构
异常应该用于处理与意外错误相关的不规则行为。它们不应该替代测试。如果可以通过在运行之前验证条件来避免异常,那么就这么做。异常应该留给真正的错误。
例如,你进入了雷区并触发了一枚地雷,导致了一个异常;这个异常被成功处理,你被抬到安全的地方。但你本可以通过一开始就阅读雷区前的警告标志来避免这一切。
好处
- 有时,简单的条件比异常处理代码更明显。
如何重构
-
为边缘案例创建条件,并将其移动到 try/catch 块之前。
-
将代码从
catch
部分移动到这个条件内。 -
在
catch
部分,放置抛出普通未命名异常的代码,并运行所有测试。 -
如果在测试中没有抛出任何异常,去掉
try
/catch
操作符。
</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/dealing-with-generalization
抽象有自己的一组重构技术,主要与沿类继承层次移动功能、创建新类和接口、以及用委托替代继承和反之相关。
提取字段
问题: 两个类有相同的字段。
解决方案: 从子类中移除字段并将其移动到超类中。
提取方法
问题: 你的子类有执行相似工作的 方法。
解决方案: 使方法相同,然后将它们移动到相关的超类中。
提取构造函数主体
问题: 你的子类的构造函数中的代码大部分是相同的。
解决方案: 创建一个超类构造函数,将子类中相同的代码移动到其中,并在子类构造函数中调用超类构造函数。
推送方法
问题: 在超类中实现的行为仅被一个(或少数几个)子类使用吗?
解决方案: 将该行为移动到子类中。
推送字段
问题: 一个字段仅在少数子类中使用吗?
解决方案: 将字段移动到这些子类中。
提取子类
问题: 一个类的特性仅在某些情况下使用。
解决方案: 创建一个子类并在这些情况下使用它。
提取超类
问题: 你有两个类有共同的字段和方法。
解决方案: 为它们创建一个共享的超类,并将所有相同的字段和方法移动到其中。
提取接口
问题: 多个客户端正在使用类接口的相同部分。另一种情况:两个类中的接口部分相同。
解决方案: 将这部分相同的内容移动到自己的接口中。
合并层次结构
问题: 你有一个类层次结构,其中子类几乎与其超类相同。
解决方案: 合并子类和超类。
形成模板方法
问题: 你的子类实现的算法包含相似步骤且顺序相同。
解决方案: 将算法结构和相同步骤移到超类中,将不同步骤的实现留在子类中。
用委托替换继承
问题: 你有一个子类仅使用超类方法的一部分(或无法继承超类数据)。
解决方案: 创建一个字段并放入超类对象,将方法委托给超类对象,并去掉继承。
用继承替换委托
问题: 一个类包含许多简单的方法,这些方法委托给另一个类的所有方法。
解决方案: 使该类成为一个委托继承者,从而使得委托方法变得不必要。
提升字段
问题
两个类有相同的字段。
解决方案
从子类中移除该字段并将其移至父类。
之前!提升字段 - 之前之后!提升字段 - 之后
为什么要重构
子类各自独立发展,导致出现相同(或几乎相同)的字段和方法。
优势
-
消除子类中的字段重复。
-
如果存在重复的方法,便于后续将其从子类迁移到父类。
如何重构
-
确保子类中的字段用于相同的需求。
-
如果字段名称不同,将它们重命名为相同的名称,并替换现有代码中所有对这些字段的引用。
-
在父类中创建一个同名字段。请注意,如果这些字段是私有的,父类字段应该是受保护的。
-
从子类中移除字段。
-
你可能想考虑使用 自我封装字段 来为新字段提供访问方法,从而隐藏它。
</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;然后在超类中抽象声明这些 getter。
-
对于方法:使用提升方法或者在超类中声明抽象方法(请注意,如果类之前不是抽象的,它将变为抽象类)。
-
-
从子类中移除方法。
-
检查方法被调用的位置。在某些地方,你可能能够用超类替代子类。
</images/refactoring/banners/tired-of-reading-banner-1x.mp4?id=7fa8f9682afda143c2a491c6ab1c1e56>
</images/refactoring/banners/tired-of-reading-banner.png?id=1721d160ff9c84cbf8912f5d282e2bb4>
你的浏览器不支持 HTML 视频。
厌倦了阅读?
难怪,阅读我们这里所有的文本需要 7 小时。
尝试我们的交互式重构课程。它提供了一种更不乏味的学习新知识的方法。
让我们看看……
提升构造函数主体
问题
你的子类具有大部分相同的构造函数代码。
解决方案
创建一个超类构造函数,并将子类中相同的代码移到其中。在子类构造函数中调用超类构造函数。
之前
class Manager extends Employee {
public Manager(String name, String id, int grade) {
this.name = name;
this.id = id;
this.grade = grade;
}
// ...
}
之后
class Manager extends Employee {
public Manager(String name, String id, int grade) {
super(name, id);
this.grade = grade;
}
// ...
}
之前
public class Manager: Employee
{
public Manager(string name, string id, int grade)
{
this.name = name;
this.id = id;
this.grade = grade;
}
// ...
}
之后
public class Manager: Employee
{
public Manager(string name, string id, int grade): base(name, id)
{
this.grade = grade;
}
// ...
}
之前
class Manager extends Employee {
public function __construct($name, $id, $grade) {
$this->name = $name;
$this->id = $id;
$this->grade = $grade;
}
// ...
}
之后
class Manager extends Employee {
public function __construct($name, $id, $grade) {
parent::__construct($name, $id);
$this->grade = $grade;
}
// ...
}
之前
class Manager(Employee):
def __init__(self, name, id, grade):
self.name = name
self.id = id
self.grade = grade
# ...
之后
class Manager(Employee):
def __init__(self, name, id, grade):
Employee.__init__(name, id)
self.grade = grade
# ...
之前
class Manager extends Employee {
constructor(name: string, id: string, grade: number) {
this.name = name;
this.id = id;
this.grade = grade;
}
// ...
}
之后
class Manager extends Employee {
constructor(name: string, id: string, grade: number) {
super(name, id);
this.grade = grade;
}
// ...
}
为什么要重构
这个重构技术与提升方法有什么不同?
-
在 Java 中,子类不能继承构造函数,因此你不能简单地将提升方法应用于子类构造函数,并在将所有构造函数代码移到超类后删除它。除了在超类中创建构造函数外,子类中还需要有构造函数,以便简单地委托给超类构造函数。
-
在 C++和 Java 中(如果你没有显式调用超类构造函数),超类构造函数会在子类构造函数之前自动调用,这使得只需要从子类构造函数的开头移动公共代码(因为你不能在子类构造函数的任意位置调用超类构造函数)。
-
在大多数编程语言中,子类构造函数可以有与超类不同的参数列表。因此,你应该仅创建一个真正需要的超类构造函数。
如何重构
-
在超类中创建一个构造函数。
-
将每个子类构造函数开头的公共代码提取到超类构造函数中。在这样做之前,尽量将尽可能多的公共代码移动到构造函数的开头。
-
在子类构造函数的第一行放置对超类构造函数的调用。
</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 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…
提取子类
问题
一个类的特性只在特定情况下使用。
解决方案
创建一个子类并在这些情况下使用它。
之前之后
为什么重构
你的主类有用于实现某个罕见用例的方法和字段。虽然这种情况很罕见,但这个类对此负责,将所有相关字段和方法移动到一个完全独立的类是错误的。然而,它们可以被移动到一个子类,这正是我们将通过这种重构技术实现的。
好处
-
快速简便地创建子类。
-
如果你的主类当前实现多个特殊情况,可以创建几个独立的子类。
缺点
-
尽管看似简单,继承可能会导致死胡同,如果你必须分离几个不同的类层次。例如,如果你有
Dogs
类,其行为根据狗的大小和毛发不同,你可以提炼出两个层次:-
按大小:
Large
、Medium
和Small
-
按毛发:
Smooth
和Shaggy
一切似乎都很好,除了当你需要创建一个既是
Large
又是Smooth
的狗时,问题就会出现,因为你只能从一个类创建对象。也就是说,你可以通过使用组合而不是继承来避免这个问题(见策略模式)。换句话说,Dog
类将有两个组件字段,大小和毛发。你将从必要的类中插入组件对象到这些字段中。因此,你可以创建一个拥有LargeSize
和ShaggyFur
的Dog
。 -
如何重构
-
从感兴趣的类创建一个新的子类。
-
如果你需要额外的数据从子类创建对象,请创建一个构造函数并将必要的参数添加到其中。不要忘记调用构造函数的父类实现。
-
找到对父类构造函数的所有调用。当需要子类的功能时,用子类构造函数替换父构造函数。
-
将必要的方法和字段从父类移动到子类。通过向下推送方法和向下推送字段来完成这一过程。先移动方法通常更简单。这样,字段在整个过程中仍然可以访问:在移动之前来自父类,在移动完成后来自子类。
-
在子类准备好后,找到所有控制功能选择的旧字段。使用多态性删除这些字段,以替代所有使用过这些字段的操作符。一个简单的例子:在 Car 类中,你有字段
isElectricCar
,并且根据这个字段,在refuel()
方法中,汽车要么加油,要么充电。重构后,isElectricCar
字段被移除,Car
和ElectricCar
类将拥有各自的refuel()
方法实现。
</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 小时。
尝试我们的交互式重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看……
合并层次结构
问题
您有一个类层次结构,其中子类几乎与其超类相同。
解决方案
合并子类和超类。
之前!合并层次结构 - 之前之后!合并层次结构 - 之后
为什么要重构
您的程序随着时间的推移而增长,子类和超类几乎变得相同。一个功能从子类中删除,一个方法移到超类……现在您有两个相似的类。
好处
-
程序复杂性降低。更少的类意味着您头脑中需要理清的事物更少,并且在将来代码更改时需要担心的可破坏的活动部分也更少。
-
当方法在一个类中早期定义时,浏览您的代码会更容易。您无需遍历整个层次结构来找到特定方法。
什么时候不使用
-
您正在重构的类层次结构是否有多个子类?如果是这样,重构完成后,剩余的子类应成为合并层次结构的类的继承者。
-
但请记住,这可能导致违反里斯科夫替换原则。例如,如果您的程序模拟城市交通网络,您不小心将
Transport
超类合并到Car
子类中,那么Plane
类可能会成为Car
的继承者。哎呀!
如何重构
-
选择哪个类更容易删除:超类还是其子类。
-
如果您决定去掉子类,请使用提升字段和提升方法。如果您选择删除超类,请使用下推字段和下推方法。
-
将您要删除的类的所有用法替换为字段和方法要迁移到的类。通常这将涉及创建类的代码、变量和参数类型定义,以及代码注释中的文档。
-
删除空类。
</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 个小时。
尝试我们的互动重构课程。它提供了一种更轻松的学习新知识的方法。
我们来看看…
用委托替代继承
问题
您有一个子类只使用超类的一部分方法(或无法继承超类数据)。
解决方案
创建一个字段并放入一个超类对象,委托方法给超类对象,去掉继承。
之前之后
为什么重构
用组合替代继承可以大幅改善类设计,如果:
-
您的子类违反了里氏替换原则,即如果继承只是为了组合公共代码,而不是因为子类是超类的扩展。
-
子类只使用超类的一部分方法。在这种情况下,总会有人调用本不应该调用的超类方法,这只是时间问题。
本质上,这种重构技术将两个类分开,使超类成为子类的助手,而不是其父类。子类将只拥有委托给超类对象的方法,而不是继承所有超类方法。
好处
-
一个类不包含从超类继承的任何不必要的方法。
-
可以将各种不同实现的对象放入委托字段中。实际上,您获得了策略设计模式。
缺点
- 您必须编写许多简单的委托方法。
如何重构
-
在子类中创建一个字段以持有超类。在初始阶段,将当前对象放入其中。
-
更改子类方法,使其使用超类对象而不是
this
。 -
对于从超类继承的方法在客户端代码中被调用的情况,请在子类中创建简单的委托方法。
-
从子类中移除继承声明。
-
通过创建一个新对象来更改存储前超类的字段的初始化代码。
</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 小时。
尝试我们的互动重构课程。它提供了一种不那么乏味的学习新知识的方法。
让我们看看…