天天看點

設計模式-建立型模式 GoF

目錄

GoF 23(分類)

OOP 七大原則

Creational Pattems

Singleton

背景

适用場景

執行個體-實作

理論-實作

與其他模式關系

Factory

與其他模式的關系

Abstract Factory

Builder

Prototype

閱讀推薦:

設計模式-簡單篇

設計模式-總篇

項目位址:https://gitee.com/zwtgit/gof23

學習網站推薦:

https://refactoringguru.cn/design-patterns/catalog

https://www.journaldev.com/1827/java-design-patterns-example-tutorial

設計模式是針對軟體設計中常見問題的工具箱, 其中的工具就是各種經過實踐驗證的解決方案。 即使你從未遇到過這些問題, 了解模式仍然非常有用, 因為它能指導你如何使用面向對象的設計原則來解決各種問題。

算法更像是菜單: 提供達成目标的明确步驟。 而模式更像是藍圖: 你可以看到最終的結果和模式的功能, 但需要自己确定實作步驟。

建立型模式:提供建立對象的機制, 增加已有代碼的靈活性和可複用性。

結構型模式:介紹如何将對象和類組裝成較大的結構, 并同時保持結構的靈活和高效。

行為模式:負責對象間的高效溝通和職責委派。

設計模式-建立型模式 GoF

建立型模式:

單例模式,工廠模式, 抽象工廠模式, 建造者模式, 原型模式

結構型模式:

擴充卡模式, 橋接模式, 裝飾模式, 組合模式, 外觀模式, 享元模式, 代理模式

行為型模式:

模闆方法模式, 指令模式, 疊代器模式, 觀察者模式, 中介者模式, 備忘錄模式, 解釋器模式, 狀态模式, 政策模式, 職責鍊模式, 通路者模式

面向對象程式設計(Object Oriented Programming,OOP)。

1、開閉原則

對擴充開放, 對修改關閉。

2、單一職責原則

每個類應該實作單一的職責,不要存在多于一個導緻類變更的原因,否則就應該把類拆分。該原則是實作高内聚、低耦合的指導方針。

3、裡氏替換原則(Liskov Substitution Principle)

任何基類可以出現的地方,子類一定可以出現。裡氏替換原則是繼承複用的基石,隻有當衍生類可以替換基類,軟體機關的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。

裡氏替換原則是對開閉原則的補充。實作開閉原則的關鍵就是抽象化。而基類與子類的繼承關系就是抽象化的具體實作,是以裡氏替換原則是對實作抽象化的具體步驟的規範。裡氏替換原則中,子類對父類的方法盡量不要重寫和重載。因為父類代表了定義好的結構,通過這個規範的接口與外界互動,子類不應該随便破壞它。

4、依賴倒轉原則(Dependence Inversion Principle)

面向接口程式設計,依賴于抽象而不依賴于具體。用到具體類時,不與具體類互動,而與具體類的上層接口互動。

5、接口隔離原則(Interface Segregation Principle)

每個接口中不存在子類用不到卻必須實作的方法,否則就要将接口拆分。使用多個隔離的接口,比使用單個接口(多個接口中的方法聚合到一個的接口)要好。

6、迪米特法則(最少知道原則)(Demeter Principle)

一個類對自己依賴的類知道的越少越好。無論被依賴的類多麼複雜,都應該将邏輯封裝在方法的内部,通過 public 方法提供給外部。這樣當被依賴的類變化時,才能最小的影響該類。

7、合成複用原則(Composite Reuse Principle)

軟體複用時,要先盡量使用組合或者聚合等關聯關系實作,其次才考慮使用繼承。即在一個新對象裡通過關聯的方式使用已有對象的一些方法和功能。

保證一個類隻有一個執行個體, 并提供一個通路該執行個體的全局節點。

政府是單例模式的一個很好的示例。 一個國家隻有一個官方政府。 不管組成政府的每個人的身份是什麼, “某政府” 這一稱謂總是鑒别那些掌權者的全局通路節點。

如果程式中的某個類對于所有用戶端隻有一個可用的執行個體,可以使用單例模式。

如果你需要更加嚴格地控制全局變量,可以使用單例模式 。

Java 核心程式庫中仍有相當多的單例示例:

<code>java.lang.Runtime#getRuntime()</code>

<code>java.awt.Desktop#getDesktop()</code>

<code>java.lang.System#getSecurityManager()</code>

基礎單例(單線程)

基礎單例(多線程)

相同的類在多線程環境中會出錯。 多線程可能會同時調用建構方法并擷取多個單例類的執行個體。

采用延遲加載的線程安全單例

為了解決這個問題, 你必須在建立首個單例對象時對線程進行同步。

