摘要:本篇文章主要講解重構改善既有代碼的設計這本書中的第七章在對象之間搬移特性中的知識點,搬移函數問題你的程序中,有個函數與其所駐之外的另一個進行更多交流調用后者,或被后者調用。動機在之間移動狀態和行為,是重構過程中必不可少的措施。
如果你注定要成為厲害的人, 那問題的答案就深藏在你的血脈里。
本篇文章主要講解 《重構---改善既有代碼的設計》 這本書中的 第七章在對象之間搬移特性中 的知識點,
Move Method(搬移函數)問題:你的程序中,有個函數與其所駐class之外的另一個class進行更多交流:調用后者,或被后者調用。
解決:在該函數最常引用的class中建立一個有著類似行為的新函數。將舊函數變成一個單純的委托函數(delegating method),或是將舊函數完全移除。
動機「函數搬移」是重構理論的支柱。如果一個class有太多行為,或如果一個class與另一個class有太多合作而形成高度耦合(highly coupled),我就會搬移函數。通過這種手段,我可以使系統中的classes更簡單,這些classes最終也將更干凈利落地實現系統交付的任務。
常常我會瀏覽class的所有函數,從中尋找這樣的函數:使用另一個對象的次數比使用自己所駐對象的次數還多。一旦我移動了一些值域,就該做這樣的檢查。一旦發現「有可能被我搬移」的函數,我就會觀察調用它的那一端、它調用的那一端,以及繼承體系中它的任何一個重定義函數。然后,我會根據「這個函數與哪個對象的交流比較多」,決定其移動路徑。
這往往不是一個容易做出的決定。如果不能肯定是否應該移動一個函數,我就會繼續觀察其他函數。移動其他函數往往會讓這項決定變得容易一些。有時候,即使你移動了其他函數,還是很難對眼下這個函數做出決定。其實這也沒什么大不了的。 如果真的很難做出決定,那么或許「移動這個函數與否」并不那么重要。所以,我會憑本能去做,反正以后總是可以修改的。
檢查source class定義之source method所使用的一切特性(features),考慮它們是否也該被搬移。(譯注:此處所謂特性泛指class定義的所有東西,包括值域和函數。)
如果某個特性只被你打算搬移的那個函數用到,你應該將它一并搬移。如果另有其他函數使用了這個特性,你可以考慮將使用該特性的所有函數全都一并搬移。有時候搬移一組函數比逐一搬移簡單些。
范例我用一個表示「帳戶」的account class來說明這項重構:
class Account... //用戶類型類 private AccountType _type; //透支天數 private int _daysOverdrawn; //透支費用 double overdraftCharge() { //譯注:透支金計費,它和其他class的關系似乎比較密切。 //判斷保險 if (_type.isPremium()) { double result = 10; if (_daysOverdrawn > 7) result += (_daysOverdrawn - 7) * 0.85; return result; } else return _daysOverdrawn * 1.75; } //銀行操作 double bankCharge() { double result = 4.5; if (_daysOverdrawn > 0) result += overdraftCharge(); return result; }
假設有數種新帳戶,每一種都有自己的「透支金計費規則」。
所以我希望將overdraftCharge()搬移到AccountType class去。
第一步要做的是:觀察被overdraftCharge()使用的每一特性(features),考慮是否值得將它們與overdraftCharge()—起移動。此例之中我需要讓daysOverdrawn值域留在Account class,因為其值會隨不同種類的帳戶而變化。然后,我將overdraftCharge()函數碼拷貝到AccountType中,并做相應調整。
class AccountType... double overdraftCharge(int daysOverdrawn) { if (isPremium()) { double result = 10; if (daysOverdrawn > 7) result += (daysOverdrawn - 7) * 0.85; return result; } else return daysOverdrawn * 1.75; }
在這個例子中,「調整」的意思是:(1)對于「使用AccountType特性」的語句,去掉_type;(2)想辦法得到依舊需要的Account class特性。當我需要使用source class特性,我有四種選擇:(1)將這個特性也移到target class;(2)建立或使用一個從target class到source的引用〔指涉)關系;(3)將source object當作參數傳給target class;(4)如果所需特性是個變量,將它當作參數傳給target method。
本例中我將_daysOverdrawn變量作為參數傳給target method(上述(4))。
調整target method使之通過編譯,而后我就可以將source method的函數本體替換為一個簡單的委托動作(delegation),然后編譯并測試:
class Account... double overdraftCharge() { return _type.overdraftCharge(_daysOverdrawn); }
我可以保留代碼如今的樣子,也可以刪除source method。如果決定刪除,就得找出source method的所有調用者,并將這些調用重新定向,改調用Account的bankCharge():
class Account... double bankCharge() { double result = 4.5; if (_daysOverdrawn > 0) result += _type.overdraftCharge(_daysOverdrawn); return result; }
所有調用點都修改完畢后,我就可以刪除source method在Account中的聲明了。我可以在每次刪除之后編譯并測試,也可以一次性批量完成。如果被搬移的函數不是private,我還需要檢查其他classes是否使用了這個函數。在強型(strongly typed) 語言中,刪除source method聲明式后,編譯器會幫我發現任何遺漏。
此例之中被移函數只取用(指涉〕一個值域,所以我只需將這個值域作為參數傳給target method就行了。如果被移函數調用了Account中的另一個函數,我就不能這么簡單地處理。這種情況下我必須將source object傳遞給target method:
class AccountType... double overdraftCharge(Account account) { if (isPremium()) { double result = 10; if (account.getDaysOverdrawn() > 7) result += (account.getDaysOverdrawn() - 7) * 0.85; return result; } else return account.getDaysOverdrawn() * 1.75; }
如果我需要source class的多個特性,那么我也會將source object傳遞給target method。不過如果target method需要太多source class特性,就得進一步重構。通常這種情況下我會分解target method,并將其中一部分移回source class。
Move Field(搬移值域)問題:你的程序中,某個field(值域〕被其所駐class之外的另一個class更多地用到。
解決:在target class 建立一個new field,修改source field的所有用戶,令它們改用此new field。
在classes之間移動狀態(states)和行為,是重構過程中必不可少的措施。
隨著系統發展,你會發現自己需要新的class,并需要將原本的工作責任拖到新的class中。這個星期中合理而正確的設計決策,到了下個星期可能不再正確。這沒問題;如果你從來沒遇到這種情況,那才有問題。
如果我發現,對于一個field(值域),在其所駐class之外的另一個class中有更多函數使用了它,我就會考慮搬移這個field。上述所謂「使用」可能是通過設值/取值(setting/getting)函數間接進行。我也可能移動該field的用戶(某函數),這取決于是否需要保持接口不受變化。
如果這些函數看上去很適合待在原地,我就選擇搬移field。
使用Extract Class 時,我也可能需要搬移field。此時我會先搬移field,然后再搬移函數。
如果field的屬性是public,首先使用Encapsulate Field(封裝字段) 將它封裝起來。
? 如果你有可能移動那些頻繁訪問該field的函數,或如果有許多函數訪問某個field,先使用Self Encapsulate Field 也許會有幫助。
編譯,測試。
在target class中建立與source field相同的field,并同時建立相應的設值/取值 (setting/getting)函數。
編譯target class。
決定如何在source object中引用target object。
? 一個現成的field或method可以助你得到target object。如果沒有,就看能否輕易建立這樣一個函數。如果還不行,就得在source class中新建一個field來存放target object。這可能是個永久性修改,但你也可以暫不公開它,因為后續重構可能會把這個新建field除掉。
刪除source field。
將所有「對source field的引用」替換為「對target適當函數的調用」。
? 如果是「讀取」該變量,就把「對source field的引用」替換為「對target取值函數(getter)的調用」;如果是「賦值」該變量,就把對source field的引用」替換成「對設值函數(setter)的調用」。
? 如果source field不是private,就必須在source class的所有subclasses中查找source field的引用點,并進行相應替換。
· 編譯,測試。
范例下面是Account class的部分代碼:
class Account... private AccountType _type; private double _interestRate; double interestForAmount_days (double amount, int days) { return _interestRate * amount * days / 365; }
我想把表示利率的_interestRate搬移到AccountType class去。
目前已有數個函數引用了它,interestForAmount_days() 就是其一。
下一步我要在AccountType中建立_interestRate field以及相應的訪問函數:
class AccountType... private double _interestRate; void setInterestRate (double arg) { _interestRate = arg; } double getInterestRate () { return _interestRate; }
這時候我可以編譯新的AccountType class。
現在,我需要讓Account class中訪問此_interestRate field的函數轉而使用AccountType對象,然后刪除Account class中的_interestRate field。
我必須刪除source field,才能保證其訪問函數的確改變了操作對象,因為編譯器會幫我指出未正確獲得修改的函數。
private double _interestRate; double interestForAmount_days (double amount, int days) { return _type.getInterestRate() * amount * days / 365; }范例:使用Self Encapsulate(自我封裝)
如果有很多函數已經使用了_interestRate field,我應該先運用Self Encapsulate Field:
class Account... private AccountType _type; private double _interestRate; double interestForAmount_days (double amount, int days) { return getInterestRate() * amount * days / 365; } private void setInterestRate (double arg) { _interestRate = arg; } private double getInterestRate () { return _interestRate; }
這樣,在搬移field之后,我就只需要修改訪問函數(accessors)就行了 :
double interestForAmountAndDays (double amount, int days) { return getInterestRate() * amount * days / 365; } private void setInterestRate (double arg) { _type.setInterestRate(arg); } private double getInterestRate () { return _type.getInterestRate(); }
如果以后有必要,我可以修改訪問函數(accessors)的用戶,讓它們使用新對象。 Self Encapsulate Field 使我得以保持小步前進。如果我需要對做許多處理,保持小步前進是有幫助的。特別值得一提的是:首先使用Self Encapsulate Field 使我得以更輕松使用Move Method 將函數搬移到target class中。如果待移函數引用了field的訪問函數(accessors),那么那些引用點是無須修 改的。
Extract Class(提煉類)問題:某個class做了應該由兩個classes做的事。
解決:建立一個新class,將相關的值域和函數從舊class搬移到新class。
動機你也許聽過類似這樣的教誨:一個class應該是一個清楚的抽象(abstract),處理一些明確的責任。
但是在實際工作中,class會不斷成長擴展。你會在這兒加入一些功能,在那兒加入一些數據。給某個class添加一項新責任時,你會覺得不值得為這項責任分離出一個多帶帶的class。于是,隨著責任不斷増加,這個class會變得過份復雜。很快,你的class就會變成一團亂麻。
這樣的class往往含有大量函數和數據。這樣的class往往太大而不易理解。此時你需要考慮哪些部分可以分離出去,并將它們分離到一個多帶帶的class中。如果某些數據和某些函數總是一起出現,如果某些數據經常同時變化甚至彼此相依,這就表示你應該將它們分離出去。一個有用的測試就是問你自己,如果你搬移了某些值域和函數,會發生什么事?其他值域和函數是否因此變得無意義?
另一個往往在開發后期出現的信號是class的「subtyped方式」。如果你發現subtyping只影響class的部分特性,或如果你發現某些特性「需要以此方式subtyped」,某些特性「需要以彼方式subtyped」,這就意味你需要分解原來的class。
決定如何分解c;ass所負責任。
建立一個新class,用以表現從舊class中分離出來的責任。
? 如果舊class剩下的責任與舊class名稱不符,為舊class易名。
建立「從舊class訪問新class」的連接關系(link)。
? 也許你有可能需要一個雙向連接。但是在真正需要它之前,不要建立 「從新class通往舊class」的連接。
對于你想搬移的每一個值域,運用Move Field 搬移之。
每次搬移后,編譯、測試。
使用Move Method 將必要函數搬移到新class。先搬移較低層函數(也就是「被其他函數調用」多于「調用其他函數」者),再搬移較高層函數。
每次搬移之后,編譯、測試。
檢查,精簡每個class的接口。
? 如果你建立起雙向連接,檢查是否可以將它改為單向連接。
決定是否讓新class曝光。如果你的確需要曝光它,決定讓它成為reference object (引用型對象〕或immutable value object(不可變之「實值型對象」)。
范例讓我們從一個簡單的Person class開始
class Person... private String _name; private String _officeAreaCode; private String _officeNumber; public String getName() { return _name; } public String getTelephoneNumber() { return ("(" + _officeAreaCode + ") " + _officeNumber); } String getOfficeAreaCode() { return _officeAreaCode; } void setOfficeAreaCode(String arg) { _officeAreaCode = arg; } String getOfficeNumber() { return _officeNumber; } void setOfficeNumber(String arg) { _officeNumber = arg; }
在這個例子中,我可以將「與電話號碼相關」的行為分離到一個獨立class中。首 先我耍定義一個TelephoneNumber class來表示「電話號碼」這個概念:
class TelephoneNumber { }
易如反掌!然后,我要建立從Person到TelephoneNumber的連接:
class Person private TelephoneNumber _officeTelephone = new TelephoneNumber();
現在,我運用Move Field 移動一個值域:
class Person... public String getName() { return _name; } public String getTelephoneNumber(){ return _officeTelephone.getTelephoneNumber(); } TelephoneNumber getOfficeTelephone() { return _officeTelephone; } private String _name; private TelephoneNumber _officeTelephone = new TelephoneNumber(); class TelephoneNumber... public String getTelephoneNumber() { return ("(" + _areaCode + ") " + _number); } String getAreaCode() { return _areaCode; } void setAreaCode(String arg) { _areaCode = arg; } String getNumber() { return _number; } void setNumber(String arg) { _number = arg; } private String _number; private String _areaCode;
下一步要做的決定是:要不要對客戶揭示這個新口class?我可以將Person中「與電 話號碼相關」的函數委托(delegating)至TelephoneNumber,從而完全隱藏這個新class;也可以直接將它對用戶曝光。我還可以將它暴露給部分用戶(位于同一個package中的用戶),而不暴露給其他用戶。
如果我選擇暴露新class,我就需要考慮別名(aliasing)帶來的危險。如果我暴露了TelephoneNumber ,而有個用戶修改了對象中的_areaCode值域值,我又怎么能知道呢?而且,做出修改的可能不是直接用戶,而是用戶的用戶的用戶。
面對這個問題,我有下列數種選擇:
允許任何對象修改TelephoneNumber 對象的任何部分。這就使得TelephoneNumber 對象成為引用對象(reference object),于是我應該考慮使用 Change Value to Reference。這種情況下,Person應該是TelephoneNumber的訪問點。
不許任何人「不通過Person對象就修改TelephoneNumber 對象」。為了達到目的,我可以將TelephoneNumber「設為不可修改的(immutable),或為它提供一個不可修改的接口(immutable interface)。
另一個辦法是:先復制一個TelephoneNumber 對象,然后將復制得到的新對象傳遞給用戶。但這可能會造成一定程度的迷惑,因為人們會認為他們可以修改TelephoneNumber對象值。此外,如果同一個TelephoneNumber 對象 被傳遞給多個用戶,也可能在用戶之間造成別名(aliasing)問題。
Extract Class 是改善并發(concurrent)程序的一種常用技術,因為它使你可以為提煉后的兩個classes分別加鎖(locks)。如果你不需要同時鎖定兩個對象, 你就不必這樣做。這方面的更多信息請看Lea[Lea], 3.3節。
這里也存在危險性。如果需要確保兩個對象被同時鎖定,你就面臨事務(transaction)問題,需要使用其他類型的共享鎖〔shared locks〕。正如Lea[Lea] 8.1節所討論, 這是一個復雜領域,比起一般情況需要更繁重的機制。事務(transaction)很有實用性,但是編寫事務管理程序(transaction manager)則超出了大多數程序員的職責范圍。
問題:你的某個class沒有做太多事情(沒有承擔足夠責任)。
解決:將class的所有特性搬移到另一個class中,然后移除原class。
動機Inline Class正好與Extract Class 相反。如果一個class不再承擔足夠 責任、不再有多帶帶存在的理由〔這通常是因為此前的重構動作移走了這個class的 責任),我就會挑選這一「萎縮class」的最頻繁用戶(也是個class),以Inline Class手法將「妻縮class」塞進去。
作法在absorbing class(合并端的那個class)身上聲明source class的public協議, 并將其中所有函數委托(delegate)至source class。
? 如果「以一個獨立接口表示source class函數」更合適的話,就應該在inlining之前先使用Extract Interface。
修改所有source class引用點,改而引用absorbing class。
? 將source class聲明為private,以斬斷package之外的所有引用可能。 同時并修改source class的名稱,這便可使編譯器幫助你捕捉到所有對于source class的"dangling references "(虛懸引用點)。
編譯,測試。
運用Move Method 和 Move Field ,將source class的特性全部搬移至absorbing class。
為source class舉行一個簡單的喪禮。
范例先前(上個重構項〉我從TelephoneNumber「提煉出另一個class,現在我要將它inlining塞回到Person去。一開始這兩個classes是分離的:
class Person... private String _number; private String _areaCode; public String getName() { return _name; } public String getTelephoneNumber(){ return _officeTelephone.getTelephoneNumber(); } TelephoneNumber getOfficeTelephone() { return _officeTelephone; } private String _name; private TelephoneNumber _officeTelephone = new TelephoneNumber(); class TelephoneNumber... public String getTelephoneNumber() { return ("(" + _areaCode + ") " + _number); } String getAreaCode() { return _areaCode; } void setAreaCode(String arg) { _areaCode = arg; } String getNumber() { return _number; } void setNumber(String arg) { _number = arg; }
首先我在Person中聲明TelephoneNumber「的所有「可見」(public)函數:
class Person... String getAreaCode() { return _officeTelephone.getAreaCode(); //譯注:請注意其變化 } void setAreaCode(String arg) { _officeTelephone.setAreaCode(arg); //譯注:請注意其變化 } String getNumber() { return _officeTelephone.getNumber(); //譯注:請注意其變化 } void setNumber(String arg) { _officeTelephone.setNumber(arg); //譯注:請注意其變化 }
現在,我要找出TelephoneNumber的所有用戶,讓它們轉而使用Person接口。于是下列代碼:
Person martin = new Person(); martin.getOfficeTelephone().setAreaCode ("781");
就變成了:
Person martin = new Person(); martin.setAreaCode ("781");
現在,我可以持續使用Move Method 和 Move Field ,直到TelephoneNumber不復存在。
Hide Delegate(隱藏「委托關系」)問題:客戶直接調用其server object(服務對象)的delegate class。
解決:在server端(某個class〕建立客戶所需的所有函數,用以隱藏委托關系(delegation)。
動機封裝」即使不是對象的最關鍵特征,也是最關鍵特征之一。「封裝」意味每個對象都應該盡可能少了解系統的其他部分。如此一來,一旦發生變化,需要了解這一 變化的對象就會比較少——這會使變化比較容易進行。
任何學過對象技術的人都知道:雖然Java允許你將值域聲明為public,但你還是應該隱藏對象的值域。隨著經驗日漸豐富,你會發現,有更多可以(并值得)封裝的東西。
如果某個客戶調用了「建立于server object (服務對象)的某個值域基礎之上」的函數,那么客戶就必須知曉這一委托對象(delegate object。譯注:即server object的那個特殊值域)。萬一委托關系發生變化,客戶也得相應變化。你可以在server 端放置一個簡單的委托函數(delegating method),將委托關系隱藏起來,從而去除這種依存性(圖7.1)。這么一來即便將來發生委托關系上的變化,變化將被限制在server中,不會波及客戶。
對于某些客戶或全部客戶,你可能會發現,有必要先使用Extract Class 。一旦你對所有客戶都隱藏委托關系(delegation),你就可以將server 接口中的所有 委托都移除。
作法對于每一個委托關系中的函數,在server端建立一個簡單的委托函數(delegating method)。
調整客戶,令它只調用server 提供的函數(譯注:不得跳過徑自調用下層)。
? 如果client (客戶〕和server不在同一個package,考慮修改委托函數 (delegate method)的訪問權限,讓client得以在package之外調用它。
每次調整后,編譯并測試。
如果將來不再有任何客戶需要取用圖7.1的Delegate (受托類),便可移除server中的相關訪問函數(accessor for the delegate)。
編譯,測試。
范例本例從兩個classes開始,代表「人」的Person和代表「部門」的Department:
class Person { Department _department; public Department getDepartment() { return _department; } public void setDepartment(Department arg) { _department = arg; } } class Department { private String _chargeCode; private Person _manager; public Department (Person manager) { _manager = manager; } public Person getManager() { return _manager; } ...
如果客戶希望知道某人的經理是誰,他必須先取得Department對象:
manager = john.getDepartment().getManager();
這樣的編碼就是對客戶揭露了Department的工作原理,于是客戶知道:Department用以追蹤「經理」這條信息。
如果對客戶隱藏Department,可以減少耦合(coupling)。 為了這一目的,我在Person中建立一個簡單的委托函數:
public Person getManager() { return _department.getManager(); }
現在,我得修改Person的所有客戶,讓它們改用新函數:
manager = john.getManager();
只要完成了對Department所有函數的委托關系,并相應修改了Person的所有客 戶,我就可以移除Person中的訪問函數getDepartment()了。
Remove Middle Man(移除中間人)問題:某個class做了過多的簡單委托動作(simple delegation)。
解決:讓客戶直接調用delegate(受托類)。
動機在Hide Delegate的「動機」欄,我談到了「封裝 delegated object(受托對 象)」的好處。
但是這層封裝也是要付出代價的,它的代價就是:每當客戶要使用 delegate(受托類)的新特性時,你就必須在server 端添加一個簡單委托函數。隨著delegate的特性(功能)愈來愈多,這一過程會讓你痛苦不己。server 完全變成了一 個「中間人」,此時你就應該讓客戶直接調用delegate。
很難說什么程度的隱藏才是合適的。還好,有了Hide Delegate和Remove Middle Man,你大可不必操心這個問題,因為你可以在系統運行過程中不斷進行調整。隨著系統的變化,「合適的隱藏程度」這個尺度也相應改變。六個月 前恰如其分的封裝,現今可能就顯得笨拙。重構的意義就在于:你永遠不必說對不起——只要把出問題的地方修補好就行了。
建立一個函數,用以取用delegate(受托對象)。
對于每個委托函數(delegate method),在server中刪除該函數,并將「客戶對該函數的調用」替換為「對delegate(受托對象)的調用」。
處理每個委托函數后,編譯、測試。
范例我將以另一種方式使用先前用過的「人與部門」例子。還記得嗎,上一項重構結束時,Person將Department隱藏起來了:
class Person... Department _department; public Person getManager() { return _department.getManager(); class Department... private Person _manager; public Department (Person manager) { _manager = manager; }
為了找出某人的經理,客戶代碼可能這樣寫:
manager = john.getManager();
像這樣,使用和封裝Department都很簡單。但如果大量函數都這么做,我就不得不在Person之中安置大量委托行為(delegations)。這就是移除中間人的時候了。 首先在Person建立一個「受托對象(delegate)取得函數」:
class Person... public Department getDepartment() { return _department; }
然后逐一處理每個委托函數。針對每一個這樣的函數,我要找出通過Person使用的函數,并對它進行修改,使它首先獲得受托對象(delegate),然后直接使用之:
manager = john.getDepartment().getManager();
然后我就可以刪除Person的getManager() 函數。如果我遺漏了什么,編譯器會 告訴我。
為方便起見,我也可能想要保留一部分委托關系(delegations)。此外我也可能希望對某些客戶隱藏委托關系,并讓另一些用戶直接使用受托對象。基于這些原因,一些簡單的委托關系(以及對應的委托函數)也可能被留在原地。
問題:你所使用的server class需要一個額外函數,但你無法修改這個class。
解決:在client class中建立一個函數,并以一個server class實體作為第一引數(argument)
Date newStart = new Date (previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1); Date newStart = nextDay(previousEnd); private static Date nextDay(Date arg) { return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1); }動機
這種事情發生過太多次了:你正在使用一個class,它真的很好,為你提供了你想要的所有服務。
而后,你又需要一項新服務,這個class卻無法供應。
于是你開始咒罵:「為什么不能做這件事?」如果可以修改源碼,你便可以自行添加一個新函數; 如果不能,你就得在客戶端編碼,補足你要的那個函數。
如果client class只使用這項功能一次,那么額外編碼工作沒什么大不了,甚至可能根本不需要原本提供服務的那個class。然而如果你需要多次使用這個函數,你就得不斷重復這些代碼。還記得嗎,重復的代碼是軟件萬惡之源。這些重復性代碼應該被抽出來放進同一個函數中。進行本項重構時,如果你以外加函數實現一項功能, 那就是一個明確信號:這個函數原本應該在提供服務的(server)class中加以實現。
如果你發現自己為一個server class建立了大量外加函數,或如果你發現有許多classes都需要同樣的外加函數,你就不應該再使用本項重構,而應該使用 Introduce Local Extension。
但是不要忘記:外加函數終歸是權宜之計。如果有可能,你仍然應該將這些函數搬移到它們的理想家園。如果代碼擁有權(code ownership)是個需要考量的問題, 就把外加函數交給server class的擁有者,請他幫你在此server class中實現這個函數。
在client class中建立一個函數,用來提供你需要的功能。
? 這個函數不應該取用client class的任何特性。如果它需要一個值,把該值當作參數傳給它。
以server class實體作為該函數的第一個參數。
將該函數注釋為:「外加函數(foreign method),應在server class實現。」
? 這么一來,將來如果有機會將外加函數搬移到server class中,你便可以輕松找出這些外加函數。
程序中,我需要跨過一個收費周期(billing period)。原本代碼像這樣:
Date newStart = new Date (previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1);
我可以將賦值運算右側代碼提煉到一個獨立函數中。這個函數就是Date class的一個外加函數:
Date newStart = nextDay(previousEnd); private static Date nextDay(Date arg) { // foreign method, should be on date return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1); }Introduce Local Extension(引入本地擴展)
問題:你所使用的server class需要一些額外函數,但你無法修改這個class。
解決:建立一個新class,使它包含這些額外函數。讓這個擴展品成為source class的subclass (子類〕或wrapper(外覆類)。
很遺憾,classes的作者無法預知未來,他們常常沒能為你預先準備一些有用的函數。
如果你可以修改源碼,最好的辦法就是直接加入自己需要的函數。但你經常無法修改源碼。如果只需要一兩個函數,你可以使用Introduce Foreign Method。
但如果你需要的額外函數超過兩個,外加函數(foreign methods)就很難控制住它 們了。所以,你需要將這些函數組織在一起,放到一個恰當地方去。要達到這一目 的,標準對象技術subclassing和wrapping是顯而易見的辦法。這種情況下我把 subclass 或wrapper稱為local extention(本地擴展〕。
所謂本地擴展是一個獨立的class,但也是被擴展的子類型。這意味它提供original class的一切特性,同時并額外添加新特性。在任何使用original class的地方,你都可以使用local extention取而代之。
作法建立一個extension class,將它作為原物(原類〉的subclass或wrapper。
在extension class 中加入轉型構造函數(converting constructors )。
? 所謂「轉型構造函數」是指接受原物(original)作為參數。如果你釆用subclassing方案,那么轉型構造函數應該調用適當的subclass構造函數;如果你采用wrapper方案,那么轉型構造函數應該將它所獲得之引數(argument)賦值給「用以保存委托關系(delegate)」的那個值域。
在extension class中加入新特性。
根據需要,將原物(original)替換為擴展物(extension)。
將「針對原始類(original class)而定義的所有外加函數(foreign methods)」 搬移到擴展類extension中。
范例我將以Java 1.0.1的Date class為例。Java 1.1已經提供了我想要的功能,但是在它到來之前的那段日子,很多時候我需要擴展Java 1.0.1的Date class。
第一件待決事項就是使用subclass或wrapper。subclassing是比較顯而易見的辦法:
Class mfDate extends Date { public nextDay()... public dayOfYear()...
wrapper則需要用上委托(delegation):
class mfDate { private Date _original;范例:是用Subclass(子類)
首先,我要建立一個新的MfDateSub class來表示「日期」(譯注:"Mf"是作者Martin Fowler的姓名縮寫),并使其成為Date的subclass:
class MfDateSub extends Date
然后,我需要處理Date 和我的extension class之間的不同處。MfDateSub 構造函數需要委托(delegating)給Date構造函數:
public MfDateSub (String dateString) { super (dateString); };
現在,我需要加入一個轉型構造函數,其參數是一個隸屬原類的對象:
public MfDateSub (Date arg) { super (arg.getTime()); }
現在,我可以在extension class中添加新特性,并使用Move Method 將所有外加函數(foreign methods)搬移到extension class。于是,下面的代碼:
client class... private static Date nextDay(Date arg) { // foreign method, should be on date return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1); }
經過搬移之后,就成了:
class MfDate... Date nextDay() { return new Date (getYear(),getMonth(), getDate() + 1); }范例:是用wrapper(外覆類)
首先聲明一個wrapping class:
class mfDate { private Date _original; }
使用wrapping方案時,我對構造函數的設定與先前有所不同。現在的構造函數將只是執行一個單純的委托動作(delegation):
public MfDateWrap (String dateString) { _original = new Date(dateString); };
而轉型構造函數則只是對其instance變量賦值而己:
public MfDateWrap (Date arg) { _original = arg; }
接下來是一項枯燥乏味的工作:為原始類的所有函數提供委托函數。我只展示兩個函數,其他函數的處理依此類推。
public int getYear() { return _original.getYear(); } public boolean equals (MfDateWrap arg) { return (toDate().equals(arg.toDate())); }
完成這項工作之后,我就可以后使用Move Method 將日期相關行為搬移到新class中。于是以下代碼:
client class... private static Date nextDay(Date arg) { // foreign method, should be on date return new Date (arg.getYear(),arg.getMonth(), arg.getDate() + 1); }
經過搬移之后,就變成:
class MfDate... Date nextDay() { return new Date (getYear(),getMonth(), getDate() + 1); }
使用wrappers有一個特殊問題:如何處理「接受原始類之實體為參數」的函數?例如:
public boolean after (Date arg)
由于無法改變原始類〔original),所以我只能以一種方式使用上述的after() :
aWrapper.after(aDate) // can be made to work aWrapper.after(anotherWrapper) // can be made to work aDate.after(aWrapper) // will not work
這樣覆寫(overridden)的目的是為了向用戶隱藏wrapper 的存在。這是一個好策略,因為wrapper 的用戶的確不應該關心wrapper 的存在,的確應該可以同樣地對待wrapper(外覆類)和orignal((原始類)。但是我無法完全隱藏此一信息,因為某些系統所提供的函數(例如equals() 會出問題。
你可能會認為:你可以在MfDateWrap class 中覆寫equals(),像這樣:
public boolean equals (Date arg) // causes problems
但這樣做是危險的,因為盡管我達到了自己的目的,Java 系統的其他部分都認為equals() 符合交換律:如果a.equals(b)為真,那么b.equals(a)也必為真。違反這一規則將使我遭遇一大堆莫名其妙的錯誤。
要避免這樣的尷尬境地,惟一辦法就是修改Date class。但如果我能夠修改Date ,我又何必進行此項重構?所以,在這種情況下,我只能(必須〕向用戶暴露「我進行了包裝」這一事實。我將以一個新函數來進行日期之間的相等性檢查(equality tests):
public boolean equalsDate (Date arg)
我可以重載equalsDate() ,讓一個重載版本接受Date 對象,另一個重載版本接受MfDateWrap 對象。這樣我就不必檢查未知對象的型別了:
public boolean equalsDate (MfDateWrap arg)
subclassing方案中就沒有這樣的問題,只要我不覆寫原函數就行了。
但如果我覆寫了original class 中的函數,那么尋找函數時,我會被搞得暈頭轉向。一般來說,我不會在extension class 中覆寫0original class 的函數,我只會添加新函數。
譯注:equality(相等性)是一個很基礎的大題目。《Effective Java》 by Joshua Bloch 第3章,以及《Practical Java》by Peter Haggar 第2章,對此均有很深入的討論。這兩本書對于其他的基礎大題目如Serizable,Comparable,Cloneable,hashCode() 也都有深刻討論。
感謝觀看 你肯定有收獲對吧
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70755.html
摘要:前言決定把責任放在哪對于對象設計是最重要的之一。重構可以很好的解決這個問題。方法建立一個新類,將相關的字段和函數從舊類搬移到新類。方法將這個類的所有特性搬移到另一個類中,然后移除原類。讓這個擴展品成為源類的子類或包裝類。 前言 決定把責任放在哪對于對象設計是最重要的之一。重構可以很好的解決這個問題。以下是筆者的重構方法注:客戶:調用接口客戶類:使用了接口的類服務類:提供服務的類 Mov...
摘要:重構在不改變代碼的外在的行為的前提下對代碼進行修改最大限度的減少錯誤的幾率本質上,就是代碼寫好之后修改它的設計。重構可以深入理解代碼并且幫助找到。同時重構可以減少引入的機率,方便日后擴展。平行繼承目的在于消除類之間的重復代碼。 重構 (refactoring) 在不改變代碼的外在的行為的前提下 對代碼進行修改最大限度的減少錯誤的幾率 本質上, 就是代碼寫好之后 修改它的設計。 1,書中...
摘要:為何重構重構有四大好處重構改進軟件設計如果沒有重構,程序的設計會逐漸腐敗變質。經常性的重構可以幫助維持自己該有的形態。你有一個大型函數,其中對局部變量的使用使你無法采用。將這個函數放進一個單獨對象中,如此一來局部變量就成了對象內的字段。 哪有什么天生如此,只是我們天天堅持。 -Zhiyuan 國慶抽出時間來閱讀這本從師傅那里借來的書,聽說還是程序員的必讀書籍。 關于書的高清下載連...
摘要:并根據目錄選讀第章重構,第一個案例這是只是一個方法。絕大多數情況下,函數應該放在它所使用的數據的所屬對象內最好不要在另一個對象的屬性基礎上運用語句。 什么是重構 在不改變代碼外在行為的前提下,對代碼做出修改以改進程序內部的結構簡單地說就是在代碼寫好后改進它的設計 誰該閱讀這本書 專業程序員(能夠提高你的代碼質量) 資深設計師和架構規劃師(理解為什么需要重構,哪里需要重構) 閱讀技巧...
閱讀 2518·2021-09-24 10:29
閱讀 3799·2021-09-22 15:46
閱讀 2571·2021-09-04 16:41
閱讀 2977·2019-08-30 15:53
閱讀 1258·2019-08-30 14:24
閱讀 3052·2019-08-30 13:19
閱讀 2170·2019-08-29 14:17
閱讀 3520·2019-08-29 12:55