软件配置管理(五)常用重构技巧

一、重新组织函数

1.提炼函数

将一段代码组织并独立出来,放进一个独立函数中,并让函数名称解释用途。

2.内联函数

若一个函数的函数体足以解释函数作用,其解释性与函数名称相差无几。那么可以移除函数,将函数体插入函数调用点。

3.内联临时变量

若一个临时变量只被一个简单的表达式赋值一次,且这个变量妨碍了其他的重构手段。那么可以将所有对该变量的引用替换为表达式本身。

4.以查询取代临时变量

若在一个函数中,一个表达式的运算结果被赋值给了一个临时变量,而大量的临时变量会导致理解性变差,而且临时变量无法被其他函数使用。此时可以把表达式独立为一个函数(称为查询式),并将所有临时变量的引用替换为对函数的调用。

5.引入解释性变量

在复杂的表达式中,可以将表达式的中间结果赋给一个临时变量,用变量名解释表达式的用途。

6.分解临时变量

在一段程序中,某个临时变量被多次赋值,但每次赋值的原因都不同(既不是循环变量,也不是用来收集计算结果的变量)。此时可以针对每次赋值都创造一个独立的临时变量,增强解释性。

7.移除对参数的赋值

不要对函数的参数进行赋值,用临时变量取代被重新赋值的参数。

8.以函数对象取代函数

只要将相对独立的代码从大函数提炼出来,就可以大大提高代码的可读性。但局部变量(临时变量)的存在会增加函数分解的难度。如果一个函数中的局部变量泛滥成灾,那么想分解这个函数是非常困难的。“以查询替换临时变量”手法可以帮助减轻负担,但有时候还是会发现根本无法拆解一个需要拆解的函数。这种情况就应该考虑使用函数对象来解决。

在一个大型函数中,临时变量的使用导致无法提炼函数,此时可以将这个函数放进一个单独的对象中,将临时变量作为对象的成员变量,这样就可以将新对象中的大型函数分解。

示例代码

9.替换算法

在一个函数体内,将某个算法替换为另一个算法。

二、在对象之间搬移特性

1.搬移函数

在一个类中的某个函数相较于所在类,其与另一个类有着更加多的联系(依恋情结)。可以在其所依恋的类中建立一个有类似行为的新函数,将旧函数变成一个委托函数,或直接移除。

2.搬移字段

在一个类中的某个字段相较于所在类,其与另一个类有着更加多的联系。可以在其目标类中新建一个字段,并将对旧字段的引用转移到新字段中。

3.提炼类

若一个类不符合单一职责原则,那么可以新建一个类,并将相关的字段和函数转移。

4.将类内联化

若一个类的职责过小,那么可以将这个类中的所有字段和函数转移到另一个类中,并移除原类。

5.隐藏“委托关系”

“封装”意味每个对象都应该尽可能少了解系统的其他部分。

如果某个客户先通过委托类的字段得到另一个对象,然后调用后者的函数,那么客户必须知晓这一层委托关系。万一委托关系发生变化,客户也得相应变化。
可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这么一来,即便将来发生委托关系上的关系,变化也将被限制在服务对象中,不会涉及客户。
示例代码

6.移除中间人

在“隐藏委托关系”中,谈到了“封装受托对象的好处”。但是这层封装是需要付出代价的:每当客户要使用受托类的新特性时,你就必须在服务器端添加一个简单委托函数。但是,随着受托类的特性越来越多,这一过程就会让你变得痛苦。

这时,服务类做了过多的简单委托动作,完全变成了一个“中间人”,应该让客户直接调用受托类。

该手法和“隐藏委托关系”正好相反,正是由于相反,才能够在实际的应用中进行灵活的变通。可能一些委托关系需要保留,而另一些却需要移除,让客户直接使用受托对象。这些都是可以随之变通的。

7.引入外加函数

你正在使用一个类,它真的很好,为你提供了需要的所有服务。
而后,你又需要一项新服务,这个类却无法供应。于是你开始咒骂“为什么不能做这件事?”如果可以修改源码,你便可以自行添加一个新函数;

若无法修改此类的源代码(不完美的库类),此时可以在客户端建立一个函数,并在第一个参数传入服务类的实例 示例代码

适配器模式也可完成这个任务,但会增加一个额外的类。

8.引入本地扩展

我们都无法预知一个类的未来,它们常常无法为你预先准备一些有用的函数。如果可以修改源码,那就太好了,那样就可以直接加入自己需要的函数。但是你经常无法修改源码。如果只是需要一两个函数,可以引入外加函数进行处理。但如果需要多个函数,外加函数就很难控制它们了。