Eager initialization

在預先初始化中,單例類的執行個體是在類加載時建立的,這是建立單例類的最簡單方法,但它有一個缺點,即即使用戶端應用程式可能不會使用它,也會建立執行個體。

如果您的單例類沒有使用大量資源,則可以使用這種方法。但是在大多數情況下,Singleton 類是為檔案系統、資料庫連接配接等資源建立的。除非用戶端調用該<code>getInstance</code>方法,否則我們應該避免執行個體化。此外,此方法不提供任何異常處理選項。

Static block initialization

Lazy Initialization

在單線程環境下工作正常,但是當涉及到多線程系統時,如果多個線程同時處于 if 條件内,則可能會導緻問題。它将破壞單例模式,兩個線程将獲得單例類的不同執行個體。

Thread Safe Singleton

但由于與同步方法相關的成本,它降低了性能,盡管我們隻需要它用于可能建立單獨執行個體的前幾個線程(閱讀:Java 同步)。為了每次都避免這種額外的開銷,使用了雙重檢查鎖定原則。在這種方法中,同步塊在 if 條件中使用,并進行額外檢查以確定僅建立單例類的一個執行個體。

Bill Pugh Singleton Implementation

在 Java 5 之前,java 記憶體模型有很多問題,并且在某些場景中,如果太多線程試圖同時擷取 Singleton 類的執行個體,上述方法會失敗。

請注意包含單例類執行個體的私有内部靜态類。當加載單例類時,<code>SingletonHelper</code>類不會加載到記憶體中,隻有當有人調用getInstance方法時,才會加載這個類并建立單例類執行個體。

這是 Singleton 類最廣泛使用的方法,因為它不需要同步。我在我的許多項目中都使用了這種方法,而且它也很容易了解和實作。

Using Reflection to destroy Singleton Pattern

反射可用于破壞上述所有單例實作方法。讓我們用一個示例類來看看這個。

您運作上面的測試類時,您會注意到兩個執行個體的 hashCode 不相同,這破壞了單例模式。反射非常強大,并在很多架構中使用,如 Spring 和 Hibernate。

Enum Singleton

Serialization and Singleton

有時在分布式系統中,我們需要在 Singleton 類中實作 Serializable 接口,以便我們可以将其狀态存儲在檔案系統中并在以後的某個時間點檢索它。這是一個也實作了 Serializable 接口的小型單例類。

序列化單例類的問題在于,無論何時反序列化它,它都會建立該類的一個新執行個體。讓我們用一個簡單的程式來看看它。

是以它破壞了單例模式,為了克服這種情況,我們需要做的就是提供<code>readResolve()</code>方法的實作。

外觀模式類通常可以轉換為單例模式類, 因為在大部分情況下一個外觀對象就足夠了。

如果你能将對象的所有共享狀态簡化為一個享元對象, 那麼享元模式就和單例類似了。 但這兩個模式有兩個根本性的不同。

隻會有一個單例實體, 但是享元類可以有多個實體, 各實體的内在狀态也可以不同。

單例對象可以是可變的。 享元對象是不可變的。

抽象工廠模式、 生成器模式和原型模式都可以用單例來實作。

工廠方法模式是一種建立型設計模式, 其在父類中提供一個建立對象的方法, 允許子類決定執行個體化對象的類型。

設計模式-建立型模式 GoF

調用工廠方法的代碼 (通常被稱為用戶端代碼) 無需了解不同子類傳回實際對象之間的差别。 用戶端将所有産品視為抽象的 <code>運輸</code> 。 用戶端知道所有運輸對象都提供 <code>傳遞</code>方法, 但是并不關心其具體實作方式。

當你在編寫代碼的過程中, 如果無法預知對象确切類别及其依賴關系時, 可使用工廠方法。

如果你希望使用者能擴充你軟體庫或架構的内部元件, 可使用工廠方法。

如果你希望複用現有對象來節省系統資源, 而不是每次都重新建立對象, 可使用工廠方法。

工廠方法模式在 Java 代碼中得到了廣泛使用。 當你需要在代碼中提供高層次的靈活性時, 該模式會非常實用。

java.util.Calendar、ResourceBundle 和 NumberFormat<code>getInstance()</code>方法使用工廠模式。

<code>valueOf()</code> Boolean、Integer 等包裝類中的方法。

核心 Java 程式庫中有該模式的應用:

<code>java.util.Calendar#getInstance()</code>

<code>java.util.ResourceBundle#getBundle()</code>

<code>java.text.NumberFormat#getInstance()</code>

<code>java.nio.charset.Charset#forName()</code>

<code>java.net.URLStreamHandlerFactory#createURLStreamHandler(String)</code> (根據協定傳回不同的單例對象)

