重構模式3

refactoring Patterns:第三部分  
內容:
應用Refactoring需要考慮的問題
數據庫
接口改變和Published Interface
用Refactoring思想武裝自己的設計
編程語言
支持Refactoring的語言特點和編程風格
使Refactoring複雜化的語言特點和編程風格
解析引用的方式
反射、Meta級程序分析和變更
一個小結
關於作者
相關內容:
該系列的其他部分
應用 Refactoring 需要考慮的問題

石一楹 ([email protected])
浙江大學靈峯科技開發公司技術總監
2001 年 12 月

本文緊接第二部分,繼續講述應用 refactoring 應該考慮的問題。


任何一種技術都可能有它自己的麻煩。但是往往在我們使用一種新技術的時候,可能還不能深入到發現它帶來的問題,正如Martin Fowler所說:
      在學習一種能夠極大提高生產力的新技術時,你很難看到它不能應用的場合。

他把Refactoring的情景和麪向對象出現使得情景相比較:
      情況恰如10年前的對象。不是我不考慮對象有限制。只是因爲我不知道那些限制是什麼,雖然我知道他帶來的好處。

但是Martin Fowler和其他人確實觀察到了Refactoring可能引發的某些問題,我們可以來看一下:

數據庫
很多應用程序的代碼可能與數據庫結構綁定得非常嚴密。如果要修改這些代碼,需要改變還有數據庫結構和原先已經存在的數據。

O/R mapping可以用來解決這個問題。使用專業的O/R mapping工具能夠實現關係數據庫的遷移。但是,就算這樣,遷移也需要付出額外的代價。

如果你使用的並非關係數據庫,而是直接採用OO數據庫,這一點的影響可能會變得更小。

所以,我建議每一個使用數據庫的應用程序都應該採用O/R mapping或者OO數據庫。目前出現的各種企業級應用解決方案如J2EE本身就提供這樣的構架。

如果你的代碼沒有這樣一個隔離層,那麼你必須手工或編寫專用的代碼來實現這些遷移功能。

接口改變和Published Interface
有很多Refactoring操作(如rename method name)確實改變了接口。面向對象承諾在接口不變的情況下給你以實現變化的自由。但如果接口發生改變,那麼你就不得不非常小心了。

爲了保證系統的可觀察行爲不變,你必須保證這些接口的改變不會影響到你無法取得 的代碼。如果你擁有了所有使用該接口的類的源代碼,你只要把這些地方同時也改變即可。

但是,如果你沒有辦法得到所有這些使用的代碼,那麼你就不得不採取額外的途徑。事實上,如果你的代碼是一個代碼庫(如Sun JDK的集合框架)或者是一個Framework,那麼這一點幾乎是不可避免的。

要使得這些依賴於你老接口的代碼能夠繼續工作,你必須保留老接口。現在你有兩套接口,一套是老的,一套是經過Refactoring的新接口。你必須把對老接口的調用分派到新接口。千萬不要拷貝整個函數體,因爲這會產生大量的重複代碼。

這種方法雖然能夠解決問題,但是卻非常麻煩。由於Refactoring通常會涉及到狀態、行爲在不同類之間的轉移,如果一個方法從一個類移動到另一個類,那麼使用這種分派的方法可能需要一些不必要的中間狀態或者參數。這會使你的代碼顯得難以理解和維護,在一定程度上削減了Refactoring所應起到的作用。

因此,這種方法只應該用於過渡時期。給用戶一定的時間,允許用戶代碼能夠逐漸轉移到新接口,在超過一定的期限後,刪除老方法,不再支持老接口。這也是Java Deprecated API的意義所在。

像這樣保護接口雖然可能,卻非常困難。你至少需要在一段時間內維護兩套接口,以保證原來使用你老接口的客戶代碼還能繼續使用你的新代碼,Martin Fowler把這些接口稱之爲Published Interface。雖然你不可能避免公佈你的一部分接口,不然誰也不能使用你的代碼,但是過早公佈不必要的接口會造成不必要的麻煩,就像Martin Fowler給我們的提示:
Don't publish interface prematurely.