所以,需要将这些额外函数组织起来,放到一个恰当的地方去。要达到这样的目的,需要用到子类化(适配器模式)和包装(装饰器模式)这两种技术。这种情况下,把子类或包装类统称为本地扩展。

三、重新组织数据

1.自封装字段

不要直接访问类中的成员变量(字段),这会增大耦合。为这些字段创造getter和setter方法,且自能用这些方法访问字段。

2.以对象取代数据值

开发初期,我们以简单的数据项表示简单的情况。但是随着开发的进行,你可能会发现,这些简单数据项不再那么简单了。比如说,一开始你可能会用一个字符串来表示“电话号码”概念,但是随后就会发现,电话号码需要“格式化”、“抽取区号”之类的特殊行为。如果这样的数据有一两个,还可以把相关函数放进数据项所属的对象里;但是Duplicate Code(重复代码)坏味道和Feature Envy(依恋情结)坏味道很快就会从代码中散发出来。

当这些坏味道开始出现,就应该将数据值变成对象。

3.将值对象改为引用对象

系统中的对象可以分为引用对象和值对象.
引用对象每个对象代表真实世界的一个实物,你可以直接以“==”检查两个对象是否相等。
值对象像是“钱”、“日期”这样的东西,它们完全由其所含的数据值来定义,你并不在意副本的存在。值对象有一个非常重要的特性,它们应该是不可变的

有时候,你会从一个简单的值对象开始,在其中保存少量的不可修改的数据。而后,你可能会希望给这个对象加入一些可修改数据,并确保对任何一个对象的修改都能影响到所有引用此一对象的地方,这时候,你就希望将这个对象变为一个引用对象。

4.将引用对象改为值对象

若一个引用对象很小,且确定其是不可变的,那么可以将其变为值对象,方便管理。

5.以对象取代数组

若一个数组中的各个元素代表不同的东西,那么将这个数组替换为对象,使用成员变量标识数组的不同元素。

6.复制“被监视的数据”

一个分层良好的系统,应该将处理用户界面(GUI)和处理业务逻辑(Business Logic)的代码分开。

若一些领域数据存在GUI里,而业务逻辑函数需要访问这些数据。此时可以将这些数据复制到一个领域对象中,并用观察者模式同步领域对象和GUI对象的重复数据。

7.将单向关联改为双向关联

若两个类都需要使用对方的属性,但其中只有一条单向连接。此时可以添加一个方向指针,并使修改双方关系的函数同时更新这两条连接。

8.将双向关联改为单向关联

在拥有双向关联的两个类间,当其中一个类不再需要另一个类的特性使,可以去除不必要的关联。

9.以字面常量取代魔法数

若一个字面常量带有特殊含义,此时可以创造一个常量,赋值为字面常量,并使用有意义的变量名。

10.封装字段

将public字段声明为private并提供访问函数。

11.封装集合

在函数返回集合时,让函数返回这个集合的只读副本,并提供修改集合元素的函数。
示例代码

12.以数据类取代记录

记录型结构是许多编程环境的共同性质。有一些理由使它们被带进面向对象程序之中:你可能面对的是一个遗留程序,也可能需要通过一个传统API来与记录结构交流,或是处理从数据库读出的记录。这些时候你就有必要创建一个接口类,用以处理这些外来数据。最简单的做法就是先建立一个看起来类似外部记录的类,以便日后将某些字段和函数搬移到这个类中。

新建一个类(“哑”数据对象),表示这个记录。对应记录中的每一项数据,在新建的类中建立对应的一个private字段。并提供相应的取值/设值函数。

13.以类取代类型🐎

若一个类有一个不影响类行为的类型码,可以以一个新的类替换这个数值类型码。

在使用Replace Type Code with Class (以类取代类型码)之前,你应该先考虑类型码的其他替换方式。
只有当类型码是纯粹数据时(也就是类型码不会在switch语句中引起行为变化时),你才能以类来取代它。
更重要的是:任何switch语句都应该运用Replace Conditional with Polymorphism (以多态取代条件表达式)去掉。
为了进行那样的重构,你首先必须运用 Replace Type Code with Subclass (以子类取代类型码)或Replace Type Code with State/Strategy (以状态策略取代类型码),把类型码处理掉。

14.以子类取代类型🐎