<code>java.util.EnumSet#of()</code>

<code>javax.xml.bind.JAXBContext#createMarshaller()</code> 及其他類似的方法。

識别方法: 工廠方法可通過建構方法來識别, 它會建立具體類的對象, 但以抽象類型或接口的形式傳回這些對象。

生成跨平台的 GUI 元素

buttons

buttons/Button.java: 通用産品接口

buttons/HtmlButton.java: 具體産品

buttons/WindowsButton.java: 另一個具體産品

factory

factory/Dialog.java: 基礎建立者

factory/HtmlDialog.java: 具體建立者

factory/WindowsDialog.java: 另一個具體建立者

Demo.java: 用戶端代碼

OutputDemo.txt: 執行結果 (Html­Dialog)

OutputDemo.png: 執行結果 (Windows­Dialog)

設計模式-建立型模式 GoF

在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更友善地通過子類進行定制), 随後演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加複雜)。

抽象工廠模式通常基于一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。

你可以同時使用工廠方法和疊代器模式來讓子類集合傳回不同類型的疊代器, 并使得疊代器與集合相比對。

原型并不基于繼承, 是以沒有繼承的缺點。 另一方面, 原型需要對被複制對象進行複雜的初始化。 工廠方法基于繼承, 但是它不需要初始化步驟。

工廠方法是模闆方法模式的一種特殊形式。 同時, 工廠方法可以作為一個大型模闆方法中的一個步驟。

抽象工廠模式是一種建立型設計模式, 它能建立一系列相關的對象, 而無需指定其具體類。

如果代碼需要與多個不同系列的相關産品互動, 但是由于無法提前擷取相關資訊, 或者出于對未來擴充性的考慮, 你不希望代碼基于産品的具體類進行建構, 在這種情況下, 你可以使用抽象工廠。

如果你有一個基于一組抽象方法的類, 且其主要功能是以變得不明确, 那麼在這種情況下可以考慮使用抽象工廠模式。

使用示例: 抽象工廠模式在 Java 代碼中很常見。 許多架構和程式庫會将它作為擴充和自定義其标準元件的一種方式。

以下是來自核心 Java 程式庫的一些示例:

<code>javax.xml.parsers.DocumentBuilderFactory#newInstance()</code>

<code>javax.xml.transform.TransformerFactory#newInstance()</code>

<code>javax.xml.xpath.XPathFactory#newInstance()</code>

識别方法: 我們可以通過方法來識别該模式——其會傳回一個工廠對象。 接下來, 工廠将被用于建立特定的子元件。

跨平台 GUI 元件系列及其建立方式

在本例中, 按鈕和複選框将被作為産品。 它們有兩個變體: macOS 版和 Windows 版。

抽象工廠定義了用于建立按鈕和複選框的接口。 而兩個具體工廠都會傳回同一變體的兩個産品。

用戶端代碼使用抽象接口與工廠和産品進行互動。 同樣的代碼能與依賴于不同工廠對象類型的多種産品變體進行互動。

buttons: 第一個産品層次結構

buttons/Button.java

buttons/MacOSButton.java

buttons/WindowsButton.java

checkboxes: 第二個産品層次結構

checkboxes/Checkbox.java

checkboxes/MacOSCheckbox.java

checkboxes/WindowsCheckbox.java

factories

factories/GUIFactory.java: 抽象工廠

factories/MacOSFactory.java: 具體工廠 ( mac­OS)

factories/WindowsFactory.java: 具體工廠 (Windows)

app

app/Application.java: 用戶端代碼

Demo.java: 程式配置

生成器重點關注如何分步生成複雜對象。 抽象工廠專門用于生産一系列相關對象。 抽象工廠會馬上傳回産品, 生成器則允許你在擷取産品前執行一些額外構造步驟。

當隻需對用戶端代碼隐藏子系統建立對象的方式時, 你可以使用抽象工廠來代替外觀模式。

你可以将抽象工廠和橋接模式搭配使用。 如果由橋接定義的抽象隻能與特定實作合作, 這一模式搭配就非常有用。 在這種情況下, 抽象工廠可以對這些關系進行封裝, 并且對用戶端代碼隐藏其複雜性。

抽象工廠、 生成器和原型都可以用單例模式來實作。

亦稱:建造者模式、Builder

使用生成器模式可避免 “重疊構造函數 (telescopic constructor)” 的出現。

當你希望使用代碼建立不同形式的産品 (例如石頭或木頭房屋) 時, 可使用生成器模式。

使用生成器構造組合樹或其他複雜對象。

生成器在 Java 核心程式庫中得到了廣泛的應用:

