關于重構
重構是一種改善已有代碼和設計的有效手段,Martin Fowler的著作
Refactoring:Improving the Design of Existing Code一書里提出了若干種重構的模式,深刻地影響了眾多的開發(fā)人員。但如果認為重構只能做到小范圍的代碼優(yōu)化,或者設計優(yōu)化,并視之為無法影響更高層面工作的雕蟲小技,那就大錯特錯了。之后 Joshua Kerievsky 的著作
Refactoring to Patterns則創(chuàng)造性地把重構和模式聯系在一起,讓人們認識到重構的巨大威力。重構從來不是程序員躲在象牙塔孤芳自賞的技術,也可以對系統(tǒng)的設計開發(fā)發(fā)揮巨大的作用。
如果說Martin的
Refactoring只是深刻地影響了普通開發(fā)人員的程序設計和代碼編寫,Joshua的
Refactoring to Patterns則切切實實地給架構設計人員或者tech lead的工作指出了革命性的變化。那么,對于項目的功能開發(fā),重構又意味著什么呢?對于項目經理來講,應用重構的技術和思想,對整個項目的功能開發(fā)是否能帶來特別的好處?下面將通過一個例子給大家展示在開發(fā)新功能時,對開發(fā)的每一步都保持重構的思想,將整體功能的開發(fā)分解成若干步驟的“重構”,從而非常簡易清晰地完成功能開發(fā)。
新功能的提出
某系統(tǒng)中存在 ReferenceFile 類,作為用戶向系統(tǒng)上傳的文件的抽象,是系統(tǒng)其他地方會使用到的參考文檔。在最初的需求中,參考文檔與用戶上傳的文件一一對應,并且用戶能指定某些系統(tǒng)特定的屬性,比如文檔類別、文檔管理部門等。于是在最初的設計中,該類的屬性包含兩部分:一部分是前面提到的系統(tǒng)屬性;另一部分是描述文件信息的屬性,比如文件名、文件存儲路徑等。請注意,這里我將“參考文檔”與“上傳文件”兩個概念區(qū)分出來,是為了便于下文解釋??偟膩碚f,在這個階段,“參考文檔”就是系統(tǒng)對“上傳文件”的抽象。
接到這個需求之后,我們使用TDD,很快就驅動設計出該類的業(yè)務方法;再使用Acceptance TDD,又對該類的功能的進行了全面的覆蓋;最后使用hibernate的O/R Mapping,按照屬性和表字段一對一的關系,把該類和數據庫的表關聯起來。完成UI方面的設計,并把前后臺整合在一起。系統(tǒng)上線試運行后用戶認為這塊很好地契合了需求。
但是,需求總是不斷在變的。上線過程中,用戶的上級部門提出參考文檔應該可以對應到多個上傳文件,系統(tǒng)其他地方使用時把其下所有上傳的文件作為一個“參考文檔”整體來對待。也就是說,對 ReferenceFile 類而言,其中的系統(tǒng)屬性仍然是保持一份,但是上傳文件的屬性則變成多份。概括下來,客戶提出的新需求如下:
1. 參考文檔可以管理多個上傳文件
2. 用戶創(chuàng)建或者修改參考文檔時,可以同時上傳多個文件,并能對已上傳的文件進行刪除修改
3. 系統(tǒng)在其他地方仍然是針對參考文檔來參考引用用戶上傳的文件
4. 參考文檔的預覽和展示需要調整成支持多個上傳文件
實現過程
該系統(tǒng)是標準的j2ee web 分層系統(tǒng),包括web UI、controller、service、domain model、dao這幾層。本文的重點是如何應用重構開發(fā)功能,本文將著重關注于domain層的改動,會包括domain model API的改動,以及domain model 持久化機制的改動。其他層次,比如controller、service等,因為主要是作為domain model的消費者,主要是使用domain model 的public API,故放在一起作為整體來對待,下文將統(tǒng)一稱為client 代碼。至于最外層的 web UI層,則因為主要是根據系統(tǒng)功能提供交互上的操作和內容展現,而且大部分情況下也會有專門UI設計開發(fā),本文就不涉及了。
另外,系統(tǒng)還包括大量不同層次的測試代碼,比如unit test、functional test、integration test和regression test等等。從另外一個角度,測試代碼又可以分成2部分:text fixture和test case。test fixture主要是負責測試數據的準備,test case才是測試用例的實現代碼。前面提到的測試,除了unit test之外都主要是基于 web UI 模擬用戶使用系統(tǒng)功能,test case 主要是針對 web UI 來寫,故對于這部分的測試而言,domain model 的修改主要會影響到測試數據的準備。而對于 unit test,又可以根據SUT的不同,分為幾個部分:針對model的unit test、針對client(包括controller和service)的unit test。其中,針對model的unit test也只是model API的消費者,也可以視為domain model的client。針對controller和service的unit test,理論上也只針對于SUT的API,對model的API依賴也只是在test fixture那塊。所以,根據我們的分析,我們知道測試代碼可以簡化成兩部分,一部分是與controller/service類似的domain model的client,另一部分是使用domain model生成一組aggregation的test fixture。
綜上所述,我們把整個功能實現過程中涉及的工作主要歸類為:domain model API的改動、domain model持久化機制的修改、domain model client的修改,以及test fixture的修改?,F在對于需要做什么事情,就變得清晰了。我們接下來對前面三項工作來分析。
面臨的現狀
仔細分析我們面臨的情況:
1. 文件的相關信息在原始的 ReferenceFile 類里面是作為一對一的屬性組存在
2. ReferenceFile 類使用 Hibernate 進行屬性字段一對一的持久化
3. ReferenceFile 類以及原功能有 unit test、dao test,以及functional test 覆蓋
此時的 ReferenceFile 類是這樣的:
public class ReferenceFile {
private String category;
private String fileName;
//相應的 getter/setter,以及業(yè)務方法
}
ReferenceFile 類的hibernate映射文件是這樣的:
<class name="ReferenceFile" table="referenceFiles">
<id/>
<property name="category"/>
<property name="fileName"/>
//
</class>
回頭看看在這次功能調整中,我們需要做哪幾項任務?其中會涉及哪些方面?
-
domain model 的修改
-
domain model 持久化機制的修改
-
domain model 增加一對多的關系
抽取新類
domain model 的修改
很明顯,隨著需求的變化,作為一組時時刻刻同時出現而且內聚性非常強的屬性,原來記錄文件相關信息的屬性組,比如文件名、上傳路徑以及類型等等,以及操作這些屬性的方法需要抽取到一個單獨的類里面。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.”因此,我們決定把這些屬性組和方法抽取到一個新類。新的類的職責變成維護上傳文件的相關信息,而 ReferenceFile 則化身為一組上傳文件的集合,不用操心文件的存儲和具體細節(jié),更利于系統(tǒng)其他地方進行引用。
那我們該如何進行演化呢?這里我們可以使用 Martin Fowler在Refactoring書中的“Extract Class”技巧。請大家自行參閱,就不具體講了。經過這一步,我們現在可以得到這樣一個結構:ReferenceFile has an Attachment。 這兩個類的代碼大概如下:
public class ReferenceFile {
private String category;
private Attachment attachment;
//相應的 getter/setter,以及業(yè)務方法
}
public class Attachnment {
private String fileName;
//相應的 getter/setter,以及業(yè)務方法
}
domain model 持久化機制的修改
接下來,我們需要修改 ReferenceFile 的持久化機制。在原始的設計里面,ReferenceFile類的屬性一一對應到數據庫表中的字段?,F在屬性被分到了兩個對象里面,為了 Hibernate依舊能把這些屬性都持久化到一張數據庫表里面,我們使用了 Hibernate 提供的 component配置。下面是改動后的配置:
<class name="ReferenceFile" table="referenceFiles">
<id/>
<property name="category"/>
<component class="Attachment">
<property name="fileName"/>
//
</component>
</class>
運行測試,OK,所有的測試都pass了。至此,我們抽取新類的步驟就完成了。接下來,我們需要完成“一對多”的演化。
公開新類
domain model 的修改
在這里面,我們需要將 ReferenceFile 類里面的 Attachment 類公布出來,直接在client code里面使用這個類。這樣,原本屬于 Attachment 類的方法就能徹底地從 ReferenceFile 類里面移走,ReferenceFile類只留下必要的業(yè)務方法和 Attachment 對象的getter/setter。Martin Fowler在Refactoring:Improving the Design of Existing Code里提到“move methods”,我們采用這種技巧,很容易地把原來與Attachment類相關的業(yè)務方法都移到Attachment類里面,ReferenceFile類里面只保留對attachment屬性的getter/setter方法。公布Attachment對象之后的結構:
public class ReferenceFile {
private String category;
private Attachment attachment;
//相應的 getter/setter,以及業(yè)務方法
}
public class Attachnment {
private Long id;
private String fileName;
//相應的 getter/setter,以及業(yè)務方法
}
domain model 持久化機制的修改
這里,我們就考慮把Attachment單獨持久化到自己的數據庫表里面了。原來的component就變成了現在一對一關聯。改動后的配置如下:
<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>
實現類之間的一對多聯系
domain model 的修改
到這里,讀者就能發(fā)現這是一種Kent Beck曾經總結過的“First One, Then Many”情況。關于“First One,Then Many”,Kent Beck曾寫了一篇文章介紹如何可靠地擁抱變化,原文鏈接如下http://www.threeriversinstitute.org /FirstOneThenMany.html。在那篇文章中,Kent的問題是面對未來可能的需求變化,如何使用 Succession 的方式幫助系統(tǒng)架構平滑演化。下面是 Kent 的觀點:
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
- 修改原來單個Attachment的getter/setter,改成從attachments里面得到首元素或者往里面添加新元素,如getAttachments().get(0)
- 運行測試,確保所有測試都通過
- inline ReferenceFile類里面的對單個Attachment的getter/setter方法。這里要注意test fixture里面對domain model的aggregation的創(chuàng)建,而且因為涉及對List的操作,所以可能需要修改原來的測試代碼和test fixture
- 運行測試,確保所有測試都通過
到這里,“一對多”的工作完成之后,ReferenceFile 和 Attachment 類就變成了下文的樣子:
public class ReferenceFile {
private String category;
private List<Attachment> attachments;
//相應的 getter/setter,以及業(yè)務方法
}
public class Attachnment {
private Long id;
private String fileName;
//相應的 getter/setter,以及業(yè)務方法
}
domain model 持久化機制的修改
為了能實現一對多的實體關系,我們需要引入新的表作為“多”方,并保持“一”方的主鍵。使用Hibernate提供的one-to-many很容易做到這點,接下來是簡單的配置文件:
<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>
結論
至此,我們就完成了新功能的開發(fā),可以看出整個過程的思路非常明顯,而且因為主要是沿著重構的思想一路下來,思路非常清晰。另外,因為重構已經有成熟的IDE支持,我們可以利用到IDE的很多便利,這從另一方面也給我們帶來了非常的效率。
從整個過程來看,重構的一些方法和思想,不僅可以讓我們對遺留代碼進行優(yōu)化,使之能有利于新功能的開發(fā)(比如本文中的抽取新類和公開新類,都是為了下文的“由一到多”的功能開發(fā)),而且可以讓我們在開發(fā)功能的時候能從一個更高的角度來分解功能的開發(fā)工作,從而把原本復雜無序的過程簡化抽象成一段明確的重構鏈。那么,重構是否就是開發(fā)人員開發(fā)軟件的領域專屬語言呢(refactoring as DSLs to developers' development)?敬請期待本博關于這點的其他博文。