用Refactoring思想武裝自己的設計
如果你不理解OO的思想,那麼你就不可能真正地用好OO語言。同樣,如果你沒有把Refactoring的思想貫穿於你的開發過程,你也不可能用好Refactoring。

Refactoring包含兩個方面的想法:它告訴你可以從簡單的設計做起,因爲即使代碼已經實現,你還是可以用它來改進你的設計。然而,另一方面,它絕不是告訴你可以信手塗鴉。我給你的忠告是:
Started simple but not stupid。

如果你一開始就設計了愚蠢的接口,甚至是錯誤的接口。在程序演變的過程中,這一部分可能變成系統的核心。對之進行Refactoring可能需要花費大量的精力,而改變接口和類的操作可能會是這些Refactoring主要內容。對核心類接口的變化可能會迅速波及到系統的各個層面,如果你的總體結構是好的,那麼這種漣漪可能會在某一個層次消失。(譬如環狀和層次性的體系結構。)如果你沒有這樣的抽象機制和保護體系,那麼對核心類的修改將會直接導致整個系統的變更,這是不能接受的。

所以,在設計一個類的時候,你需要問自己幾個問題,如果事情發生了這種變化,我會如何修改來適應?如果發生了那種變化,我會怎樣來適應?如果你能夠想到可能的Refactoring方法,那麼證明你的設計是可行的。這並不意味着你要去實現這樣的設計,而是保證自己的設計不會把自己逼入到死角。如果你發現自己的代碼幾乎沒有辦法Refactoring來適應新的需求,那麼你要仔細考慮考慮別的思路。

每次公司的程序員問我一個設計是否合理,我總是反問幾個問題:你如何適應這種變化,適應那種可能的變化。我同時指出現在沒有必要去實現這些變化。我很少直接回答他好壞或者給他一個答案,但在思考了我反問他們的問題以後,程序員總能對自己的設計做出好的評判,從而找到很好的解決方案。所以,使用Refactoring的思想考慮你的設計。

編程語言
雖然Refactoring是一種獨立於編程語言的方法,但你所使用的編程語言往往會或多或少地影響到Refactoring的效率,從而影響你採用Refactoring的積極性.

Refactoring最初的研究是從Smalltalk開始的.隨着Refactoring在Smalltalk上的極端成功,更多的面向對象社團開始把Refactoring擴展到其他語言環境.但是不同語言的不同特點有時會對應用Refactoring提供便利,有時卻會製造障礙.

支持Refactoring的語言特點和編程風格

.靜態類型檢查和存取保護
靜態類型檢查可以縮小對你想要refactoring的程序部分的可能引用範圍.舉個例子,如果你想要改變一個類的成員函數名,那麼你必須改變函數的聲明和所有對該函數的引用.如果程序很大,那麼查找這樣和改變這樣的引用就比較困難.

和Smalltalk這樣的動態類型語言不同,對靜態類型進行檢查的語言(C++,Java,Delphi等等)通常具有類繼承和相關的存取保護(private,protected,public),這些特點使得尋找對某一個函數的引用變得相對簡單.如果重命名的函數原先聲明爲private,那麼對該函數的引用只能是在他所在的類或者該類的友類(C++)等等.如果聲明爲protected,那麼只有本類,子類和友員類(同包類)才能引用到該成員函數.如果聲明爲public,那麼還只需要在本類、子類、友類和明確引入該類的其他類即可(include,import)。

我想提起大家注意的另外一個問題。在軟件的最初開發和整個開發流程中儘可能早地應用好的設計原則是一個軟件項目成功的重要因素。不管是從封裝的角度還是從Refactoring的角度來看,定義成員變量和成員函數應當從最高的保護級別開始。除了非常明顯的例子之外,你最好首先把成員變量和函數定義爲private。隨着軟件開發的進一步深入,當其他類對該類提出"額外"的請求,你慢慢地放寬保護。原則是:如果能夠放在private,就不要放在protected,能夠放在protected,就不要放在public。

使Refactoring複雜化的語言特點和編程風格