<code>java.lang.StringBuilder#append()</code> ( <code>非同步</code> )

<code>java.lang.StringBuffer#append()</code> ( <code>同步</code> )

<code>java.nio.ByteBuffer#put()</code> (還有 <code>Char­Buffer</code>、 <code>Short­Buffer</code>、 <code>Int­Buffer</code>、 <code>Long­Buffer</code>、 <code>Float­Buffer</code> 和 <code>Double­Buffer</code>)

<code>javax.swing.GroupLayout.Group#addComponent()</code>

<code>java.lang.Appendable</code>的所有實作

識别方法: 生成器模式可以通過類來識别, 它擁有一個建構方法和多個配置結果對象的方法。 生成器方法通常支援鍊式程式設計 (例如 <code>someBuilder-&gt;setValueA(1)-&gt;setValueB(2)-&gt;create()</code> )。

分步制造汽車

在本例中, 生成器模式允許你分步驟地制造不同型号的汽車。

示例還展示了生成器如何使用相同的生産過程制造不同類型的産品 (汽車手冊)。

主管控制着構造順序。 它知道制造各種汽車型号需要調用的生産步驟。 它僅與汽車的通用接口進行互動。 這樣就能将不同類型的生成器傳遞給主管了。

最終結果将從生成器對象中獲得, 因為主管不知道最終産品的類型。 隻有生成器對象知道自己生成的産品是什麼。

builders

builders/Builder.java: 通用生成器接口

builders/CarBuilder.java: 汽車生成器

builders/CarManualBuilder.java: 汽車手冊生成器

cars

cars/Car.java: 汽車産品

cars/Manual.java: 手冊産品

cars/CarType.java

components

components/Engine.java: 産品特征 1

components/GPSNavigator.java: 産品特征 2

components/Transmission.java: 産品特征 3

components/TripComputer.java: 産品特征 4

director

director/Director.java: 主管控制生成器

OutputDemo.txt: 執行結果

你可以在建立複雜組合模式樹時使用生成器, 因為這可使其構造步驟以遞歸的方式運作。

你可以結合使用生成器和橋接模式: 主管類負責抽象工作, 各種不同的生成器負責實作工作。

原型模式是一種建立型設計模式, 使你能夠複制已有對象, 而又無需使代碼依賴它們所屬的類。

原型模式将克隆過程委派給被克隆的實際對象。 模式為所有支援克隆的對象聲明了一個通用接口, 該接口讓你能夠克隆對象, 同時又無需将代碼和對象所屬類耦合。 通常情況下, 這樣的接口中僅包含一個 <code>克隆</code>方法。

所有的類對 <code>克隆</code>方法的實作都非常相似。 該方法會建立一個目前類的對象, 然後将原始對象所有的成員變量值複制到建立的類中。 你甚至可以複制私有成員變量, 因為絕大部分程式設計語言都允許對象通路其同類對象的私有成員變量。

支援克隆的對象即為原型。 當你的對象有幾十個成員變量和幾百種類型時, 對其進行克隆甚至可以代替子類的構造。

其運作方式如下: 建立一系列不同類型的對象并不同的方式對其進行配置。 如果所需對象與預先配置的對象相同, 那麼你隻需克隆原型即可, 無需建立一個對象。

如果你需要複制一些對象, 同時又希望代碼獨立于這些對象所屬的具體類, 可以使用原型模式。

如果子類的差別僅在于其對象的初始化方式, 那麼你可以使用該模式來減少子類的數量。 别人建立這些子類的目的可能是為了建立特定類型的對象。

使用示例: Java 的 <code>Cloneable</code> (可克隆) 接口就是立即可用的原型模式。

任何類都可通過實作該接口來實作可被克隆的性質。

java.lang.Object#clone() (類必須實作 <code>java.lang.Cloneable</code> 接口)

識别方法: 原型可以簡單地通過 <code>clone</code>或 <code>copy</code>等方法來識别。

複制圖形

讓我們來看看在不使用标準 <code>Cloneable</code>接口的情況下如何實作原型模式。

shapes: 形狀清單

shapes/Shape.java: 通用形狀接口

shapes/Circle.java: 簡單形狀

shapes/Rectangle.java: 另一個形狀

Demo.java: 克隆示例

原型可用于儲存指令模式的曆史記錄。

大量使用組合模式和裝飾模式的設計通常可從對于原型的使用中獲益。 你可以通過該模式來複制複雜結構, 而非從零開始重新構造。

有時候原型可以作為備忘錄模式的一個簡化版本, 其條件是你需要在曆史記錄中存儲的對象的狀态比較簡單, 不需要連結其他外部資源, 或者連結可以友善地重建。