若一个类有一个会影响类行为的类型码,可以以一个子类取代这个类型码。
示例代码

15.以State(状态模式)/Strategy(策略模式)取代类型🐎

若一个类有一个会影响行为的类型码,但无法通过继承消除它(类型码的值在对象生命期中发生变化或其他原因使得宿主类不能被继承),此时可以用状态对象或具体策略对象取代类型码。

16.以字段取代子类

若一个类的各个子类间唯一的差别只在“返回常量数据”的函数身上,此时可以修改这些函数,是它们返回超类的某个(新增)字段,然后销毁子类。

建立子类的目的是为了增加新特性或变化父类行为,如果其带来的影响只是返回的数据值不同,就没有存在的必要

四、简化条件表达式

1.分解条件表达式

若一个条件表达式过于复杂,可以将if-then-else三个段落分别提炼独立的函数。

2.合并条件表达式

若在并列的条件表达式中,它们的then语句有相同操作,可以用逻辑与和逻辑或语句将各条件中的if语句联立,并将then语句合并。

合并后的条件代码会告诉你“实际上只有一次条件检查,只不过有多个并列条件需要检查而已”,从而使这一次检查的用意更清晰。
其次,这项重构往往可以为你使用“提取方法”做好准备。

3.合并重复的条件片段

若在条件表达式的每个分支上有相同的代码,此时可以将这段重复代码搬移到条件表达式之外。

4.移除控制标记

若在一系列布尔表达式中,某个变量带有控制标记的作用,可以用break、continue和return语句取代控制标记。

5.以卫语句取代嵌套条件表达式

条件表达式通常有2种表现形式。第一:所有分支都属于正常行为。第二:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况。
这2类条件表达式有不同的用途。如果2条分支都是正常行为,就应该使用形如if……else……的条件表达式;

如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。

给某个分支以特别的重视。它告诉阅读者:这种情况很罕见,如果它真的发生了,请做一些必要的整理工作,然后退出。

6.以多态取代条件表达式

若一个条件表达式根据对象类型的不同而选择不同的行为,此时应将条件表达式的每个分支拆分为一个子类内的override函数中,然后将原始函数声明为抽象函数。

7.引入Null对象

将null值替换为null对象,这样就不用在每一个引用处判断对象是否为空。
示例代码

8.引入断言

为一段需要对程序状态做出某种假设的代码添加断言,以明确表现这种假设。

五、 简化函数调用

1.函数改名

修改那些不能明确揭示函数用途的函数名称。

2.添加参数

但某个函数需要从调用端获得更多信息时,为这个函数添加一个对象参数,让该对象带进函数所需信息。

除了添加参数外,你常常还有其他选择。只要可能,其他选择都比添加参数要好,因为它们不会增加参数列的长度。过长的参数列是不好的味道,因为程序员很难记住那么多参数而且长参数列往往伴随着坏味道:数据泥团(Data Clumps)。

3.移除参数

移除函数体不再用到的参数。

4.将查询函数和修改函数分离

将既返回对象状态值,又能修改对象状态的函数分离为两个函数,一个负责查询,一个负责修改。

5.令函数携带参数

若两个任务类似的函数,只因少数几个值使行为略为不同,这种情况下,可以建立单一的函数,并通过参数来处理那些变化,用以简化问题,去除重复代码,并提高灵活性。

6.以明确函数取代参数

若一个函数完全取决于参数值而采取不同的行为,可以针对参数的每一个可能值建立一个单独的函数。

7.保持对象完整

若函数参数是某个对象中的若干字段,此时可以改为传递整个对象。

8.以函数取代参数

如果函数可以通过其他途径获得参数值,那么它就不应该通过参数取得该值。

如果对象调用了某个函数,并将这个函数的返回值作为参数传递给另一个参数,但后者其实可以直接调用前者,这时应让后者在函数体中直接调用前者,并获取前者返回值,而不需要使用参数传递。

9.引入参数对象

若某些参数总是同时出现,则可以以一个对象取代这些参数。

10.移除设值函数

若类中某个字段只在对象被创建时被设值,之后不再改变,则应去掉该字段的setter函数

11.隐藏函数

如果一个函数从来没有被其他类用到,那么应将这个函数修改为privete。

12.以工厂函数取代构造函数。

如果创建对象时不仅仅要做简单的构建动作,那么应将构造函数替换为工厂函数。

13.封装向下转型

