重构笔记
最近刚看完了《重构-改善既有代码的设计》([美] Martin Fowler)这本书,里面介绍了很多的重构手法,
挺实用的,看的时候简单的记录了一下,不是很详细,还是分享一下吧。
1)提炼函数(Extract Method)
使用场景 |
1、函数过长 2、全部或某一段代码块重复性太强,copy的过多 |
好处 |
1、颗粒度小,整体被复用的机会大,即不需要再多次重复内部逻辑 2、规范一个好的命名(命名长度可以很长,但一定要更好的解释此段的语义), 可以更加清晰此块代码的实际作用 |
实际运用 |
1、不需要传参,即直接提取函数段,并放入一个新的函数中即可。 2、需要传参: 1)按值传递(单值 / 多值)【JAVA规范】 单值问题上直接传递即可。 若参数值过多,可以将其封装成一个临时对象(内部类)来传递。 注意:值传递时,不要直接对参数赋值,将其当做被final修饰过 的值来处理 。 做法: (1) 将待处理的参数赋值给一个临时变量。 (2) 中间的处理部分也以指定的临时来进行运作。 (3) 在有必要时需要返回这个临时变量。 (移除对参数的赋值【Remove Assignments to Parameters】) 2)按引用传递 |
备注 |
|
2)内联函数(Inline Method)
使用场景 |
1、一般为源函数的函数体很短,很清晰易懂,且不被重复调用 |
好处 |
1、其语义本身就已经很清晰了,没必要添加成新的函数 |
实际运用 |
1、直接将源函数的函数体放入目标函数的对应位置即可 |
备注 |
|
3)以函数对象取代函数(Replace Method with Method Object)
使用场景 |
1、一般为长函数中,局部变量过多,而无法很好的提炼成小函数 |
好处 |
1、将该函数存放至函数对象中之后,局部变量可转换成对象的字段来处理, 在字段的处理上整体会更加清晰一些,面向对象的概念性更强。 |
实际运用 |
1、新建一个类,类名的语义符合待处理函数的用途。 2、新建一个final修饰的字段,用来保存源对象(即源函数所处的那个对象), 并将源函数中的变量 + 参数都设置成该函数对象的字段。 3、为该函数对象新建一个构造函数,接受源对象 + 源函数本身的所有参数作为参数。 4、新建一个compute()函数,并将源函数中的函数体移植到此函数中。 5、在源函数的函数体中调用该函数对象的compute()即可。 |
备注 |
|
4)以查询取代临时变量(Replace Temp with Query)
使用场景 |
1、一般为函数体中的临时变量(为另一个函数所函数的值)基本上只使用1次 |
好处 |
1、减少临时变量的使用,让代码更加清晰一些 |
实际运用 |
1、直接使用另一个函数来进行操作,而不是先使用临时变量来接收返回值 |
备注 |
|
5)分解临时变量(Split Temporary Variable)
使用场景 |
1、某个临时变量被使用超过一个,且即不是循环变量,也不用于收集计算结果 |
好处 |
1、对临时变量进行分解后,可对函数体进行其他形式的重构,而不必受限于临 时变量在该函数体中的变化 |
实际运用 |
1、针对每一次赋值,创建一个独立、对应的临时变量 |
备注 |
|
6)将查询函数与修改函数分离(separate Query from Modifier)
使用场景 |
1、某个函数即返回对象的状态值,又修改该对象状态 |
好处 |
1、符合单一原则,不会在执行此函数的时候出现副作用 |
实际运用 |
1、建立两个函数,一个查询,一个修改 |
备注 |
在并发问题上的考量: 在高并发的时候,不仅分离成两个不同的函数,还需要保留第三个函数来 同时执行这两个函数,并将这两个函数分别声明为synchronized,且限制 他们在包(protected)级别 或 private级别 |
7)引入解释性变量(Introduce Explaining Variable)
使用场景 |
1、复杂的表达式 |
好处 |
1、让表达式清晰 |
实际运用 |
1、将该复杂表达式(或其中一部分)的结果放进一个临时变量,以此便令名称来解释表达式的意图 |
备注 |
也可使用提炼函数的方法来处理 |
8)替换算法(Substitute Algorithm)
使用场景 |
1、将某段复杂的逻辑转换成清晰的表达式 |
好处 |
1、理解清晰 |
实际运用 |
需要更具具体的业务逻辑来处理。 如有些部分可以使用guava / JDK8 等工具来处理会使语义更加明确 |
备注 |
也可使用提炼函数的方法来处理 |
9)搬移函数(Move Method)
使用场景 |
1、在程序执行中,某个函数与其所驻类之外的另一个类有更多交流,调用后者,或被后者调用。 |
好处 |
1、降低相关类之间的耦合度 |
实际运用 |
通俗的来说就是: 在该函数最常引用的类中建立一个有着类似行为的新函数。将旧函数变成一个单纯的委托函数, 或是直接将旧函数移除掉。
具体做法: 1)检查源类中被源函数所使用的一切特性(字段 或 函数),是否也可一起被搬移, 有时候搬移一组函数比搬移单个要更好。 2)检查源类的子类或父类中是否存在对该函数的其他声明。 3)在目标类中声明这个函数。 4)将源函数的代码复制到目标函数中,调整目标函数。 (1)目标函数引用了源函数的一些特性,且不能被一起搬移,则 可以将源类的引用作为参数传递至目标函数中,或新创建函 数来取得目标对象的这些特性。 (2)若源函数中有异常的处理,需要考虑是否需要将异常带到目 标函数中处理还是继续留在原地。 5)修改源函数 或 将源函数移除掉: 若需要经常在源函数中引用目标函数,则尽量保留源函数。 |
备注 |
|
10)自封装字段(Self Encapsulate Field)
使用场景 |
1、若某个字段提供直接访问方式,在函数中或许会因为过多的来调用而增加耦合度, 将其变得笨拙。 |
好处 |
1、面向对象概念更加清晰 |
实际运用 |
1、直接在该类中将该字段设置为private,并提供get/set方法 |
备注 |
|
11)搬移字段(Move Field)
使用场景 |
1、在程序执行中,某个字段与其所驻类之外的另一个类有更多交流 |
好处 |
1、降低相关类之间的耦合度 |
实际运用 |
通俗的来说就是: 在目标类中新建此字段,用新字段替换掉所有引用了源字段函数中的源字段部分。 需要注意的是: 1)如何在源类中获取到目标字段,可在源类中添加一个目标类的引用。 |
备注 |
|
12)提炼类(Extract Class)
使用场景 |
1、一个类做了本应该由两个类来做的事 |
好处 |
1、抽象更加明确,满足单一原则 |
实际运用 |
1、建立一个新类,将相关函数和字段搬移至新类中 2、如有必要,可添加新类与源类之间的对象引用 |
备注 |
|
13)将类内联化(Inline Class)
使用场景 |
1、两个相关类都没有处理过多的事,可将其合并为一个类 |
好处 |
1、清理掉萎缩类,让结构变得干净 |
实际运用 |
1、建立一个新类,分别将两个类中的函数和字段搬移至新类中 |
备注 |
|
14)隐藏“委托关系”(Hide Delegate)
使用场景 |
1、客户通过委托类来调用另一个对象 |
好处 |
1、隐藏其中的细节,避免客户知道其实现逻辑 |
实际运用 |
在服务类上建立客户所需要的所有函数,用以隐藏委托关系 |
备注 |
例如: 类A、B、C 需求:A不能直接获取C信息,A能直接获取B信息,B能直接获取C信息 处理:在B中委托一个函数来获取C信息,则A可直接调用B中的函数来间 接获取了C信息。 |
15)移除中间人(委托函数)(Remove Midden Man)
使用场景 |
1、某个类包含了太多的委托函数 |
好处 |
1、让客户直接调用委托类,清晰 |
实际运用 |
客户直接调用委托类,不必走中间过程 |
备注 |
这和Hide Delegate并不冲突,当委托函数只有一两个时可使用隐藏委托关系, 但多了就没必要使用中间过程了,这里面有一个度需要控制。 |
16)引入外加函数(Introduce Foreign Method)
使用场景 |
1、当需要为一个服务类增加一个函数时,但又修改不了服务类,一般在源码类中会遇到, 不能修改源代码,只能对其进行扩展,或重写特定函数 |
好处 |
1、根据业务需求扩展源类 |
实际运用 |
在客户类中建立一个新函数,并将服务类作为参数进行传递,且函数的返回值也为该服务类。 |
备注 |
外加函数只是权宜之计,在权限的允许下可将其加入服务类中 |
17)引入本地扩展【子类 + 包装类】(Introduce Local Extension)
使用场景 |
1、同引入外加函数的使用场景一致,但不同的是当外加函数过多时即使用此重构方法 |
好处 |
1、在本地新建一个类来包含这些额外函数,让此类作为源类的子类或包装类 |
实际运用 |
1、建立一个扩展类,作为源类的子类或包装类。 2、在扩展类中加入转型构造函数: 即“接收源对象作为参数”的构造函数。 1)子类化方案: 转型构造函数需要调用父类的构造函数。 2)包装类方案:(不是太明白) 转型构造函数需要将其得到的传入参数以实例变量的形式保存下 来,用作委托对象的原对象。 3、将外加函数搬移至此类中 |
备注 |
|
18)以对象取代数据值(Replace Data Value with Object)
使用场景 |
1、某个类中需要使用到另一个类的部分信息 如:在用户类User中需要知道用户所阅读的书籍信息,若需要详细了解书籍信息 可以直接将书籍类Book的引用作为User的一个字段。 |
好处 |
1、可查看更多信息,后续扩展Book类时,User类也不需要修改 |
实际运用 |
将Book的引用作为User的一个字段 |
备注 |
|
19)以工厂函数取代构造函数(Replace Constructor with Factory Method)
使用场景 |
1、你希望在创建对象时不仅仅是做简单的建构动作 |
好处 |
1、将“对象创建请求的接收者”和“被创建对象所属的类”分开了, 如:Date date = DateFactory.create();
对象创建请求的接收者:原为Date,现为DateFactory.create 被创建对象所属的类:原Date类 |
实际运用 |
1、将构造函数替换为工厂函数 private Book(String name){ this.name = checkNull(name); }
public Book create(String name){ return new Book(name); //原构造函数设为私有的 } |
备注 |
1、当此工厂函数创建的子类只有少数几个时,可使用明确函数来创建子类 即在工厂类中添加类似如下创建函数: public Book createBook(String name){ return new Book(name); } 2、当需要创建的子类数量多的时候,可以使用字符串类型的类型码 + 反 射机制来控制: static Book create(String name){ try{ return (Book) Class.forName(name).newInstance(); } catch(Exception e){} }
//部分代码 switch(name){ case XXX: return create(“XXX”); …… default: throws new Exception(“xxxxxxx”); }
可使用类(枚举类也可以)取代类型码 |
20)将对象替代数组(Replace Array with Object)
使用场景 |
1、有一个数组,但数组中每个元素又代表着不同的东西 |
好处 |
1、对象化,让所存储的每一个元素都能清晰的表达自己 |
实际运用 |
以对象替换数组,数组中的每一个元素,用字段来表示 |
备注 |
|
21)将单向关联改为双向关联
使用场景 |
1、两个类都需要使用对方的特性,但其中只有一条单向连接 |
好处 |
1、可以双向连接 |
实际运用 |
添加一个反向指针,并使修改函数能够同时更新两条连接 |
备注 |
例如订单类 + 客户类(一个客户可以有多个不同的订单) public class Order{ private Customer _customer; public void setCustomer(Customer customer){ if(this._customer != null) this.customer.getOrders().remove(this); this._customer = customer; if(this._customer != null) this._customer.getOrders().add(this); } public Customer getCustomer(){ return this._customer; } }
public class Customer{ private Set orders = Sets.newHashSet(); public Set getOrders(){ return this.orders; } } |
22)以字面常量取代魔法数(Replace Magic Number with Symbolic Constant)
使用场景 |
1、有一个带有特别含义的字面数值 |
好处 |
1、修改、查找时都会方便 |
实际运用 |
创造一个静态常量,命名很重要,其值则为那个字面数值 |
备注 |
|
23)封装字段(Encapsulate Field)
使用场景 |
1、将Public定义的字段变成private的 |
好处 |
1、对象化字段 |
实际运用 |
将字段改成private的修饰符,再提供其get/set函数 |
备注 |
自封装字段和这一个概念 |
24)封装集合(Encapsulate Collection)
使用场景 |
1、函数返回一个集合 |
好处 |
1、避免直接去操作所返回的那个集合 |
实际运用 |
新建一个集合用作所返回集合的一个只读副本,并在这个类中提供增加/移除集合元素的函数。 List list = service.findAll(); List res = Collections.unmodifiableList(list); 之后的操作都在只读集合res中操作,避免破坏原list |
备注 |
Collections.unmodifiableList(list); //得到集合的只读副本 |
25)以子类取代类型码(Replace Type Code with Subclasses)
使用场景 |
1、有一个不可变的类型码,但它会影响宿主类的行为 |
好处 |
|
实际运用 |
以子类取代这个类型码。 1、使用自封装字段方式将类型码封装起来,如果类型码为构造函数的参 数,则需要将构造函数换成工厂函数。 2、为类型码的每个数值建立一个对应的子类,并复写父类获取类型码的函 数,返回对应的类型码 |
备注 |
此方法仍不够好,仍需要switch表达式的介入,下面介绍的的重构手法将完全去除掉switch语句 |
26)以多态取代条件表达式(Replace Conditional with Polymorphism)
使用场景 |
1、有一个条件表达式,需要根据对象类型的不同而选择不同的行为 |
好处 |
简化了条件表达式 |
实际运用 |
将这个条件表达式的每个分支放进一个子类的覆写函数中,然后将原始函数声明为抽象函数。 |
备注 |
|
27)以字段取代子类(Replace Subclass with Fields)
使用场景 |
1、当各个子类的唯一差别仅是“返回常量数据”不同时可使用 |
好处 |
较少子类 |
实际运用 |
1、在所有子类中通过工厂函数的方式重构其构造函数 2、其他直接引用了子类的地方,都改换成引用超类 3、针对子类中的每一个常量函数,在超类中对应声明一个final字段 4、在超类中声明一个protected构造函数,用来初始化新增的字段 5、使用内联函数方法将子类构造函数内联到超类的工厂函数中 6、再在超类中实现所有的常量函数,且返回对应的字段 7、删除子类 |
备注 |
|
28)分解条件表达式(Decompose Conditional)
使用场景 |
1、if、then、else条件语句过多 |
好处 |
结构清晰 |
实际运用 |
将if、then、else段落中分别提炼出独立的函数 |
备注 |
|
29)合并条件表达式(Consolidate Conditional Expression)
使用场景 |
1、有一系列的条件测试,但都得到相同的结果 |
好处 |
结构清晰 |
实际运用 |
将这个测试合并为一个条件表达式,并将这个条件表达式提炼成一个函数 |
备注 |
三目运算符在重构方面使用也挺好的 |
29)合并重复的条件片段(Consolidate Duplicate Conditional Fragments)
使用场景 |
1、在条件表达式的每个分支上有着相同的一段代码 |
好处 |
去重 |
实际运用 |
将重复代码搬移到条件表达式之外 |
备注 |
|
30)移除控制标记(Remove Control Flag)
使用场景 |
1、在一系列布尔表达式中,某个变量带有“控制标记”的作用 |
好处 |
当使用控制标记时,其他使用者不能一下看明白这个标记的意思, 因此使用此重构方式来简化代码结构 |
实际运用 |
找出对标记变量赋值的语句,适当的使用break、continue或return来取代控制标记 |
备注 |
|
31)使用卫语句来取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
使用场景 |
1、函数中的条件逻辑太多使人难以看清正常的执行路径 |
好处 |
结构清晰 |
实际运用 |
对于每个检查,放进一个卫语句(卫语句要不就从函数中返回,要不就抛出一个异常) |
备注 |
卫语句: 如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。 这样的单独检查就是“卫语句”。 |
32)以多态取代条件表达式(Replace Conditional with Polymorphism)
使用场景 |
1、以多态取代条件表达式 |
好处 |
减少switch、if、else等语句的出现,更重要的是多态可降低系统各部分之间的依赖, 在碰到新的修改时,不必直接去修改条件表达式,而是以新增子类的形式来完善即可 |
实际运用 |
将这个条件表达式的每个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数 |
备注 |
|
33)函数重命名(Rename Method)
使用场景 |
1、函数的命名不能直接看出函数的用途 |
好处 |
看其名,知其意 |
实际运用 |
为该删除命一个好的名字。 1、先为这段函数写一段注释 2、根据注释来为其命名 |
备注 |
|
34)添加参数(Add Parameter)
使用场景 |
1、某个函数需要从嗲用端得到更多的信息 |
好处 |
|
实际运用 |
若函数参数过多,可自己封装一个参数对象来进行传递,或使用现成的对象来进行传递 |
备注 |
既然有添加参数,那当然也有移除参数,和此重构相反,就不多说了 |
35)将查询函数和修改函数分离(Separate Query from Modifier)
使用场景 |
1、某个函数既返回对象状态值,又修改对象状态 |
好处 |
减小因某次修改会产生副作用的概率 |
实际运用 |
建立两个不同的函数,其中一个负责查询,另一个负责修改 |
备注 |
|
36)令函数携带参数(Parameterize Method)
使用场景 |
1、若干个函数做了类似的工作,但在函数本体中却包含了不同的值 |
好处 |
简化重复代码 |
实际运用 |
建立单一函数,以参数表达那些不同的值 |
备注 |
将少量数值看做参数,来找出有重复性的代码 |
37)以明确函数取代参数(Replace Parameter with Explicit Methods)
使用场景 |
1、存在一个函数,其中完全取决于参数值而采取不同行为 |
好处 |
|
实际运用 |
针对该参数的每一个可能的值,建立一个独立的函数 |
备注 |
|
38)保持对象完整(Preserve Whole Object)
使用场景 |
1、从对象中取出若干值来当做参数进行传递 |
好处 |
|
实际运用 |
直接传递整个对象 |
备注 |
和引入参数对象(Introduce Parameter Object)重构方式一样 |
39)以函数替代临时参数(Replace Parameter with Methods)
使用场景 |
1、对象调用某个函数,并将所得结果作为参数,传递给另一个函数。 而接受该函数的函数本身也能够调用前一个函数。 |
好处 |
减少临时变量的使用 |
实际运用 |
让参数接受者去除该项参数,并直接使用前一个函数 |
备注 |
也要分情况来考虑: 当原函数返回的值在当前函数的很多地方都会使用时,可使用变量先接住原函数的返回值。 当其他地方使用很少时并且函数的执行不会消耗提多资源时,可直接使用函数来替代此参数。 |
40)移除设值函数(Remove Setting Method)
使用场景 |
1、类中的某个字段应该在对象创建时被设值,然后就不再改变 |
好处 |
|
实际运用 |
去掉该字段的所有设值函数,在构造函数中进行赋值 |
备注 |
|
41)隐藏函数(Hide Method)
使用场景 |
1、未使用的函数 |
好处 |
|
实际运用 |
使用private来进行隐藏,降低函数的可见度 |
备注 |
|
42)以异常取代错误码(Replace Error Code with Exception)
使用场景 |
1、某个函数返回一个特定的代码,用来表示某种错误情况 |
好处 |
|
实际运用 |
改为返回异常 |
备注 |
|
43、字段上移
44、函数上移
45、字段下移
46、函数下移
47)构造函数本体上移(Pull Up Constructor Body)
使用场景 |
1、在多个子类的构造函数中,其本体几乎都一致 |
好处 |
|
实际运用 |
在超类中新建一个构造函数,并在子类构造函数中调用它 |
备注 |
|
48、提炼子类
49、提炼父类
50、提炼接口
51)塑造模板函数(Form Template Method)
使用场景 |
1、在一些子类中,其中相应的某些函数以相同的顺序执行类似的操作, 但各个操作的细节上又有所不同 |
好处 |
|
实际运用 |
将这些操作分别放进独立的函数中,并保持他们都有相同的签名, 于是原函数也就变得相同了,然后将原函数上移至超类中。 说白了,就是将不同的地方通过分解/封装转换成相同的部分,再将相同之处提炼至父类中。 |
备注 |
分解目标函数时需要注意 |
52)以委托取代继承(Replace Inheritance with Delegation)
使用场景 |
1、某个子类只能使用超类接口中的一部分,或是根本不需要继承而来的数据 |
好处 |
|
实际运用 |
在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉继承。 1、在子类中新增一个字段,并new一个超类的实例 2、在有调用超类函数的地方修改为使用新增的字段来调用 |
备注 |
同时,也有以继承取代委托的重构方法,当然了,其用法也正好相反 |
最近也在看秦小波的《设计模式之禅》,写的也挺好的