天天看點

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

點選檢視第一章 點選檢視第三章

第2章

建立型模式

本章主要介紹了建立型模式(Creational Pattern)。建立型模式主要用于處理對象的建立問題,本章主要介紹以下内容:

  • 單例模式
  • 工廠模式
  • 建造者模式
  • 原型模式
  • 對象池模式

2.1 單例模式

自Java語言推廣使用以來,單例模式(singleton pattern)就是最常用的設計模式,它具有易于了解、使用簡便等特點。有時單例模式會過度使用或在不合适的場景下使用,造成弊大于利的後果,是以,單例模式有時被認為是一種反模式。但是很多情況下單例模式是不可或缺的。

單例模式,顧名思義,用來保證一個對象隻能建立一個執行個體,除此之外,它還提供了對執行個體的全局通路方法。單例模式的實作方式如圖2-1所示。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

單例模式的實作非常簡單,隻由單個類組成。為確定單例執行個體的唯一性,所有的單例構造器都要被聲明為私有的(private),再通過聲明靜态(static)方法實作全局通路獲得該單例執行個體。實作代碼如下所示:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

當我們在代碼中使用單例對象時,隻需進行簡單的調用,代碼如下所示:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

在getInstance方法中,需要判斷執行個體是否為空。如果執行個體不為空,則表示該對象在之前已被建立;否則,用新的構造器建立它。經過這些操作,無論是哪種情況,執行個體都不再為空,可以傳回執行個體對象。

2.1.1 同步鎖單例模式

單例模式的實作代碼簡單且高效,但還需注意一種特殊情況,在多線程應用中使用這種模式,如果執行個體為空,可能存在兩個線程同時調用getInstance方法的情況。如果發生這種情況,第一個線程會首先使用新構造器執行個體化單例對象,同時第二個線程也會檢查單例執行個體是否為空,由于第一個線程還沒完成單例對象的執行個體化操作,是以第二個線程會發現這個執行個體是空的,也會開始執行個體化單例對象。

上述場景看似發生機率很小,但在執行個體化單例對象需要較長時間的情況下,發生的可能性就足夠高,這種情況往往不能忽視。

要解決這個問題很簡單,我們隻需要建立一個代碼塊來檢查執行個體是否空線程安全。可以通過以下兩種方式來實作。

  • 向getInstance方法的聲明中添加synchronized關鍵字以保證其線程安全:
帶你讀《Java設計模式及實踐》之二:建立型模式第2章
  • 用synchronized代碼塊包裝if (instance == null)條件。在這一環境中使用synch-

    ronized代碼塊時,需要指定一個對象來提供鎖,Singleton.class對象就起這種作用。如以下代碼片段所示:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

2.1.2 擁有雙重校驗鎖機制的同步鎖單例模式

前面的實作方式能夠保證線程安全,但同時帶來了延遲。用來檢查執行個體是否被建立的代碼是線程同步的,也就是說此代碼塊在同一時刻隻能被一個線程執行,但是同步鎖(locking)隻有在執行個體沒被建立的情況下才起作用。如果單例執行個體已經被建立了,那麼任何線程都能用非同步的方式擷取目前的執行個體。

隻有在單例對象未執行個體化的情況下,才能在synchronized代碼塊前添加附加條件移動線程安全鎖:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

注意到instance == null條件被檢查了兩次,因為我們需要保證在synchronized代碼塊中也要進行一次檢查。

2.1.3 無鎖的線程安全單例模式

Java中單例模式的最佳實作形式中,類隻會加載一次,通過在聲明時直接執行個體化靜态成員的方式來保證一個類隻有一個執行個體。這種實作方式避免了使用同步鎖機制和判斷執行個體是否被建立的額外檢查:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

2.1.4 提前加載和延遲加載

按照執行個體對象被建立的時機,可以将單例模式分為兩類。如果在應用開始時建立單例執行個體,就稱作提前加載單例模式;如果在getInstance方法首次被調用時才調用單例構造器,則稱作延遲加載單例模式。

前面例子中描述的無鎖線程安全單例模式在早期版本的Java中被認為是提前加載單例模式,但在最新版本的Java中,類隻有在使用時候才會被加載,是以它也是一種延遲加載模式。另外,類加載的時機主要取決于JVM的實作機制,因而版本之間會有不同。是以進行設計時,要避免與JVM的實作機制進行綁定。

