重構模式(三)---- 應用 Refactoring 需要考慮的問題

casularm發表於2004-10-23


        本文緊接第二部分,繼續講述應用 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系統演化的最佳方法之一,不要放棄。
        

相關文章