使用物件導向程式語言,開發者基本上都與物件為伍,建立物件並加以操作是基本動作,然而物件的運用,有賴與其他物件的合作關係,即便是物件本身在建構時,可能也有多個方式或是繼承階層問題,因此,選擇何種建構方式,得先瞭解建構物件的需求與流程為何。

例如,傳給建構式(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實例會被快取,以便後續需要同範圍內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》,也探討了使用工廠方法來簡化物件建構。

如何有效率地產生、管理、初始物件,一直都值得討論,雖然最後目的,都是要取得一個堪用或符合規格的物件,然而規格本身來說,也許就很複雜。無論是區分主要與附屬建構流程、區分物件建構與初始流程,或是決定採用建構式或工廠方法,重點都是在思考物件符合規格的過程樣貌,而不僅僅是達到取得物件的目標,就算了事。

 

作者簡介


Advertisement

更多 iThome相關內容