目前,Java語言并沒有提供一種建立提前加載單例模式的可靠選項。如果确實需要提前執行個體化,可以在程式的開始通過調用getInstance方法強制執行,如下面代碼所示:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

2.2 工廠模式

正如前面章節所描述,在面向對象程式設計中,繼承是一個基本概念,它與多态共同構成了類的父子繼承關系(Is-A關系)。Car對象可以被當作Vehicle對象處理,Truck對象也可以被當作Vehicle對象處理。一方面,這種抽象方式使得同一段代碼能為Car和Truck對象提供同樣的處理操作,使代碼更加簡潔;另一方面,如果要擴充新的Vehicle對象類型,比如Bike或Van,不再需要修改代碼,隻需添加新的類即可。

在大多數情況下,最棘手的問題往往是對象的建立。在面向對象程式設計中,每個對象都使用特定類的構造器進行執行個體化操作,如下面代碼所示:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

這段代碼說明了Vehicle和Car兩個類之間的依賴關系。這樣的依賴關系使代碼緊密耦合,在不更改的情況下很難擴充。舉例來說,假設要用Truck替換Car,就需要修改相應的代碼:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

這裡存在兩個問題:其一,類應該保持對擴充的開放和對修改的關閉(開閉原則);其二,每個類應該隻有一個發生變化的原因(單一職責原則)。每增加新的類造成主要代碼修改時會打破開閉原則,而主類除了其固有功能之外還負責執行個體化vehicle對象,這種行為将會打破單一職責原則。

在這種情況下就需要一種更好的設計方案。我們可以增加一個新類來負責執行個體化vehicle對象,稱之為簡單工廠模式。

2.2.1 簡單工廠模式

工廠模式用于實作邏輯的封裝,并通過公共的接口提供對象的執行個體化服務,在添加新的類時隻需要做少量的修改。

簡單工廠的實作描述如圖2-2所示。

類SimpleFactory中包含執行個體化ConcreteProduct 1和ConcreteProduct 2的代碼。當

客戶需要對象時,調用SimpleFactory的createProduct()方法,并提供參數指明所需對象的類型。SimpleFactory執行個體化相應的具體産品并傳回,傳回的産品對象被轉換為基類類型。是以,無論是ConcreteProduct 1還是ConcreteProduct 2,客戶能以相同的方式處理。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

1.靜态工廠模式

下面我們寫一個簡單的工廠類用來建立Vehicle執行個體。我們建立一個抽象Vehicle類和繼承自它的三個具體類:Bike、Car和Truck。工廠類(也叫靜态工廠類)代碼如下所示:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

工廠類邏輯非常簡單,隻負責Vehicle類的執行個體化,符合單一職責原則;使用者隻調用Vehicle接口,這樣做可以減少耦合,符合依賴倒置原則;但是當增加一個新的Vehicle類時,需要對VehicleFactory類進行修改,這樣就打破了開閉原則。

我們可以改進這種簡單工廠模式,使得注冊的新類在使用時才被執行個體化,進而保證其對擴充開放,同時對修改閉合。

具體的實作方式有以下兩種:

  • 使用反射機制注冊産品類對象和執行個體化。
  • 注冊産品對象并向每個産品添加newInstance方法,該方法傳回與自身類型相同的新執行個體。

2.使用反射機制進行類注冊的簡單工廠模式

為此,我們需要使用map對象來儲存産品ID及其對應的類:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

然後,增加一個注冊新Vehicle類的方法:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

構造方法如下所示:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

但在某些情況下,反射機制并不适用。比如,反射機制需要運作時權限,這在某些特定環境中是無法實作的。反射機制也會降低程式的運作效率,在對性能要求很高的場景下應該避免使用這種機制。

3.使用newInstance方法進行類注冊的簡單工廠模式

前面的代碼中使用了反射機制來實作新Vehicle類的執行個體化。如果要避免使用反射機制,可以使用注冊新Vehicle類的類似工廠類,不再将類添加到map對象中,而是将要注冊的每種對象執行個體添加其中。每個産品類都能夠建立自己的執行個體。

