Martin Fowler在《refactoring》中講到了這個問題,但是它在最開始的類別裡面就有 type 這樣的類別屬性(校註1)。實際情況下,如果已經明確地指定了類型屬性,就不太可能不會直接去實作類別。我在這裡所描述的,是我認為最可能出現的實際情況,所以 refactoring 的方法和過程與 Martin Fowler 的完全不同,大家可以相互參考。
Refactoring 一般發生於你需要增加新功能之時,由於新功能加入是如此之困難,你會想到是否有必要改變原來的設計和程式結構,從而使得功能的加入變得更加簡單。當然,在這裡,你要記住兩頂帽子,一頂是修改,一頂使功能增加,千萬不要同時戴上兩頂帽子。
但是,一個很好的習慣是經常對你的程式碼進行審查,聞一聞其中的味道,是否有點異樣。大量的 case、if-else-if-else或類似的 switch 敘述就是一種異味。如果在你的程式碼中出現了很長的case語句,並且這些 case 或 if-else-if-else 語句都具有相同的結構,或者都是對常數整數或常數字串進行判斷,那麼,這樣的條件需要變成子類別(校註2)。
如果這些條件之下不影響行為,那麼這相當於一個列舉型態,你可以使用 Martin Fowler 的「把類型程式碼變成類別」的 refactoring 方法(校註3)。
但是,如果在不同的條件之下帶有不同的行為,那麼這些不同的行為必定帶有各自的資料結果,顯然這些資料結構將全部保存在條件語句所在方法的類別中。你的類別將日益增長,類別的介面將越來越臃腫。更嚴重的是,當你需要增加新功能的時候,不能僅僅依靠增加一個類別就能夠實現,而是需要找到這樣的 switch 敘述出現的地方,直接修改來源程式碼。對來源程式碼的修改甚至會引起介面的變化,從而影響該類別原來的介面。
下面的例子取自 Delphi 的一個片段。我在程式碼中省去了多執行緒相關的內容和其他細節。同時,這裡展示的程式碼也並非最後的結果,因為還有後續的 refactoring 使之成為 state + strategy 的混合體。
系統有一個 TPeer 類別,代表 P2P 系統中的一個類別,這個類別監聽所有的
Socket 事件,並將它交給一個 TProtocol 的物件來處理。
這裡的 dealwithDiscovery,dealwithRegist 以及 dealwithUnRegist 是前一步 refactoing 的結果。它們需要其他方法來輔助,所以在 TProtocol 類別中,包含了大量不同情況下的資料結構和相關的操作。
聞一下這裡的味道,如果你的程式出現這樣的程式碼,那麼 TProtocol 類別的長度可想而知,它做了許多不同的事情,承擔不同的責任,實現不同的行為。更麻煩的是,如果我現在想要處理一個其他的協議,如 "Test",那麼你必須找到這段程式碼,修改 if 語句,在最後的 else 之前,加上一個新的判斷:
if method = ’Test’ then begin dealwithTest(fromPeer, header, EOSXMLParser); |
顯然,這樣的結果是我們無法承受的,它至少違反了我們以下的直覺和原則:
總而言之,這是一段傳統程式碼。
實質的問題乃 if 語句判斷這些不可變的字串,實際上是在對各種類型建模,我們在物件導向的系統中,往往使用類別(Class)來實現類型(Type)。
TProtocolFactory = class private constructor create; public class function getInstance:TProtocolFactory; function CreateProtocol(name:String):TProtocol; end; { TProtocolFactory } constructor TProtocolFactory.create; begin inherited create; end; function TProtocolFactory.CreateProtocol(name: String): TProtocol; begin result := TNullProtocol.getInstance; //(Null Object,我將在其他文章中講到) end; class function TProtocolFactory.getInstance: TProtocolFactory; const fInstance:TProtocolFactory = nil; begin if (fInstance = nil) then fInstance := TProtocolFactory.create; result := fInstance; end; |
該工廠方法是一個 Singleton。其次增加新的測試該單元。
上面的第一個 if 表示 DiscoveryPeer 的處理,使用方法名加上 TProtocol,得到子類別的名稱為:
'T' + 'DiscoveryPeer' + 'Protocol'.. |
所以這個子類別的名稱為 TDiscoveryPeerProtocol,注意在進行 Refactoring 的時候,要小心地選擇你所使用的名字。
TDiscoveryPeerProtocol = class(TProtocol) end; |
TDiscoveryPeerProtocol = class(TProtocol) private //私有資料和方法 protected //保護方法 public procedure dealwithDiscovery(fromPeer:Tpeer;header:TmsgHeader; EOSXMLParser:TEOSXMLParser); end; |
TDiscoveryPeerProtocol = class(TProtocol) private //私有資料和方法 protected //保護方法 public procedure process(fromPeer:Tpeer;header:TmsgHeader; EOSXMLParser:TEOSXMLParser); end; |
原來是:
function TProtocolFactory.CreateProtocol(name: String): TProtocol; begin result := TNullProtocol.;(Null Object,我將在其他文章中講到) end; |
改為:
function TProtocolFactory.CreateProtocol(name: String): TProtocol; begin if method='DiscoveryPeer' then begin result := TDiscoveryPeerProtocol.create; end else result := TNullProtocol.getInstance; // (Null Object,我將在其他文章中講到) end; |
rocedure TProtocol.process(fromPeer : TP2PPeer;msgHeader:TMsgHeader; EOSXMLParser:TEOSXMLParser); var method:String; begin method := header.getMethod(); if method = 'DiscoveryPeer' then begin dealwithDiscovery(fromPeer, header, EOSXMLParser); end else if method='RegistPeer' then begin dealwithRegist(fromPeer, header, EOSXMLParser); end else if method='UnRegistPeer' then begin dealwithUnRegist(fromPeer, header, EOSXMLParser); end else exit; end; |
變為:
procedure TProtocol.process(fromPeer : TP2PPeer;msgHeader:TMsgHeader; EOSXMLParser:TEOSXMLParser); var method:String; begin method := header.getMethod(); if method='RegistPeer' then begin dealwithRegist(fromPeer, header, EOSXMLParser); end else if method='UnRegistPeer' then begin dealwithUnRegist(fromPeer, header, EOSXMLParser); end else exit; end; |
var aProtocol:TProtocol; aProtocol := TProtocolFactory.getInstance.CreateProtocol(header.getMethod()); aProtocol.process(fromPeer, header, EOSXMLParser); |
測試程式碼,保證原來所有的功能完全正確。
function TProtocolFactory.CreateProtocol(peer:TP2PPeer;name: String): TP2PProtocol; begin if name = 'RegistPeer' then result := TRegistPeerProtocol.create(peer) else if name = 'DiscoveryPeer' then result := TDiscoveryPeerProtocol.create(peer) else if name = 'UnReigstPeer' then result := TUnRegistPeerProtocol.Create(peer) else result := TNullProtocol.getInstance; end; |
同時, 原來 TProtocol 中 Process 方法變為:
procedure TProtocol.process(fromPeer : TP2PPeer;msgHeader:TMsgHeader; EOSXMLParser:TEOSXMLParser); var method:String; begin method := header.getMethod(); end; |
事實上,process 已經不做任何工作了,所以我們可以把它聲明為抽象:
procedure process(aThread:TThread;fromPeer:TP2PPeer; EOSXMLParse:TEOSXMLParse);virtual;abstract; |
測試。
如果你需要增加新的行為,現在所需要做的就是從 TProtocol 中繼承一個子類別,然後在 FactoryMethod 中加入新的實例過程。
現在,唯一出現 if-then-else 的地方是工廠方法,我們可以滿意於這樣的結果。但其實若使用 Delphi 的 MetaClass,則可以排除這樣的判斷結構,在 Java 中的實作則更簡單,可以參考 www.erptao.org 上的《參數化類別》。
要注意的是,如果把 client 的 TPeer 算進去,實際上很容易實現了state/strategy 樣式,我將在其他地方論述。
石一楹是一個專注於物件導向領域的系統設計師,目前的工作是在中國大陸一家 ERP 公司任職 CTO。他住在浙江杭州,有一個兩個月大的女兒。外號「石破天」(Shi Potian),可以於各論壇中發現這位(金庸筆下)大俠的影蹤。
朱子乃點空間「UML專欄」編輯,另對這篇文章負責校稿、修訂及編排等工作。