向下转型也许是无法避免的,但你仍然应该尽可能少做。如果你的某个函数返回一个值,并且你知道所返回的对象类型比函数签名所昭告的更特化,你便是在函数用户身上强加了非必要的工作。这种情况下你不应该要求用户承担向下转型的责任,应该尽量为他们提供准确的类型。
以上所说的情况,常会在返回迭代器或集合的函数身上发生。此时你就应该观察人们拿这个迭代器干什么用,然后针对性地提供专用函数。

将向下转型的动作转移到函数体中,而不是让用户完成这个动作。

14.以异常取代错误码

对于一个成员方法而言,它要么执行成功,要么执行失败。成员方法执行成功的情况很容易理解,但如果执行失败了却没有那么简单,因为我们需要将执行失败的原因通知调用者。抛出异常和返回错误代码都是用来通知调用者的手段
当我们想要告示调用者更多细节的时候,就需要与调用者约定更多的错误代码。于是。错误代码飞速膨胀,直到看起来似乎无法维护,因为我们需要查找并确认错误代码。
使用了CLR异常机制后,代码更加清晰,易于理解。
而在某些情况下,错误代码将无用武之地,如构造函数、操作符重载及属性。语法的特性决定了其不具备任何返回值,于是异常处理被当做取代错误代码的首要选择。

将函数返回错误码改为使用异常。
示例代码

15.以测试取代异常

异常只应该被用于异常的,罕见的行为,也就是那些产生意料之外错误的行为,而不应该成为条件检查的替代品

如果调用者有能力在异常被抛出之前检查抛出条件,那么应该让调用者在调用函数前检查条件。
示例代码

六、处理概括关系

1.字段上移

当两个子类拥有相同字段时,将该字段移至超类。

2.函数上移

当函数在各个子类产生完全相同的结果时,将该函数移至超类。

3.构造函数本体上移

当各个子类中的构造函数几乎完全移至时,在超类新建一个构造函数,并在子类构造函数中调用它。

4.函数下移

当超类的某个函数只与部分子类有关时,将这个函数移到相关子类中。

5.字段下移

当超类中的某个字段只被部分子类用到时,将这个字段移到需要它的子类中。

6.提炼子类

当类中某些特性只被部分实例用到时,新建一个子类,将那部分特性移到子类中。

Extract Class(提炼类)是Extract Subclass之外的另一种选择,两者之间的抉择其实就是委托和继承之间的抉择。Extract Subclass通常更容易进行,但它也有限制:一旦对象创建完成,你无法再改变与类型相关的行为。此外,子类只能用以表现一组变化。如果你希望一个类以几种不同的方式变化,就必须使用委托。

7.提炼超类

当两个类有相似特性时,为这两个类建立一个超类,将相同特性移至超类中。

8.提炼接口

如果若干用户使用类中相同的特殊方法,或两个类的方法有部分相同,可以将相同方法提炼到一个接口中,并让这些类实现接口。
实例代码

9.折叠继承体系

如果超类和子类没有太大区别,那么将他们合为一体。

10.塑造模板函数。

如果一些子类中相应的某些函数以相同顺序执行类似的操作,但操作细节在不同子类间有所不同。这时可以将子类中对应函数修改为相同的函数名,并在超类中创建对应的被重写的函数,最后在超类中创建一个新的函数,按顺序执行这些函数完成操作。
实例代码

11.以委托取代继承

如果某个子类只是用了超类的一部分接口,或者没有使用超类的数据,那么应去除继承关系,转而在子类中新建一个持有超类的字段,并调整子类函数,改为委托超类。
代码示例

12.以继承取代委托

如果在两个类中使用委托关系会导致编写许多极简单的委托函数,这时应考虑让委托类继承受托类。

七、大型重构

1.梳理并分解继承体系

如果某个继承体系同时承担两项责任,这时应建立两个继承体系,并通过委托关系使其中一个调用另一个。

2.将过程化设计转化为对象设计

针对以传统过程化分隔编写的代码,将它的数据记录变成对象,将大块行为分成小块并移入相关对象中。

3.将领域和表述/显示分离

如果某些GUI类包含了领域逻辑,那么应将领域逻辑分离出来,建立单独的领域类。

4.提炼继承体系

如果某个类做了太多的、以大量条件表达式完成的工作,那么应建立继承体系,以一个子类表示其中的一种特殊情况。


软件配置管理(五)常用重构技巧
https://buttering.github.io/EasyBlog/2021/06/15/软件配置管理(五)常用重构技巧/
作者
Buttering
发布于
2021年6月15日
许可协议