首先在Vehicle基類中添加一個抽象方法:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

對于每種産品,基類中聲明為抽象的方法都要實作:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

在工廠類中,更改map用于儲存對象的ID及其對應的Vehicle對象:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

通過執行個體注冊一種新的Vehicle類型:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

也要相應地改變createVehicle方法:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

2.2.2 工廠方法模式

工廠方法模式是在靜态工廠模式上的改進。工廠類被抽象化,用于執行個體化特定産品類的代碼被轉移到實作抽象方法的子類中。這樣不需要修改就可以擴充工廠類。工廠方法模式的實作如圖2-3所示。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

下面來看一些樣例:假設有一個汽車工廠,目前隻生産兩種車型,小型跑車和大型家用車。在軟體中,顧客可以自由決定買小型車或大型車。首先,我們需要建立一個Vehicle類和兩個子類,子類分别為SportCar和SedanCar。

建立Vehicle類結構之後就可以建立抽象工廠類。要注意工廠類中并不包含任何建立新執行個體的代碼:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

為了增加汽車執行個體化的代碼,我們建立了VehicleFactory的子類,即CarFactory類,并在CarFactory中實作從父類中調用的createVehicle抽象方法。實際上,Vehicle-

Factory類将Vehicle類的具體執行個體化操作委托給了它的子類:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

在用戶端,我們隻需要建立工廠類并建立訂單:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

此時,我們意識到汽車工廠所帶來的收益,是時候進一步拓展業務了。市場調查顯示卡車的需求量很大,是以我們建一個卡車工廠(TruckFactory)。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章
帶你讀《Java設計模式及實踐》之二:建立型模式第2章

我們使用如下代碼來下訂單:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

匿名具體工廠模式

繼續在前面的代碼中添加一個BikeFactory,使得顧客可以選擇購買小型或大型自行車。這裡不用建立單獨的類檔案,隻需直接在用戶端代碼中簡單地建立一個匿名類來對VehicleFactory類進行擴充即可:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

2.2.3 抽象工廠模式

抽象工廠模式是工廠方法模式的擴充版本。它不再是建立單一類型的對象,而是建立一系列相關聯的對象。如果說工廠方法模式中隻包含一個抽象産品類,那麼抽象工廠模式則包含多個抽象産品類。

工廠方法類中隻有一個抽象方法,在不同的具體工廠類中分别實作抽象産品的執行個體化,而抽象工廠類中,每個抽象産品都有一個執行個體化方法。

如果我們采用抽象工廠模式并将它應用于包含單個對象的簇,那麼就得到了工廠方法模式。工廠方法模式隻是抽象工廠模式的一種特例。

抽象工廠設計模式的實作如圖2-4所示。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

抽象工廠模式由以下類組成:

  • AbstractFactory(抽象工廠類):抽象類,用于聲明建立不同類型産品的方法。它針對不同的抽象産品類都有對應的建立方法。
  • ConcreteFactory(具體工廠類):具體類,用于實作抽象工廠基類中聲明的方法。針對每個系列的産品都有一個對應的具體工廠類。
  • AbstracProduct(抽象産品類):對象所需的基本接口或類。一簇相關的産品類由來自不同層級的相似産品類組成。ProductA1和ProductB1來自第一個類簇,由ConcreteFactory1執行個體化。ProductA2和ProductB2來自第二個類簇,由ConcreteFactory2執行個體化。

2.2.4 簡單工廠、工廠方法與抽象工廠模式之間的對比

之前我們闡述了實作工廠模式的三種不同方式,即簡單工廠模式、工廠方法模式和抽象工廠模式。如果你目前對這三種實作方式還存在困惑,也無須自責,因為這些模式之間确實存在許多重疊的地方,況且,這些模式并不存在明确的定義,某些專家在如何實施這些模式上也存在着分歧。

本節的主旨是讓讀者了解工廠模式的核心概念。工廠模式的核心就是由工廠類來負責合适對象的建立。如果工廠類很複雜,比如同時服務于多種類型的對象或工廠,也可以根據前面内容相應的修改代碼。

2.3 建造者模式

