關(guān)于重構(gòu)
重構(gòu)是一種改善已有代碼和設(shè)計(jì)的有效手段,Martin Fowler的著作
Refactoring:Improving the Design of Existing Code一書里提出了若干種重構(gòu)的模式,深刻地影響了眾多的開發(fā)人員。但如果認(rèn)為重構(gòu)只能做到小范圍的代碼優(yōu)化,或者設(shè)計(jì)優(yōu)化,并視之為無(wú)法影響更高層面工作的雕蟲小技,那就大錯(cuò)特錯(cuò)了。之后 Joshua Kerievsky 的著作
Refactoring to Patterns則創(chuàng)造性地把重構(gòu)和模式聯(lián)系在一起,讓人們認(rèn)識(shí)到重構(gòu)的巨大威力。重構(gòu)從來(lái)不是程序員躲在象牙塔孤芳自賞的技術(shù),也可以對(duì)系統(tǒng)的設(shè)計(jì)開發(fā)發(fā)揮巨大的作用。
如果說(shuō)Martin的
Refactoring只是深刻地影響了普通開發(fā)人員的程序設(shè)計(jì)和代碼編寫,Joshua的
Refactoring to Patterns則切切實(shí)實(shí)地給架構(gòu)設(shè)計(jì)人員或者tech lead的工作指出了革命性的變化。那么,對(duì)于項(xiàng)目的功能開發(fā),重構(gòu)又意味著什么呢?對(duì)于項(xiàng)目經(jīng)理來(lái)講,應(yīng)用重構(gòu)的技術(shù)和思想,對(duì)整個(gè)項(xiàng)目的功能開發(fā)是否能帶來(lái)特別的好處?下面將通過(guò)一個(gè)例子給大家展示在開發(fā)新功能時(shí),對(duì)開發(fā)的每一步都保持重構(gòu)的思想,將整體功能的開發(fā)分解成若干步驟的“重構(gòu)”,從而非常簡(jiǎn)易清晰地完成功能開發(fā)。
新功能的提出
某系統(tǒng)中存在 ReferenceFile 類,作為用戶向系統(tǒng)上傳的文件的抽象,是系統(tǒng)其他地方會(huì)使用到的參考文檔。在最初的需求中,參考文檔與用戶上傳的文件一一對(duì)應(yīng),并且用戶能指定某些系統(tǒng)特定的屬性,比如文檔類別、文檔管理部門等。于是在最初的設(shè)計(jì)中,該類的屬性包含兩部分:一部分是前面提到的系統(tǒng)屬性;另一部分是描述文件信息的屬性,比如文件名、文件存儲(chǔ)路徑等。請(qǐng)注意,這里我將“參考文檔”與“上傳文件”兩個(gè)概念區(qū)分出來(lái),是為了便于下文解釋。總的來(lái)說(shuō),在這個(gè)階段,“參考文檔”就是系統(tǒng)對(duì)“上傳文件”的抽象。
接到這個(gè)需求之后,我們使用TDD,很快就驅(qū)動(dòng)設(shè)計(jì)出該類的業(yè)務(wù)方法;再使用Acceptance TDD,又對(duì)該類的功能的進(jìn)行了全面的覆蓋;最后使用hibernate的O/R Mapping,按照屬性和表字段一對(duì)一的關(guān)系,把該類和數(shù)據(jù)庫(kù)的表關(guān)聯(lián)起來(lái)。完成UI方面的設(shè)計(jì),并把前后臺(tái)整合在一起。系統(tǒng)上線試運(yùn)行后用戶認(rèn)為這塊很好地契合了需求。
但是,需求總是不斷在變的。上線過(guò)程中,用戶的上級(jí)部門提出參考文檔應(yīng)該可以對(duì)應(yīng)到多個(gè)上傳文件,系統(tǒng)其他地方使用時(shí)把其下所有上傳的文件作為一個(gè)“參考文檔”整體來(lái)對(duì)待。也就是說(shuō),對(duì) ReferenceFile 類而言,其中的系統(tǒng)屬性仍然是保持一份,但是上傳文件的屬性則變成多份。概括下來(lái),客戶提出的新需求如下:
1. 參考文檔可以管理多個(gè)上傳文件
2. 用戶創(chuàng)建或者修改參考文檔時(shí),可以同時(shí)上傳多個(gè)文件,并能對(duì)已上傳的文件進(jìn)行刪除修改
3. 系統(tǒng)在其他地方仍然是針對(duì)參考文檔來(lái)參考引用用戶上傳的文件
4. 參考文檔的預(yù)覽和展示需要調(diào)整成支持多個(gè)上傳文件
實(shí)現(xiàn)過(guò)程
該系統(tǒng)是標(biāo)準(zhǔn)的j2ee web 分層系統(tǒng),包括web UI、controller、service、domain model、dao這幾層。本文的重點(diǎn)是如何應(yīng)用重構(gòu)開發(fā)功能,本文將著重關(guān)注于domain層的改動(dòng),會(huì)包括domain model API的改動(dòng),以及domain model 持久化機(jī)制的改動(dòng)。其他層次,比如controller、service等,因?yàn)橹饕亲鳛閐omain model的消費(fèi)者,主要是使用domain model 的public API,故放在一起作為整體來(lái)對(duì)待,下文將統(tǒng)一稱為client 代碼。至于最外層的 web UI層,則因?yàn)橹饕歉鶕?jù)系統(tǒng)功能提供交互上的操作和內(nèi)容展現(xiàn),而且大部分情況下也會(huì)有專門UI設(shè)計(jì)開發(fā),本文就不涉及了。
另外,系統(tǒng)還包括大量不同層次的測(cè)試代碼,比如unit test、functional test、integration test和regression test等等。從另外一個(gè)角度,測(cè)試代碼又可以分成2部分:text fixture和test case。test fixture主要是負(fù)責(zé)測(cè)試數(shù)據(jù)的準(zhǔn)備,test case才是測(cè)試用例的實(shí)現(xiàn)代碼。前面提到的測(cè)試,除了unit test之外都主要是基于 web UI 模擬用戶使用系統(tǒng)功能,test case 主要是針對(duì) web UI 來(lái)寫,故對(duì)于這部分的測(cè)試而言,domain model 的修改主要會(huì)影響到測(cè)試數(shù)據(jù)的準(zhǔn)備。而對(duì)于 unit test,又可以根據(jù)SUT的不同,分為幾個(gè)部分:針對(duì)model的unit test、針對(duì)client(包括controller和service)的unit test。其中,針對(duì)model的unit test也只是model API的消費(fèi)者,也可以視為domain model的client。針對(duì)controller和service的unit test,理論上也只針對(duì)于SUT的API,對(duì)model的API依賴也只是在test fixture那塊。所以,根據(jù)我們的分析,我們知道測(cè)試代碼可以簡(jiǎn)化成兩部分,一部分是與controller/service類似的domain model的client,另一部分是使用domain model生成一組aggregation的test fixture。
綜上所述,我們把整個(gè)功能實(shí)現(xiàn)過(guò)程中涉及的工作主要?dú)w類為:domain model API的改動(dòng)、domain model持久化機(jī)制的修改、domain model client的修改,以及test fixture的修改。現(xiàn)在對(duì)于需要做什么事情,就變得清晰了。我們接下來(lái)對(duì)前面三項(xiàng)工作來(lái)分析。
面臨的現(xiàn)狀
仔細(xì)分析我們面臨的情況:
1. 文件的相關(guān)信息在原始的 ReferenceFile 類里面是作為一對(duì)一的屬性組存在
2. ReferenceFile 類使用 Hibernate 進(jìn)行屬性字段一對(duì)一的持久化
3. ReferenceFile 類以及原功能有 unit test、dao test,以及functional test 覆蓋
此時(shí)的 ReferenceFile 類是這樣的:
public class ReferenceFile {
private String category;
private String fileName;
//相應(yīng)的 getter/setter,以及業(yè)務(wù)方法
}
ReferenceFile 類的hibernate映射文件是這樣的:
<class name="ReferenceFile" table="referenceFiles">
<id/>
<property name="category"/>
<property name="fileName"/>
//
</class>
回頭看看在這次功能調(diào)整中,我們需要做哪幾項(xiàng)任務(wù)?其中會(huì)涉及哪些方面?
-
domain model 的修改
-
domain model 持久化機(jī)制的修改
-
domain model 增加一對(duì)多的關(guān)系
抽取新類
domain model 的修改
很明顯,隨著需求的變化,作為一組時(shí)時(shí)刻刻同時(shí)出現(xiàn)而且內(nèi)聚性非常強(qiáng)的屬性,原來(lái)記錄文件相關(guān)信息的屬性組,比如文件名、上傳路徑以及類型等等,以及操作這些屬性的方法需要抽取到一個(gè)單獨(dú)的類里面。Martin Fowler 在Refactoring:Improving the Design of Existing Code里面寫到“...consider where it can be split...A good sign is that a subste of the data and a subset of the methods seem to go together.”因此,我們決定把這些屬性組和方法抽取到一個(gè)新類。新的類的職責(zé)變成維護(hù)上傳文件的相關(guān)信息,而 ReferenceFile 則化身為一組上傳文件的集合,不用操心文件的存儲(chǔ)和具體細(xì)節(jié),更利于系統(tǒng)其他地方進(jìn)行引用。
那我們?cè)撊绾芜M(jìn)行演化呢?這里我們可以使用 Martin Fowler在Refactoring書中的“Extract Class”技巧。請(qǐng)大家自行參閱,就不具體講了。經(jīng)過(guò)這一步,我們現(xiàn)在可以得到這樣一個(gè)結(jié)構(gòu):ReferenceFile has an Attachment。 這兩個(gè)類的代碼大概如下:
public class ReferenceFile {
private String category;
private Attachment attachment;
//相應(yīng)的 getter/setter,以及業(yè)務(wù)方法
}
public class Attachnment {
private String fileName;
//相應(yīng)的 getter/setter,以及業(yè)務(wù)方法
}
domain model 持久化機(jī)制的修改
接下來(lái),我們需要修改 ReferenceFile 的持久化機(jī)制。在原始的設(shè)計(jì)里面,ReferenceFile類的屬性一一對(duì)應(yīng)到數(shù)據(jù)庫(kù)表中的字段。現(xiàn)在屬性被分到了兩個(gè)對(duì)象里面,為了 Hibernate依舊能把這些屬性都持久化到一張數(shù)據(jù)庫(kù)表里面,我們使用了 Hibernate 提供的 component配置。下面是改動(dòng)后的配置:
<class name="ReferenceFile" table="referenceFiles">
<id/>
<property name="category"/>
<component class="Attachment">
<property name="fileName"/>
//
</component>
</class>
運(yùn)行測(cè)試,OK,所有的測(cè)試都pass了。至此,我們抽取新類的步驟就完成了。接下來(lái),我們需要完成“一對(duì)多”的演化。
公開新類
domain model 的修改
在這里面,我們需要將 ReferenceFile 類里面的 Attachment 類公布出來(lái),直接在client code里面使用這個(gè)類。這樣,原本屬于 Attachment 類的方法就能徹底地從 ReferenceFile 類里面移走,ReferenceFile類只留下必要的業(yè)務(wù)方法和 Attachment 對(duì)象的getter/setter。Martin Fowler在Refactoring:Improving the Design of Existing Code里提到“move methods”,我們采用這種技巧,很容易地把原來(lái)與Attachment類相關(guān)的業(yè)務(wù)方法都移到Attachment類里面,ReferenceFile類里面只保留對(duì)attachment屬性的getter/setter方法。公布Attachment對(duì)象之后的結(jié)構(gòu):
public class ReferenceFile {
private String category;
private Attachment attachment;
//相應(yīng)的 getter/setter,以及業(yè)務(wù)方法
}
public class Attachnment {
private Long id;
private String fileName;
//相應(yīng)的 getter/setter,以及業(yè)務(wù)方法
}
domain model 持久化機(jī)制的修改
這里,我們就考慮把Attachment單獨(dú)持久化到自己的數(shù)據(jù)庫(kù)表里面了。原來(lái)的component就變成了現(xiàn)在一對(duì)一關(guān)聯(lián)。改動(dòng)后的配置如下:
<class name="ReferenceFile" table="referenceFiles">
<id/>
<property name="category"/>
<one-to-one name="attachment" class="Attachment"/>
//
</class>
<class name="Attachment" table="attachments">
<id/>
<property name="fileName"/>
</class>
實(shí)現(xiàn)類之間的一對(duì)多聯(lián)系
domain model 的修改
到這里,讀者就能發(fā)現(xiàn)這是一種Kent Beck曾經(jīng)總結(jié)過(guò)的“First One, Then Many”情況。關(guān)于“First One,Then Many”,Kent Beck曾寫了一篇文章介紹如何可靠地?fù)肀ё兓逆溄尤缦耯ttp://www.threeriversinstitute.org /FirstOneThenMany.html。在那篇文章中,Kent的問題是面對(duì)未來(lái)可能的需求變化,如何使用 Succession 的方式幫助系統(tǒng)架構(gòu)平滑演化。下面是 Kent 的觀點(diǎn):
Applied to software design, succession refers to creating a design in stages. The first stage is not where you expect to end up, but it provides value. You have to pay the price to progress from stage to stage, but if jumping to the final stage is prohibitively expensive in time or money, or if you don't know enough to design the "final" stage, or if (as postulated above) the customers don't know enough to specify the system that would use the final stage, then succession provides a disciplined, reasonably efficient way forward.
那么,在本文的功能開發(fā)之中,我們是如何做到的?
- 增加字段attachments,以及getter/setter
- 修改原來(lái)單個(gè)Attachment的getter/setter,改成從attachments里面得到首元素或者往里面添加新元素,如getAttachments().get(0)
- 運(yùn)行測(cè)試,確保所有測(cè)試都通過(guò)
- inline ReferenceFile類里面的對(duì)單個(gè)Attachment的getter/setter方法。這里要注意test fixture里面對(duì)domain model的aggregation的創(chuàng)建,而且因?yàn)樯婕皩?duì)List的操作,所以可能需要修改原來(lái)的測(cè)試代碼和test fixture
- 運(yùn)行測(cè)試,確保所有測(cè)試都通過(guò)
到這里,“一對(duì)多”的工作完成之后,ReferenceFile 和 Attachment 類就變成了下文的樣子:
public class ReferenceFile {
private String category;
private List<Attachment> attachments;
//相應(yīng)的 getter/setter,以及業(yè)務(wù)方法
}
public class Attachnment {
private Long id;
private String fileName;
//相應(yīng)的 getter/setter,以及業(yè)務(wù)方法
}
domain model 持久化機(jī)制的修改
為了能實(shí)現(xiàn)一對(duì)多的實(shí)體關(guān)系,我們需要引入新的表作為“多”方,并保持“一”方的主鍵。使用Hibernate提供的one-to-many很容易做到這點(diǎn),接下來(lái)是簡(jiǎn)單的配置文件:
<class name="ReferenceFile" table="referenceFiles">
<property name="category"/>
<set name="attachments" cascade="all" >
<key column="id"/>
<one-to-many class="Attachment"/>
</set>
//
</class>
<class name="Attachment" table="attachments">
<property name="fileName"/>
</class>
結(jié)論
至此,我們就完成了新功能的開發(fā),可以看出整個(gè)過(guò)程的思路非常明顯,而且因?yàn)橹饕茄刂貥?gòu)的思想一路下來(lái),思路非常清晰。另外,因?yàn)橹貥?gòu)已經(jīng)有成熟的IDE支持,我們可以利用到IDE的很多便利,這從另一方面也給我們帶來(lái)了非常的效率。
從整個(gè)過(guò)程來(lái)看,重構(gòu)的一些方法和思想,不僅可以讓我們對(duì)遺留代碼進(jìn)行優(yōu)化,使之能有利于新功能的開發(fā)(比如本文中的抽取新類和公開新類,都是為了下文的“由一到多”的功能開發(fā)),而且可以讓我們?cè)陂_發(fā)功能的時(shí)候能從一個(gè)更高的角度來(lái)分解功能的開發(fā)工作,從而把原本復(fù)雜無(wú)序的過(guò)程簡(jiǎn)化抽象成一段明確的重構(gòu)鏈。那么,重構(gòu)是否就是開發(fā)人員開發(fā)軟件的領(lǐng)域?qū)僬Z(yǔ)言呢(refactoring as DSLs to developers' development)?敬請(qǐng)期待本博關(guān)于這點(diǎn)的其他博文。