您的位置:首页 > 其它

重构-tips

2016-01-27 20:59 204 查看
一、重构第一步:为即将修改的代码建立一组可靠的测试环境(靠人工测试不如靠测试用例靠谱)。

二、函数应该放在它所使用的数据的所属对象内。

三、函数中部分变量(比如比较重要的计算结果),可以使用get,set将其设为成员变量来促成较简洁的设计

四、如果一个对象的值可能随时随其对象类型发生变化,对象类型应可以动态改变,那么可以将此对象类型设为B。

类A中包含一个基类B的成员对象b,基类B中包含一个抽象函数,基类B的子函数B1,B2,B3继承基类B之后就可以根据各自类型设置值。如此一来类A可以通过一个set函数动态改变b的具体类型,再通过b来获取值,(达到随时获取设置类型后的对象b的值的目的)。这样,无论为基类B添加新的派生类,或者改变派生类的具体实现,类A中获取值部分的代码都无需改动。

五、如果需要改变已发布的接口,可以让旧接口调用新接口,当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万 不要复制函数实现,那样会使你陷入重复代码的泥淖中难以自拔。你还应该使用Java提供的deprecation机制,将旧接口标识为deprecated。

六、除非真有必要,否则不要发布接口。

七、如果你没有足够的时间(天天加班),通常就表示你其实早就该进行重构。事先做好设计可以节省返工的高昂成本(确实)

八、实际测量验证,不要臆测

==================================================================================================================

常见的需要进行重构的代码特征

==================================================================================================================

Duplicated code

Long Method

如果你的函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用Extract Method,最终就会把许多参数和零食变量当做参数,传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,你可以运用Replace Temp with Query来消除这些临时元素。Introduce parameter Object he preserve Whole Object则可以将过长的参数变得简洁一点。

如果一段代码需要用注释来说明问题,通常也是在提醒你,这段代码可以提炼到一个函数中。(说明这段代码与上下不是一个最小逻辑单元)

条件表达式和循环常常也是提炼的信号。

Large Class

Divergent Change

针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。(比如外界改了定价标准,那么获取价格,以及基于定价标准的相应业务逻辑应该要保证都自动使用新定价标准)

Shotgun Surgery

避免遇到某种变化时,必须要在许多不同类中做出许多小修改。

Feature Envy

避免函数对某个类的兴趣高过对自己所处类的兴趣

Switch Statements

面向对象程序的一个最明显特征就是:少用switch或case 语句,从本质上来讲,switch语句的问题在于重复。

Parallel Inheritance Hierarchies(平行继承体系)

如果当你为某一个类增加一个子类,必须也为另一个类相应增加一个子类,那么便是出现了平行继承体系的情况。消除这种重复性的一般策略是:让一个继承体系的实例引用另一个继承体系的实例。

Lazy class

对于几乎没用的组件,你该以Inline Class来处理

Speculative Generality

不要过度设计。

Temporary Field

如果某个实例变量仅为某种特定情况而设,那么请使用Extract Class为这个变量创建一个家,然后把所有和这个变量相关的代码都放进这个新家。

Message Chains(过度耦合的消息链)

如果用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象。。。这就是消息链。这意味着一旦对象间的关系发生任何变化,客户端就不得不做出相应的修改。这时你该使用Hide Delegate。

Middle Man(中间人)

如果看到某个类接口有一半的函数都委托给其他类,这就是过度委托。这时应该使用Remove Middle Man,直接和真正负责的对象打交道。如果这样不干实事的函数只有少数几个,可以运用Inline method把它们放进调用端。如果这些Middle Man还有其他行为,可以运用Replace Delegation with Inheritance把它变成实责对象的子类,这样你既可以拓展原对象的行为,又不必负担那么多的委托动作。

Inappropriate Intimacy

继承往往造成过度亲密,如果觉得该让某个子类独立了,请用Replace Inheritance with Delegation让它离开继承体系。

编写高性能软件的方法:

1.分解组件时给每个组件指定的预算,由每个组件将自己的性能保持在预算内来保证整个软件的整体性能。

2.先编写构造良好有一定性能保证的程序,之后再来特别关注性能问题(比较好的方式)

确保所有测试都完全自动化,让他们检查自己的测试结果

频繁进行测试是极限编程的重要一环

如果你想重构就必须编写测试代码

JUnit测试框架

重构列表

重构记录格式(name+summary+motivation+mechanics+examples)---->用于记录重构方法,便于查阅

如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来向一系列的注释。

注意恰当的函数命名,最好以”做什么“命名,而不是以它“怎样做”命名。

==================================================================================================================

常用重构方法

==================================================================================================================

Extract Method(提炼函数)