建造者模式與其他建立型模式一樣服務于相同的目标,隻不過它出于不同的原因,通過不同的方式實作。在開發複雜的應用程式時,代碼往往會變得非常複雜。類會封裝更多的功能,類的結構也會變得更加複雜。随着功能量的增加,就需要涵蓋更多場景,進而需要建構更多不同的類。

當需要執行個體化一個複雜的類,以得到不同結構和不同内部狀态的對象時,我們可以使用不同的類對它們的執行個體化操作邏輯分别進行封裝,這些類就被稱為建造者。每當需要來自同一個類但具有不同結構的對象時,就可以通過構造另一個建造者來進行執行個體化。

它的概念不僅可以用于不同表現形式的類,還可以用于由其他對象組成的複雜對象。構造建造者類來封裝執行個體化複雜對象的邏輯,符合單一職責原則和開閉原則。實作執行個體化複雜對象的邏輯被放到了單獨的建造者類中。當需要具有不同結構的對象時,我們可以添加新的建造者類,進而實作對修改的關閉和對擴充的開放,如圖2-5所示。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

建造者模式中包含以下類:

  • Product(産品類):需要為其建構對象的類,是具有不同表現形式的複雜或複合對象。
  • Builder(抽象建造者類):用于聲明建構産品類的組成部分的抽象類或接口。它的作用是僅公開建構産品類的功能,隐藏産品類的其他功能;将産品類與建構産品類的更進階的類分離開。
  • ConcreteBuilder(具體建造者類):用于實作抽象建造者類接口中聲明的方法。除此之外,它還通過getResult方法傳回建構好的産品類。
  • Director(導演類):用于指導如何建構對象的類。在建造者模式的某些變體中,導演類已被移除,其角色被用戶端或抽象建造者類所代替。

2.3.1 汽車建造者樣例

在本節中,我們将在汽車軟體中應用建造者模式。首先,存在一個Car類,需要為它建立執行個體。通過向汽車中添加不同的元件,我們分别可以制造轎車和跑車。當開始設計軟體時,需要認識到以下幾點:

  • Car類非常複雜,建立類的對象也是一個複雜的操作。在Car類的構造函數中添加所有的執行個體化邏輯将使其變得體量龐大。
  • 我們需要建構多種類型的汽車類。針對這種情況,我們通常會添加多個不同的構造函數,但直覺告訴我們這并非最好的解決方案。
  • 将來我們可能需要建構多種不同類型的汽車對象。由于市場上對于半自動汽車的需求非常高漲,在不久的将來,我們應該做好準備進行代碼擴充而不是重新修改代碼。

為此,我們将建立以下如圖2-6所示的類結構。

CarBuilder是建造者基類,它包含了四個抽象方法。我們建立了兩個具體建造者類:ElectricCarBuilder和GasolineCarBuilder。每個建造者實作類都分别實作了CarBuilder

的所有抽象方法。那些類中不需要的方法(例如ElectricCarBuilder中的addGasTank方法)會被置空或抛出異常。ElectricCar類和GasolineCar類内部結構是不同的。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

導演類使用抽象建造者類來建立新的汽車對象。buildElectricCar和buildGasolineCar

兩個方法略有不同:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

如果想要制造一輛既有電動又有汽油發動機的混合動力汽車:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章
帶你讀《Java設計模式及實踐》之二:建立型模式第2章

2.3.2 簡化的建造者模式

在建造者模式的某些實作方式中可以移除導演類。在類例子中,導演類封裝的邏輯非常簡單,在這種情況下可以不需要導演類。簡化的建構器模式如圖2-7所示。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

我們隻是将導演類中實作的代碼移到了用戶端,但是當抽象建造者類和産品類太過複雜,或者要使用建造者類從資料流中建構對象時,我們不建議這樣修改。

2.3.3 擁有方法鍊的匿名建造者

如前所述,建構來自相同類但具有不同形式的對象的最直接方法就是建構多個構造函數,按照不同的場景進行不同的執行個體化操作。使用建造者模式避免這種情況是個不錯的實踐,在《Effective Java》一書中,Joshua Bloch建議使用内部建造者類和方法鍊來代替建構多個構造函數。

方法鍊是指通過特定方法傳回目前對象(this)的一種技術。通過這種技術,可以以鍊的形式調用方法。例如:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

在定義了更多類似上述方法之後,可以用方法鍊調用它們:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

