refactoring Patterns:第三部分 | ||||
石一楹 ([email protected]) 本文緊接第二部分,繼續講述應用 refactoring 應該考慮的問題。
他把Refactoring的情景和麪向對象出現使得情景相比較: 但是Martin Fowler和其他人確實觀察到了Refactoring可能引發的某些問題,我們可以來看一下: 數據庫 O/R mapping可以用來解決這個問題。使用專業的O/R mapping工具能夠實現關係數據庫的遷移。但是,就算這樣,遷移也需要付出額外的代價。 如果你使用的並非關係數據庫,而是直接採用OO數據庫,這一點的影響可能會變得更小。 所以,我建議每一個使用數據庫的應用程序都應該採用O/R mapping或者OO數據庫。目前出現的各種企業級應用解決方案如J2EE本身就提供這樣的構架。 如果你的代碼沒有這樣一個隔離層,那麼你必須手工或編寫專用的代碼來實現這些遷移功能。 接口改變和Published Interface 爲了保證系統的可觀察行爲不變,你必須保證這些接口的改變不會影響到你無法取得 的代碼。如果你擁有了所有使用該接口的類的源代碼,你只要把這些地方同時也改變即可。 但是,如果你沒有辦法得到所有這些使用的代碼,那麼你就不得不採取額外的途徑。事實上,如果你的代碼是一個代碼庫(如Sun JDK的集合框架)或者是一個Framework,那麼這一點幾乎是不可避免的。 要使得這些依賴於你老接口的代碼能夠繼續工作,你必須保留老接口。現在你有兩套接口,一套是老的,一套是經過Refactoring的新接口。你必須把對老接口的調用分派到新接口。千萬不要拷貝整個函數體,因爲這會產生大量的重複代碼。 這種方法雖然能夠解決問題,但是卻非常麻煩。由於Refactoring通常會涉及到狀態、行爲在不同類之間的轉移,如果一個方法從一個類移動到另一個類,那麼使用這種分派的方法可能需要一些不必要的中間狀態或者參數。這會使你的代碼顯得難以理解和維護,在一定程度上削減了Refactoring所應起到的作用。 因此,這種方法只應該用於過渡時期。給用戶一定的時間,允許用戶代碼能夠逐漸轉移到新接口,在超過一定的期限後,刪除老方法,不再支持老接口。這也是Java Deprecated API的意義所在。 像這樣保護接口雖然可能,卻非常困難。你至少需要在一段時間內維護兩套接口,以保證原來使用你老接口的客戶代碼還能繼續使用你的新代碼,Martin Fowler把這些接口稱之爲Published Interface。雖然你不可能避免公佈你的一部分接口,不然誰也不能使用你的代碼,但是過早公佈不必要的接口會造成不必要的麻煩,就像Martin Fowler給我們的提示: 用Refactoring思想武裝自己的設計 Refactoring包含兩個方面的想法:它告訴你可以從簡單的設計做起,因爲即使代碼已經實現,你還是可以用它來改進你的設計。然而,另一方面,它絕不是告訴你可以信手塗鴉。我給你的忠告是: 如果你一開始就設計了愚蠢的接口,甚至是錯誤的接口。在程序演變的過程中,這一部分可能變成系統的核心。對之進行Refactoring可能需要花費大量的精力,而改變接口和類的操作可能會是這些Refactoring主要內容。對核心類接口的變化可能會迅速波及到系統的各個層面,如果你的總體結構是好的,那麼這種漣漪可能會在某一個層次消失。(譬如環狀和層次性的體系結構。)如果你沒有這樣的抽象機制和保護體系,那麼對核心類的修改將會直接導致整個系統的變更,這是不能接受的。 所以,在設計一個類的時候,你需要問自己幾個問題,如果事情發生了這種變化,我會如何修改來適應?如果發生了那種變化,我會怎樣來適應?如果你能夠想到可能的Refactoring方法,那麼證明你的設計是可行的。這並不意味着你要去實現這樣的設計,而是保證自己的設計不會把自己逼入到死角。如果你發現自己的代碼幾乎沒有辦法Refactoring來適應新的需求,那麼你要仔細考慮考慮別的思路。 每次公司的程序員問我一個設計是否合理,我總是反問幾個問題:你如何適應這種變化,適應那種可能的變化。我同時指出現在沒有必要去實現這些變化。我很少直接回答他好壞或者給他一個答案,但在思考了我反問他們的問題以後,程序員總能對自己的設計做出好的評判,從而找到很好的解決方案。所以,使用Refactoring的思想考慮你的設計。 編程語言 Refactoring最初的研究是從Smalltalk開始的.隨着Refactoring在Smalltalk上的極端成功,更多的面向對象社團開始把Refactoring擴展到其他語言環境.但是不同語言的不同特點有時會對應用Refactoring提供便利,有時卻會製造障礙. .靜態類型檢查和存取保護 和Smalltalk這樣的動態類型語言不同,對靜態類型進行檢查的語言(C++,Java,Delphi等等)通常具有類繼承和相關的存取保護(private,protected,public),這些特點使得尋找對某一個函數的引用變得相對簡單.如果重命名的函數原先聲明爲private,那麼對該函數的引用只能是在他所在的類或者該類的友類(C++)等等.如果聲明爲protected,那麼只有本類,子類和友員類(同包類)才能引用到該成員函數.如果聲明爲public,那麼還只需要在本類、子類、友類和明確引入該類的其他類即可(include,import)。 我想提起大家注意的另外一個問題。在軟件的最初開發和整個開發流程中儘可能早地應用好的設計原則是一個軟件項目成功的重要因素。不管是從封裝的角度還是從Refactoring的角度來看,定義成員變量和成員函數應當從最高的保護級別開始。除了非常明顯的例子之外,你最好首先把成員變量和函數定義爲private。隨着軟件開發的進一步深入,當其他類對該類提出"額外"的請求,你慢慢地放寬保護。原則是:如果能夠放在private,就不要放在protected,能夠放在protected,就不要放在public。 預處理指令 依賴對象尺寸和實現格式的代碼 使用C++的指針、cast操作和sizeof(Object)這些依賴對象尺寸和實現格式的代碼很難refactor。指針和cast介入別名的概念,這使得你要查找所有對此Object有引用的代碼變得非常困難。這些特徵的一個共同特點就是它們暴露了對象的內部表達格式,從而違反了抽象的基本原則。 舉個例子,C++使用V-table機制來表達可執行程序中的成員變量。繼承得來的成員變量在前,本類定義的在後。一個我們經常使用,並且認爲安全的refactoring是push up fields,也就是把子類中的一個成員變量移到父類。因爲現在變量從父類繼承而非本類定義,經過refactoring後的可執行程序之中變量的實際位置已經發生了變化。 如果程序中所有的變量引用都是通過類接口來存取的,那麼這樣的變化不會有問題。但是,如果變量是通過指針運算(譬如,一個程序員有一個指向對象的指針,知道變量在類的第9個字節,然後使用指針運算給第9個字節賦值),上面的refacoting過程就會改變程序的行爲。類似情況,如果程序員使用if (sizeof(object)==15)這樣的條件判斷,refactoring的結果很可能會對該對象的大小產生影響,從而變得不再安全。 語言複雜度 解析引用的方式 反射、Meta級程序分析和變更 Java雖然還沒有像CLOS這樣強大的meta級功能,但是JDK的發展已經顯示了Java在這方面非常強勁的實力。象上面的例子,我們也可以在Java上做到。 一個小結 從實踐者的角度來看,目前最流行的refactoring文獻基本上都採用Java語言作爲範例,其中包括Martin的《Refactoring》。目前市場上有數種支持Java和Smalltalk的Refactoring工具,而C++的工具卻幾乎沒有。這裏面,語言本身的複雜性有很大的影響。 當然,這並不意味着C++程序員就不應該使用refactoring技術,只不過需要更多的努力。Refactoring技術已經證明自己是OO系統演化的最佳方法之一,不要放棄。
|