#如果无局部变量->直接抽离到新函数中,并在原位置插入对新函数的调用即可。

#如果有局部变量,但不修改这个局部变量->可将局部变量作为参数传递到新函数中。

#如果有局部变量,且只在抽离代码中修改,并且修改了之后还不被其他代码使用->那直接在新函数修改就可以

#如果有局部变量,且在抽离代码中修改,并且修改了之后还在其他地方使用->那么就需要让目标函数返回变量改变之后的值

如果需要修改并返回的临时变量过多,可以先尝试减少临时变量,然后考虑抽离成多个函数(这样可以尽量保持每个函数只返回一个值)

Inline Method(内联函数)

但非必要的间接性总是不好(过度解耦)

需要被子类继承的函数不要用内联(inline函數表示該函數是内聯的,它建議編譯器在調用該函數的地方直接將函數的代碼展開來

插入caller的代碼中。因此不具有动态性)

Inline Temp(内联临时变量)

如果内联临时变量妨碍了重构(通常是只被赋值一次的临时变量),那么可以将对该变量的引用动作,替换为对它赋值的那个表达式。

Replace Temp with Query(以查询取代临时变量)

常见于用临时变量来保存一些列结果,这时可以设计一个大数据结构来保存结果,然后用函数查询的方式来避免直接对临时变量的操作。可以使代码更为清晰。(这个非常常用,而且能很大程度上帮助写出结构明晰的程序)

Introduce Explaining variable(引入解释性变量)

将复杂表达式的结果用一个临时变量来表示,可以用来解释这个表达式的作用。

Split Temporary Variable(分解临时变量)

防止用同一个临时变量来接受不同意义表达式的结果(不要太抠门了,一个临时变量用到底,这样不便于程序的阅读)

Remove Assingnments to Parameters(移除对参数的赋值)

java按值传递,因此,可以修改参数对象的内部状态,但对参数对象重新赋值是没有意义的。

Replace Method with Method Object(以函数对象取代函数)

遇到过这种问题(很大的函数中有很多临时变量,也就是大胖球函数),需要后续实践。

==================================================================================================================

在对象间搬移特性

==================================================================================================================

如果函数与其所驻类之外的另一个类进行更多的交流,那么最好在后者中建立一个有着类似行为的新函数,将旧函数变为一个委托函数。

并且如果后者还需要调用前者的函数或者使用到前者的多个特性,那么可以将前者的对象作为参数传给后者。

构建好一个类之后,要考虑是否有必要将此类公开(这可能会带来意料之外的访问和修改)

Hide Delegate(隐藏委托)

在服务类上建立客户所需的所有函数,用以隐藏委托关系。

如果某个客户先通过服务对象的字段得到另一个对象,然后调用后者的函数,那么客户就必须知晓这一层的委托关系。万一委托关系发生变化,客户也得相应变化。

你可以在服务对象上放置一个简单的委托函数,将委托关系隐藏起来,从而去除这种依赖。这样一来,即便将来发生委托关系上的变化,变化也将被限制在服务对象中,不会波及客户。

Remove Middle Man(移除中间人)

是和隐藏委托相反的一个过程,减少不必要的简单委托。

所以合适的隐藏程度是软件工程师需要拿捏的一个尺度。

Introduce Local Extension(引入本地扩展)

建立一个新类,使他包含这些额外函数,让这个扩展品成为源泪的子类或者包装类。(其实就是建立子类和包装类)

使用本地扩展使得你得以坚持“函数和数据应该被统一封装”的原则。如果你一直吧本该放在扩展类中的代码零散地放置于其他类中,最终只会让其他类变得过分复杂。

但制作子类的最大障碍在于,它必须在对象创建期实施,还必须产生一个子类对象。这种情况下,如果有其他对象引用了旧对象,我们就同时又两个对象保存了原数据!如果原数据是不可修改的还好,如果可修改,那么同一个修改动作就无法保证两份副本的同时更新。这个时候我们就必须使用包装类。使用包装类时,对本地扩展的修改会波及到子类。(在什么情况下应该用包装类)

其中转型构造函数是指“接受原对象作为参数”的构造函数。

Replace Data Value with Object(以对象取代数据值)

如果你有一个数据项,需要与其他数据行为一起使用才有意义。那么可以将数据项变成对象。

Change Value to Reference(将值对象改为引用对象)

首先了解引用对象和值对象的区别,引用对象的相等表示二者是同一个对象,而值对象的相等只能表示二者的值相等,不一定是同一个对象。

很多工厂函数中都采用在工厂类中返回已经创建好的引用对象的方式来处理应该全局唯一的对象,避免冗余创建。