預處理指令
某些語言環境通常提供預處理指令,如C++。因爲預處理不是C++語言的一部分,這通常使得Refactoring工具實現變得困難。有研究指出,程序往往需要在預處理之後才能進行更好的結構分析,而在這一點上預處理指令信息已經不存在。而refactoring一旦沒有和源代碼的直接聯繫,程序員將不太可能對理解Refactoring的結果。

依賴對象尺寸和實現格式的代碼
C++繼承自C,這使得C++很快流行起來,程序員的學習難度也大大減小。但這是一把雙面刃。C++因此而支持很多編程風格,而其中的某些違反了優雅設計的基本原則。

使用C++的指針、cast操作和sizeof(Object)這些依賴對象尺寸和實現格式的代碼很難refactor。指針和cast介入別名的概念,這使得你要查找所有對此Object有引用的代碼變得非常困難。這些特徵的一個共同特點就是它們暴露了對象的內部表達格式,從而違反了抽象的基本原則。

舉個例子,C++使用V-table機制來表達可執行程序中的成員變量。繼承得來的成員變量在前,本類定義的在後。一個我們經常使用,並且認爲安全的refactoring是push up fields,也就是把子類中的一個成員變量移到父類。因爲現在變量從父類繼承而非本類定義,經過refactoring後的可執行程序之中變量的實際位置已經發生了變化。

如果程序中所有的變量引用都是通過類接口來存取的,那麼這樣的變化不會有問題。但是,如果變量是通過指針運算(譬如,一個程序員有一個指向對象的指針,知道變量在類的第9個字節,然後使用指針運算給第9個字節賦值),上面的refacoting過程就會改變程序的行爲。類似情況,如果程序員使用if (sizeof(object)==15)這樣的條件判斷,refactoring的結果很可能會對該對象的大小產生影響,從而變得不再安全。

語言複雜度
語言越複雜,對語言語義的形式化就更加困難。相對Smalltalk和稍微複雜的Java而言,C++可稱得上是一種非常複雜的語言,這使得對C++程序refactoring工具的研究大大滯後於smalltalk和Java。

解析引用的方式
由於C++絕大部分是在編譯是解析引用,所以在refactoring一個程序之後通常至少需要編譯程序的一部分,把可執行程序連接起來才能看到測試refactoring的影響。相反,smalltalk和CLOS提供解釋執行和增量編譯的技術。Java雖然沒有解釋執行,但它明確把一個公共類放在一個單元內的要求,使得執行一系列refactoring的成本減小。由於refactoring的基本方法就是每一步小小變化,每一步測試,對於C++而言,每一個迭代的成本相對較高,從而程序員變得不太願意做這些小變化。

反射、Meta級程序分析和變更
這一點可能更讓研究者關心而不是實踐者的問題。C++並沒有提供對meta級程序分析和變更的很好支持,你無法找到象CLOS這樣的metaobject協議。這些協議有時對refactoring非常有用,譬如我們可以把一個類的選定實例改變爲另一個類的實例,這時候可以利用這些反射協議實現把所有對舊對象的引用自動變更爲指向新的實例。

Java雖然還沒有像CLOS這樣強大的meta級功能,但是JDK的發展已經顯示了Java在這方面非常強勁的實力。象上面的例子,我們也可以在Java上做到。

一個小結
基於上面的比較,我們認爲Java是應用Refactoring的最佳語言。最近的觀察也證實了這一點[Lance Tokuda]。

從實踐者的角度來看,目前最流行的refactoring文獻基本上都採用Java語言作爲範例,其中包括Martin的《Refactoring》。目前市場上有數種支持Java和Smalltalk的Refactoring工具,而C++的工具卻幾乎沒有。這裏面,語言本身的複雜性有很大的影響。

當然,這並不意味着C++程序員就不應該使用refactoring技術,只不過需要更多的努力。Refactoring技術已經證明自己是OO系統演化的最佳方法之一,不要放棄。

關於作者
石一楹,現任浙江大學靈峯科技開發公司技術總監。多年從事OO系統分析、設計。現主要研究方向爲Refactoring和分析模式。你可以通過[email protected]和我聯繫。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章