但在我們的例子中是将Car對象的建造者類構造為内部類。在需要增加新用戶端時,可以執行以下操作:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

2.4 原型模式

原型模式看似複雜,實際上它隻是一種克隆對象的方法。現在執行個體化對象操作并不特别耗費性能,那麼為什麼還需要對象克隆呢?在以下幾種情況下,确實需要克隆那些已經經過執行個體化的對象:

  • 依賴于外部資源或硬體密集型操作進行新對象的建立的情況。
  • 擷取相同對象在相同狀态的拷貝而無須進行重複擷取狀态操作的情況。
  • 在不确定所屬具體類時需要對象的執行個體的情況。

請看如圖2-8所示的類圖。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

在原型模式中,主要涉及以下類:

  • Prototype(抽象原型類):聲明了clone()方法的接口或基類,其中clone()方法必須由派生對象實作。在簡單的場景中,并不需要這種基類,隻需要直接具體類就足夠了。
  • ConcretePrototype(具體原型類):用于實作或擴充clone()方法的類。clone()方法必須要實作,因為它傳回了類型的新執行個體。如果隻在基類中實作了clone()方法,卻沒有在具體原型類中實作,那麼當我們在具體原型類的對象上調用該方法時,會傳回一個基類的抽象原型對象。

可以在接口中聲明clone()方法,因而必須在類的實作過程中實作clone()方法,這項操作會在編譯階段強制執行。但是,在多繼承層次結構中,如果父類實作了clone()

方法,繼承自它的子類将不會強制執行clone()方法。

淺拷貝和深拷貝

拷貝對象時,我們應該清楚拷貝的深度。當拷貝的對象隻包含簡單資料類型(如int和float)或不可變的對象(如字元串)時,就直接将這些字段複制到新對象中。但當拷貝對象中包含對其他對象的引用時,這樣就會出現問題。例如,如果為具有引擎和四個輪子的Car類實作拷貝方法時,我們不僅要建立一個新的Car對象,還要建立一個新的Engine對象和四個新的Wheel對象。畢竟兩輛車不能共用相同的發動機和車輪,這稱為深拷貝。

淺拷貝是一種僅将本對象作為拷貝内容的方法。例如,如果我們要為Student對象實作拷貝方法,就不會拷貝它所指向的Course對象,因為多個Student對象會指向同一個Course對象。

在實踐中,我們應根據具體情況來決定使用深拷貝、淺拷貝或混合拷貝。通常,淺拷貝對應于聚合關系,而深拷貝則對應于組合關系。

2.5 對象池模式

對象的執行個體化是最耗費性能的操作之一,這在過去是個大問題,現在不用再過分關注它。但當我們處理封裝外部資源的對象(例如資料庫連接配接)時,對象的建立操作則會耗費很多資源。

解決方案是重用和共享這些建立成本高昂的對象,這稱為對象池模式,如圖2-9所示,它具有以下結構。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

對象池模式中使用的類如下所示:

  • ResourcePool(資源池類):用于封裝邏輯的類。用來儲存和管理資源清單。
  • Resource(資源類):用于封裝特定資源的類。資源類通常被資源池類引用,是以隻要資源池不重新配置設定,它們就永遠不會被回收。
  • Client(用戶端類):使用資源的類。

當用戶端需要新資源時,會向資源池類申請,資源池類檢查後擷取第一個可用資源并将其傳回給用戶端:

帶你讀《Java設計模式及實踐》之二:建立型模式第2章
帶你讀《Java設計模式及實踐》之二:建立型模式第2章

用戶端使用完資源後會進行釋放,資源會重新回到資源池以便重複使用。

帶你讀《Java設計模式及實踐》之二:建立型模式第2章

資源池的典型用例是資料庫連接配接池。通過維護資料庫連接配接池,可以讓代碼使用池中的不同資料庫連接配接。

2.6 總結

本章主要介紹了建立型設計模式。我們讨論了單例、工廠、建造者、原型和對象池等設計模式。這些模式都能夠實作新對象的執行個體化,提高建立對象代碼的靈活性和重用性。在下一章中,我們将介紹行為型模式。建立型模式有助于管理對象的建立操作,而行為型模式則提供了管理對象行為的簡便方法。