但引用对象必须被某种方式控制,你总是必须向其控制者请求适当的引用对象。它们可能造成内存区域之间错综复杂的关联。在分布式系统和并发系统中,不可变的值对象特别有用,因为你无需考虑它们的同步问题。

这里最主要是要理解引用对象和值对象的概念。

Replace Array with Object(以对象取代数组)

用对象取代数组,对于数组中的每一个元素,以一个字段来表示。

好处在于,可以运用字段名称和函数名称来标识数组中元素表示什么意思。

Dumplicate Observed Data (复制“被监视数据”)

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

观察者模式(这里需要再复习,理解不是很透彻)

Replace Magic Number with symbolic Constant(以字面常量取代魔法数)

老生常谈了,避免魔法数

Encapsulate Field(封装字段)

将public字段改为private字段,并提供访问函数。(数据声明为public被看做是一种不好的做法,因为这样会降低程序的模块化程度)

Encapsulate Collection(封装集合)

我们常常会在一个类中使用集合来保存一组实例。这样的类通常也会提供针对该集合的取值/设值函数。但是取值函数不该返回集合自身,并且不应该为这整个集合提供一个设值函数,而是提供添加/移除元素的函数。这样集合拥有者就可以控制集合元素的添加和移除。

如果类型码不会影响宿主类的行为且不会用到switch来进行判别,那么可以用类取代类型码。

如果类型码会影响宿主的行为(即不同类型有不同的处理),那么可以考虑借助多态来处理变化行为(Replace Conditional with Polymorphism)。

在借助多态之前,需要利用Replace Type code with Subclass以类型码的宿主类为基类,针对每种类型码建立相应的子类。

而如果类型码值在对象创建之后发生了变化或者类型码宿主类已经有了子类,就需要使用Replace Type code with State/Strategy(利用到State模式)。

面向对象程序中应尽可能减少switch和if-else语句,可以尝试提炼出独立函数来表意(表面这个条件分支的具体作用)。也可以将两层的if嵌套用与逻辑合并。

如果某个条件极其罕见,就应该单独检查条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为guard clauses.(这样可以降低条件分支的复杂度,常见的是一开始就检测一些异常情况直接用return函数返回)

用多态或者状态机替换条件表达式

如果你手上有个条件表达式,它根据对象类型的不同而选择不同的行为,那么可以将这个表达式的每个分支放进一个子类内的覆写函数中,然后将原始函数声明为抽象函数。

要建立继承结构,一般直接建立子类(Replace type code with subclass),但如果你需要在对象创建好之后修改类型码,就不能使用继承,只能使用State/Strategy模式。

用空对象解决空检测问题

空对象一定是一个常量,可以用Singleton模式来实现他们。

1.可以建立一个A的子类A1使其支持"对象是否为null"的检查或者建立一个Nullable接口,使A实现此接口或者给A建立一个工厂类,在工厂类中处理null检测。

2.之后将null检测替换成重构后的方式,如:

if(customer == null){

customer.getName();...

}....

换成

if(customer.isNull()){

customer.getName();...

}....

但这不是重点,重点是在Customer类提供了Null判断接口后可以在get函数里进行对Null的处理

(空对象这个要再看,因为描述不是很清楚)

==============================

简化函数调用

并发编程往往需要较长的参数列,因为这样你可以保证传递给函数的参数都是不可被修改的,例如内置型对象和值对象一定是不可变的。

明确地将“修改对象状态”的函数(修改函数)和“查询对象的函数”(查询函数)分开设计(Separate Query from Modifier)是个韩浩德习惯。

构造函数是Java和C++中特别麻烦的一个东西,因为它强迫你必须知道要创建的对象属于哪一个类,而你往往不需要知道这一点。你可以使用Replace Constructor with Factory Method避免了解这些信息。(就是用工厂函数)

记住应该用Encapsulate Downcast将向下转型封装起来。

代码首先是为人写的,其次才是为机器写的。

关于发现原有接口实现由问题的时候,如何改变原有接口的实现,在Android系统源码中可以参考有depecated标记的接口,看替换这些接口的函数是怎么被这个废弃接口调用从而保证两套接口都同时可用的(很长时间过去,旧版本影响很小的时候再去完全删除旧接口,可以保证使用旧接口的应用仍然可以运行)

在多线程系统中工作,将查询动作和修改动作分开来仍然是很有价值的。但你需要保留第三个函数来同时做这件事。这个“查询-修改”函数将调用各自独立的查询函数和修改函数,并被声明为“synchronized”。如果查询函数和修改函数未被声明为synchronized,那么你还应该将它们的可见范围限制在包级别或private级别。这样,你就可以拥有一个安全、同步的操作,它由两个较易理解的函数组成。这两个较低层函数也可以用于其他场合。

