iThome 網站首載:從建構式到工廠方法
使用物件導向程式語言,開發者基本上都與物件為伍,建立物件並加以操作是基本動作,然而物件的生成需要時間與空間,物件的運用有賴與其他物件的合作關係,即便是物件本身在建構時可能也有多個方式或是繼承階層問題,選擇何種建構方式,得先瞭解建構物件的需求與流程為何。舉例來說,傳給建構式(Constructor)的引數是物件內部真正需要的值嗎?多個建構式真的是必須的嗎?多個建構式間是否有關聯?還是你只是將應當分別寫在建構式與工廠方法(Factory method)的流程混為一談呢?
- 區分主要與附屬建構流程
建構物件會有多種方式,依不同需求可設計不同的建構式,接收不同引數並進行不同的建構流程。以Java為例,可依參數型態與個數的不同來重載(Overload)建構式,Java的每個建構式可以是建構物件的獨立流程,如果有些建構式想以另一建構式的流程作為開始,可以在建構式開頭使用this方法呼叫,將被呼叫的建構式流程作為主要(Primary)建構流程,目前建構式接下來的流程作為附屬(Auxiliary)建構流程。
問題在於Java的建構式之間可以毫無關聯,this方法的使用並非強迫,只是作為是否重用另一建構式的選項,這會使得設計建構式時,傳入建構式的引數並非建構物件時真正必須,而只是用來計算出某些值,再用來建構物件。舉例而言,某物件內部真正需要的也許是List,然而設計了兩個建構式分別可接受String與List,其中String參數的建構式中實際上會對String進行剖析求得List,再指定給物件作為內部參考,剖析String也許是個耗費資源的動作,直接設計為建構式或許並不適當。
如果設計建構式時,思考將某個建構式作為主要建構式,其他建構式一律作為附屬建構式,並限制附屬建構式開頭一定要使用this方法呼叫某個建構式,那麼最後一定會有個附屬建構式得呼叫主要建構式,也就是無論實際建構物件時使用哪個建構式,最後一定會呼叫到主要建構式,主要建構式會成為建構物件的必經路口。加上這樣的限制可使得設計建構式時必須思考,建構一個物件究竟需要的資料有哪些?建構物件必定要執行的流程是什麼?
舉例來說,建構帳戶物件時若必要的是帳號、名稱,就可規範在主要建構式中,餘額可以有預設值0或建構時指定,因此可有個附屬建構式接受帳號、名稱與餘額,附屬建構式開頭用this方法呼叫主要建構式設定帳號、名稱,之後繼續設定傳入的餘額。由於主要建構式一定會被呼叫,所以帳號與名稱絕不會是預設值。
在考慮繼承的場合時,可限制只有子類別主要建構式可以super方法呼叫父類別建構式,子類別附屬建構式只能以this呼叫子類別中其他建構式,如此可確保子類別主要建構式為建構子物件的必經路口。
- 區分物件建構與初始流程
也許Java的建構式應該叫作初始式(Initializer),因為開發者無法決定如何新建(new)物件,Java實際上是新建物件之後,立即執行建構式中定義的初始流程,這也就是先前談到,為何在建構式中剖析String並不適當,因為Java建構式中應當進行的動作是初始物件,而不是作初始物件前的前置資料準備動作。
有些語言將新建物件與初始物件分開看待,例如Python可定義類別方法__new__來決定如何新建物件,可定義實例方法__init__來決定如何初始物件,Ruby相對應的則是類別方法new與實例方法initialize。將新建物件與初始物件分開看待,就有機會決定新建物件的條件、種類或隱藏物件實際的結構。舉例來說,若在Python中定義Singleton類別時,於__new__中檢查是否已保存Singleton實例,若無則新建若有則直接傳回,如此就可實現設計模式中的單例(Singleton)模式,此時__init__就只是用來對唯一的Singleton實例進行初始動作。
有些語言並沒有內建機制分別處理新建物件與初始物件流程,但在設計時仍可分別思考物件的新建與初始,並依語言特性採取適當實現。例如JavaScript中若定義Singleton函式,new Singleton()時會新建物件並傳入Singleton函式作為this參考對象,如果Singleton中沒有明確return,那this就會被傳回,否則就是return指定的傳回對象。如果想實現Singleton模式,可以於Singleton函式中檢查是否已保存Singleton實例,若無則return this,若有則return先前保存的Singleton實例,Singleton函式實際上對新建物件與初始物件的流程分別看待。
- 工廠方法用於複雜的物件建構與初始
JavaScript中有個有趣的現象值得觀察,由於函式可以直接呼叫,也可以前置new關鍵字將之佯裝為建構式進行呼叫,因此函式在需要作為建構式時,若忘了在接上new關鍵字,就會造成難以除錯的臭蟲(Bug),因而JavaScript中並不太鼓勵使用new關鍵字,如果使用者要建構物件,開發者會傾向在函式中封裝new操作,而函式使用者一律以函式方式呼叫以取得物件,而不用明確使用new關鍵字,採用此使用模式最有名的就是jQuery程式庫,其\$函式負有多樣化任務,其中之一就是建立包裹器(Wrapper)管理選擇器(Selector)指定選取的DOM物件。
前述模式是設計模式中工廠方法(Factory method)實現,工廠方法就是將新建物件與初始物件的流程分開看待,因此可以應付複雜的物件建構與初始過程場合。實際上像是Python的__new__以及Ruby的new,可視為實現工廠方法的內建機制;Scala語言的內建機制則是在與類別同名的object中定義apply方法作為工廠方法;在沒有內建機制的語言中就必須自行處理,例如前述的JavaScript處理方式,而Java常見處理方式就是定義靜態(static)方法。
Java中具體使用工廠方法最常見的實例之一就是Integer的valueOf,此方法不會每次都產生新的Integer實例,在預設或指定範圍內的Integer實例會被快取(Cache),以便後續需要同範圍內Integer實例時直接傳回,與單例模式的實現類似,這是控制生成物件的方式;除此之外,也可控制實作物件的抽換,像是根據工廠方法指定的選項,傳回某個子類別實例,或者是某個介面的實作物件;如果物件本身建構時的結構複雜或需要特定流程,也可使用工廠方法予以隱藏,像是Arrays的asList方法,或是我先前專欄「抽象資料型態與代數資料型態」就使用了list方法,來負責建構較複雜的List代數資料型態。
在更複雜的例子中,一個物件還必須與多個物件之間建立依賴關係,採用建構式建立依賴關係的話,會造成多參數的建構式,如果因應不同場合需求而會有不同的依賴關係,又會造成多個不同簽署(Signature)的建構式定義;如果不使用建構式而改採設值方法(Setter),那麼使用者會面對一連串依賴建立的設置流程,此時採用工廠方法,可封裝多個物件建構與依賴關係建立的流程。開放原始碼Spring框架,其核心就是將工廠方法實現至極緻的實例之一。
- 釐清取得物件的需求
何時採建構式、何時採工廠方法,在不少經典名著中都有過探討,像是《Design Patterns: Elements of Reusable Object-Oriented Software》中就談過使用工廠方法的時機,《Effective Java》的第一條就討論了考慮以靜態工廠方法取代建構式的優缺點,《Refactoring Improving the Design of Existing Code》也探討了如何使用工廠方法來簡化物件的建構。
如何有效率地產生、管理、初始物件,一直都是值得討論的課題,雖然最後目的都是要取得一個堪用或符合規格的物件,然而規格本身來說也許就很複雜。無論是區分主要與附屬建構流程、區分物件建構與初始流程,或是決定採用建構式或工廠方法,重點都是在思考物件符合規格的過程應當是什麼樣貌,而不僅僅是達到取得物件的目標就算了事。