如果你为某个字段提供了设置函数,这就暗示着这个字段值可以被改变。如果你不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数(同时将该字段设为final)。这样你的意图会更加清晰,并且可以排除其值被修改的可能性。所以在构造函数中直接初始化变量是更为恰当的方式。(这个建议很有用,以前一直不确定该何种情况下提供设值函数)。

如果一个函数不需要被其他类用到,应该将这个函数设置为private

如果你希望在构建对象时不仅仅是做简单的建构动作那么可以将构造函数替换为工厂函数。工厂函数最常用的场合是派生子类的过程中以工厂函数取代类型码。(老生长谈了)

使用非受控异常首先就表示应该由调用者负责检查。异常只应该用于异常的、罕见的行为,也就是那些产生意料之外的错误的行为,而不应该成为条件检查的替代品。

Replace Inheritance with Delegation

使用Pull Up Method将两个子类中都有的函数methodA放到父类的时候,如果methodA调用的另一个函数methodB1和methodB2在两个子类中的实现不一样,那么应该先将methodB1和methodB2抽象出一个抽象函数放到父类中,然后再将methodA Pull up到父类中。

Pull Up Constructor Body 你在各个子类中拥有一个构造函数,它们的本体几乎完全一致。那么可以在超类中新建一个构造函数,并在子类构造函数中调用它。(这个建议很有用,简化构造函数部分)

超类中的某个函数或字段只与部分(而非全部)子类有关。那么应该将这个函数或字段移到相关的子类中去。反过来,如果类中的某些特性只被某些(而非全部)实例用到。那么应该新建一个子类,将上面所说的那一部分特性移到子类中。(其实类可以当集合来理解)

若干客户使用类接口中的同一子集,或者两个类的接口有部分相同。那么就应该将相同的子集提炼到一个独立接口中。(这句话概括和描述得很好理解)

Extract Interface 可能造成难闻的“重复”坏味道,幸而你可以运用Extract Class先把共通行为放进一个组件中,然后将工作委托该组件,从而解决这个问题。如果有不少共通行为,Extract Superclass会比较简单,但是每个类只能有一个超类。(这句话的第一句还不理解,需要回头再看)

如果你想要描述一个类的外部依赖接口(outbound interface,即这个类要求服务提供方提供的操作)。如果你打算将来加入其他种类的服务对象,只需要求它们实现这个接口即可。(就是说,我定义好应该做1,2,3,如果你加入进来,那么你就需要做1,2,3,但你怎么做就是你根据自己的实际情况来做具体实现了)

你有一些子类,其中相应的某些函数以相同的顺序执行类似的操作,但各个操作的细节上有所不同。。那么可以将这些操作分别放进独立函数中,并保持它们都有相同的签名,于是原函数也就变得相同了。然后将原函数上移至超类。(这里的签名其实就是指函数名,翻译真是让人容易晕)。使用模板将不同的细节部分抽象成抽象函数放到父类中之后,如果有其他类似的子类加入进来,就只需要继承这些抽象出来的接口就是,也便于程序的拓展。

Replace Inheritance with Delegation(以委托取代继承)

如果某个子类只使用超类接口中的一部分,或是根本不需要继承而来的数据。那么在子类中新建一个字段用以保存超类;调整子类函数,令它改而委托超类;然后去掉两者之间的继承关系。(如果你继承了一个类,然后发现超类中的许多操作并不真正适用于子类。这种情况下,你所拥有的接口并未真正反映出子类的功能。或者你可能发现你从超类中集成了一大堆子类并不需要的数据,抑或你可能发现超类中的某些protected函数对子类并没有什么意义。)这样,代码传达的信息就与你的意图不一致了。

如果以委托取代继承,你可以更清楚地表明:你只需要受拖类的一部分功能。接口中的哪一部分应该被使用,哪一部分应该被忽略。这样做的额外成本是需要额外写出委托函数,但这些函数都非常简单,极少可能出错。

做法是:在委托类中新建一个字段保存受托类的对象。然后对自己需要的那一部分功能创建相应函数,在这些函数中调用受拖类的相应功能函数。

Replace Delegation with Inheritance(以继承取代委托)

于前面以委托取代继承的情况相反,如果需要用到全部功能,并且不会影响多个子类数据共享,那么久可以以继承取代委托。

注意:如果受托对象被不止一个其他对象共享,而且受托对象是可变的。在这种情况下,就不能将委托关系替换为继承关系,因为这样就无法再共享数据了。数据共享是必须由委托关系承担的一种责任,你无法把他转给继承关系。如果受托对象是不可变的,数据共享就不成为题,因为你大可放心地复制对象,谁都不会知道。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: