文章目錄
-
- 設計模式
- 創建型設計模式
-
- 工廠方法
-
- 示例說明
- 工廠方法模式結構
- 案例偽代碼
- 工廠方法模式適合應用
- 實現方式
- 工廠方法模式優缺點
- 與其他模式的關系
- 概念示例
- 抽象工廠
-
- 抽象工廠模式結構
- 抽象工廠模式適合應用場景
- 實現方式
- 抽象工廠模式優缺點
- 與其他模式的關系
- 代碼示例
- 生成器模式
-
- 生成器模式結構
- 生成器模式適合應用場景
- 實現方法
- 生成器模式優缺點
- 與其他模式的關系
- 概念示例
- 原型模式
-
- 原型模式結構
- 原型模式適合應用場景
- 實現方式
- 原型模式優缺點
- 與其他模式的關系
- 概念示例
- 單例模式
-
- 單例模式結構
- 單例模式適合應用場景
- 實現方式
- 單例模式優缺點
- 與其他模式的關系
- 偽代碼
- 概念示例
- 結構性模式
-
- 適配器模式
-
- 適配器模式結構
- 適配器模式適合應用場景
- 實現方式
- 適配器模式優缺點
- 與其他模式的關系
- 概念示例
- 橋接模式
-
- 橋接模式結構
- 橋接模式適合應用場景
- 實現方法
- 橋接模式優缺點
- 與其他模式的關系
- 概念示例
- 組合模式
-
- 組合模式結構
- 組合模式適合應用場景
- 實現方式
- 組合模式優缺點
- 與其他模式的關系
- 概念示例
- 裝飾模式
-
- 裝飾模式結構
- 裝飾模式適合應用場景
- 實現方法
- 裝飾模式優缺點
- 與其他模式的關系
- 概念示例
- 外觀模式
-
- 外觀模式結構
- 外觀模式適合應用場景
- 實現方式
- 外觀模式優缺點
- 與其他模式的關系
- 概念示例
- 享元模式
-
- 享元模式結構
- 享元模式適合應用場景
- 實現方式
- 享元模式優缺點
- 與其他模式的關系
- 概念示例
- 代理模式
-
- 代理模式結構
- 代理模式適合應用場景
- 實現方式
- 代理模式優缺點
- 與其他模式的關系
- 概念示例
- 行為模式
-
- 責任鏈模式
-
- 責任鏈模式結構
- 責任鏈模式適合應用場景
- 實現方式
- 與其他模式的關系
- 代碼示例
- 命令模式
-
- 命令模式結構
- 命令模式適合應用場景
- 實現方式
- 命令模式優缺點
- 與其他模式的關系
- 概念示例
- 迭代器模式
-
- 迭代器模式結構
- 迭代器模式適合應用場景
- 實現方式
- 迭代器模式優缺點
- 與其他模式的關系
- 概念示例
- 中介者模式
-
- 中介者模式結構
- 中介者模式適合應用場景
- 實現方式
- 中介者模式優缺點
- 與其他模式的關系
- 概念示例
- 備忘錄模式
-
- 備忘錄模式結構
- 備忘錄模式適合應用場景
- 實現方式
- 備忘錄模式優缺點
- 與其他模式的關系
- 概念示例
- 觀察者模式
-
- 觀察者模式結構
- 觀察者模式適合應用場景
- 實現方式
- 觀察者模式優缺點
- 與其他模式的關系
- 概念示例
- 狀態模式
-
- 狀態模式結構
- 狀態模式適合應用場景
- 實現方式
- 狀態模式優缺點
- 與其他模式的關系
- 概念示例
- 策略模式
-
- 策略模式結構
- 策略模式適合應用場景
- 實現方式
- 策略模式優缺點
- 與其他模式的關系
- 概念示例
- 模板方法模式
-
- 模板方法模式結構
- 模板方法模式適合應用場景
- 實現方式
- 模板方法模式優缺點
- 與其他模式的關系
- 概念示例
- 訪問者模式
-
- 訪問者模式結構
- 訪問者模式適合應用場景
- 實現方式
- 訪問者模式優缺點
- 與其他模式的關系
- 概念示例
- End
設計模式
為什么我們需要了解設計模式?
- **輕松應對面試和審核。**關于模式的問題總是一再出現。
- 獲得你應得的加薪和升職!
- 擴展你的工具箱,在其中添加經過驗證且沒有問題的代碼示例。
- 定制現成的解決方案。不要重新開發所有內容。
- 更好地與同事溝通,無需冗長的解釋。
- 只需指出設計模式的名稱,你就能輕松解決問題!
什么是設計模式?
- 設計模式是軟件設計中常見問題的典型解決方案。 每個模式就像一張藍圖, 你可以通過對其進行定制來解決代碼中的特定設計問題。
- 設計模式與方法或庫的使用方式不同, 你很難直接在自己的程序中套用某個設計模式。 模式并不是一段特定的代碼, 而是解決特定問題的一般性概念。 你可以根據模式來實現符合自己程序實際所需的解決方案。
- 人們常常會混淆模式和算法, 因為兩者在概念上都是已知特定問題的典型解決方案。 但算法總是明確定義達成特定目標所需的一系列步驟, 而模式則是對解決方案的更高層次描述。 同一模式在兩個不同程序中的實現代碼可能會不一樣。
- 算法更像是菜譜: 提供達成目標的明確步驟。 而模式更像是藍圖: 你可以看到最終的結果和模式的功能, 但需要自己確定實現步驟。
模式包含哪些內容?
大部分模式都有正規的描述方式, 以便在不同情況下使用。 模式的描述通常會包括以下部分:
- 意圖部分簡單描述問題和解決方案。
- 動機部分將進一步解釋問題并說明模式會如何提供解決方案。
- 結構部分展示模式的每個部分和它們之間的關系。
- 在不同語言中的實現提供流行編程語言的代碼, 讓讀者更好地理解模式背后的思想。
部分模式介紹中還列出其他的一些實用細節, 例如模式的適用性、 實現步驟以及與其他模式的關系。
為什么以及如何學習設計模式?
- 或許你已從事程序開發工作多年, 卻完全不知道單例模式是什么。 很多人都是這樣。 即便如此, 你可能也在不自知的情況下已經使用過一些設計模式了。 所以為什么不花些時間來更進一步學習它們呢?
- 設計模式是針對軟件設計中常見問題的工具箱, 其中的工具就是各種經過實踐驗證的解決方案。 即使你從未遇到過這些問題, 了解模式仍然非常有用, 因為它能指導你如何使用面向對象的設計原則來解決各種問題。
- 設計模式定義了一種讓你和團隊成員能夠更高效溝通的通用語言。 你只需說 “哦, 這里用單例就可以了”, 所有人都會理解這條建議背后的想法。 只要知曉模式及其名稱, 你就無需解釋什么是單例。
設計模式分類
不同設計模式的復雜程度、 細節層次以及在整個系統中的應用范圍等方面各不相同。 我喜歡將其類比于道路的建造: 如果你希望讓十字路口更加安全, 那么可以安裝一些交通信號燈, 或者修建包含行人地下通道在內的多層互通式立交橋。
最基礎的、 底層的模式通常被稱為慣用技巧。 這類模式一般只能在一種編程語言中使用。
最通用的、 高層的模式是構架模式。 開發者可以在任何編程語言中使用這類模式。 與其他模式不同, 它們可用于整個應用程序的架構設計。
此外, 所有模式可以根據其意圖或目的來分類。 本書覆蓋了三種主要的模式類別:
- 創建型模式提供創建對象的機制, 增加已有代碼的靈活性和可復用性。
- 結構型模式介紹如何將對象和類組裝成較大的結構, 并同時保持結構的靈活和高效。
- 行為模式負責對象間的高效溝通和職責委派。
設計模式的優勢
- 模式是針對軟件設計中常見問題的解決方案工具箱, 它們定義了一種讓你的團隊能更高效溝通的通用語言。
創建型設計模式
這類模式提供創建對象的機制, 能夠提升已有代碼的靈活性和可復用性。
1、工廠方法模式
- 在父類中提供一個創建對象的方法, 允許子類決定實例化對象的類型。
2、抽象工廠模式
- 讓你能創建一系列相關的對象,而無需指定其具體類。
3、生成器模式
- 使你能夠分步驟創建復雜對象。該模式允許你使用相同的創建代碼生成不同類型和形式的對象。
4、原型模式
- 讓你能夠復制已有的對象,而又無需使代碼依賴它們所屬的類。
5、單例模式
- 讓你能夠保證一個類只有一個實例,并提供一個訪問該實例的全局節點。
工廠方法
Factory Method
亦稱:虛擬構造函數、Virtual Constructor、Factory Method
示例說明
意圖:
-
工廠方法模式是一種創建型設計模式, 其在父類中提供一個創建對象的方法, 允許子類決定實例化對象的類型。
-
物流 Logistics
路上物流 RoadLogistics
海上物流 SEALogistics
問題:
-
假設你正在開發一款物流管理應用。 最初版本只能處理卡車運輸, 因此大部分代碼都在位于名為
卡車
的類中。 -
一段時間后, 這款應用變得極受歡迎。 你每天都能收到十幾次來自海運公司的請求, 希望應用能夠支持海上物流功能。
-
如果代碼其余部分與現有類已經存在耦合關系, 那么向程序中添加新類其實并沒有那么容易。如:
- 卡車、輪船 一些功能具有耦合關系
-
這可是個好消息。 但是代碼問題該如何處理呢? 目前, 大部分代碼都與
卡車
類相關。 在程序中添加輪船
類需要修改全部代碼。 更糟糕的是, 如果你以后需要在程序中支持另外一種運輸方式, 很可能需要再次對這些代碼進行大幅修改。 -
最后, 你將不得不編寫繁復的代碼, 根據不同的運輸對象類, 在應用中進行不同的處理。
解決方案:
-
工廠方法模式建議使用特殊的工廠方法代替對于對象構造函數的直接調用 (即使用
new
運算符)。 不用擔心, 對象仍將通過new
運算符創建, 只是該運算符改在工廠方法中調用罷了。 工廠方法返回的對象通常被稱作 “產品”。-
子類可以修改工廠方法返回的對象類型。如:計劃交付+創建運輸
-
// 基類 Logistics: planDeliver()createTransport()// 繼承 Logistics RoadLogistics: createTransport() return new Truck()// 繼承 Logistics SEA Logistics: createTransport() return new Ship()
-
-
-
乍看之下,這種更改可能毫無意義:我們只是改變了程序中調用構造函數的位置而已。但是,仔細想一下,現在你可以在子類中重寫工廠方法,從而改變其創建產品的類型。
-
但有一點需要注意:僅當這些產品具有共同的基類或者接口時,子類才能返回不同類型的產品,同時基類中的工廠方法還應將其返回類型聲明為這一共有接口。
-
所有產品都必須使用同一接口。
-
// 基類 <<interface>> Transportdeliver()// 繼承 Transport Truck...deliver()// 繼承 Transport Ship...deliver()
-
-
-
舉例來說,
卡車Truck
和輪船Ship
類都必須實現運輸Transport
接口, 該接口聲明了一個名為deliver
交付的方法。 每個類都將以不同的方式實現該方法:卡車走陸路交付貨物, 輪船走海路交付貨物。陸路運輸Road-Logistics
類中的工廠方法返回卡車對象, 而海路運輸Sea-Logistics
類則返回輪船對象。-
只要產品類實現一個共同的接口, 你就可以將其對象傳遞給客戶代碼, 而無需提供額外數據。
-
-
調用工廠方法的代碼(通常被稱為客戶端代碼)無需了解不同子類返回實際對象之間的差別。 客戶端將所有產品視為抽象的
運輸
。客戶端知道所有運輸對象都提供交付
方法, 但是并不關心其具體實現方式。
工廠方法模式結構
-
1、產品 (Product) 將會對接口進行聲明。 對于所有由創建者及其子類構建的對象, 這些接口都是通用的。
-
<<interface>> ProductdoStuff()
-
-
2、具體產品 (Concrete Products) 是產品接口的不同實現。
-
ConcreateProductAConcreateProductB
-
-
3、創建者 (Creator) 類聲明返回產品對象的工廠方法。 該方法的返回對象類型必須與產品接口相匹配。
-
你可以將工廠方法聲明為抽象方法, 強制要求每個子類以不同方式實現該方法。 或者, 你也可以在基礎工廠方法中返回默認產品類型。
-
注意, 盡管它的名字是創建者, 但它最主要的職責并不是創建產品。 一般來說, 創建者類包含一些與產品相關的核心業務邏輯。 工廠方法將這些邏輯處理從具體產品類中分離出來。 打個比方, 大型軟件開發公司擁有程序員培訓部門。 但是, 這些公司的主要工作還是編寫代碼, 而非生產程序員。
-
Product p = createProduct() p.doStuff()creator...someOperation()createProduction():Product
-
-
4、具體創建者 (Concrete Creators) 將會重寫基礎工廠方法, 使其返回不同類型的產品。
-
注意, 并不一定每次調用工廠方法都會創建新的實例。 工廠方法也可以返回緩存、 對象池或其他來源的已有對象。
-
ConcreateCreatorA...createProduct(): Productreturn new ConcreateCreatorA()ConcreateCreatorA...createProduct(): Product
-
案例偽代碼
以下示例演示了如何使用工廠方法開發跨平臺 UI (用戶界面) 組件, 并同時避免客戶代碼與具體 UI 類之間的耦合。
-
### 執行 Button okButton = createButton() okButton.onClick(closeDialog) okButton.render()<<interface>> Buttonreader()onClick()## WindowsButton ## HTMLButtonDialog...render()createButton(): ButtonWindowsDialog...createButton(): Buttonreturn new WindowsButton()WebDialog...createButton(): Button
工廠方法模式適合應用
當你在編寫代碼的過程中, 如果無法預知對象確切類別及其依賴關系時, 可使用工廠方法。
- 工廠方法將創建產品的代碼與實際使用產品的代碼分離, 從而能在不影響其他代碼的情況下擴展產品創建部分代碼。
- 例如, 如果需要向應用中添加一種新產品, 你只需要開發新的創建者子類, 然后重寫其工廠方法即可。
如果你希望用戶能擴展你軟件庫或框架的內部組件, 可使用工廠方法。
- 繼承可能是擴展軟件庫或框架默認行為的最簡單方法。 但是當你使用子類替代標準組件時, 框架如何辨識出該子類?
- 解決方案是將各框架中構造組件的代碼集中到單個工廠方法中, 并在繼承該組件之外允許任何人對該方法進行重寫。
- 讓我們看看具體是如何實現的。 假設你使用開源 UI 框架編寫自己的應用。 你希望在應用中使用圓形按鈕, 但是原框架僅支持矩形按鈕。 你可以使用
圓形按鈕
Round-Button子類來繼承標準的按鈕
Button類。 但是, 你需要告訴UI框架
UIFramework類使用新的子類按鈕代替默認按鈕。 - 為了實現這個功能, 你可以根據基礎框架類開發子類
圓形按鈕 UI
UIWith-Round-Buttons , 并且重寫其create-Button
創建按鈕方法。 基類中的該方法返回按鈕
對象, 而你開發的子類返回圓形按鈕
對象。 現在, 你就可以使用圓形按鈕 UI
類代替UI框架
類。 就是這么簡單!
如果你希望復用現有對象來節省系統資源, 而不是每次都重新創建對象, 可使用工廠方法。
- 在處理大型資源密集型對象 (比如數據庫連接、 文件系統和網絡資源) 時, 你會經常碰到這種資源需求。
- 讓我們思考復用現有對象的方法:
- 首先, 你需要創建存儲空間來存放所有已經創建的對象。
- 當他人請求一個對象時, 程序將在對象池中搜索可用對象。
- … 然后將其返回給客戶端代碼。
- 如果沒有可用對象, 程序則創建一個新對象 (并將其添加到對象池中)。
- 這些代碼可不少! 而且它們必須位于同一處, 這樣才能確保重復代碼不會污染程序。
- 可能最顯而易見, 也是最方便的方式, 就是將這些代碼放置在我們試圖重用的對象類的構造函數中。 但是從定義上來講, 構造函數始終返回的是新對象, 其無法返回現有實例。
- 因此, 你需要有一個既能夠創建新對象, 又可以重用現有對象的普通方法。 這聽上去和工廠方法非常相像。
- 讓我們思考復用現有對象的方法:
實現方式
-
讓所有產品都遵循同一接口。 該接口必須聲明對所有產品都有意義的方法。
-
在創建類中添加一個空的工廠方法。 該方法的返回類型必須遵循通用的產品接口。
-
在創建者代碼中找到對于產品構造函數的所有引用。 將它們依次替換為對于工廠方法的調用, 同時將創建產品的代碼移入工廠方法。
你可能需要在工廠方法中添加臨時參數來控制返回的產品類型。
工廠方法的代碼看上去可能非常糟糕。 其中可能會有復雜的
switch
分支運算符, 用于選擇各種需要實例化的產品類。 但是不要擔心, 我們很快就會修復這個問題。 -
現在, 為工廠方法中的每種產品編寫一個創建者子類, 然后在子類中重寫工廠方法, 并將基本方法中的相關創建代碼移動到工廠方法中。
-
如果應用中的產品類型太多, 那么為每個產品創建子類并無太大必要, 這時你也可以在子類中復用基類中的控制參數。
例如, 設想你有以下一些層次結構的類。 基類
郵件
及其子類航空郵件
和陸路郵件
;運輸
及其子類飛機
,卡車
和火車
。航空郵件
僅使用飛機
對象, 而陸路郵件
則會同時使用卡車
和火車
對象。 你可以編寫一個新的子類 (例如火車郵件
) 來處理這兩種情況, 但是還有其他可選的方案。 客戶端代碼可以給陸路郵件
類傳遞一個參數, 用于控制其希望獲得的產品。 -
如果代碼經過上述移動后, 基礎工廠方法中已經沒有任何代碼, 你可以將其轉變為抽象類。 如果基礎工廠方法中還有其他語句, 你可以將其設置為該方法的默認行為。
工廠方法模式優缺點
優點:
- 你可以避免創建者和具體產品之間的緊密耦合。
- 單一職責原則。你可以將產品創建代碼放在程序的單一位置, 從而使得代碼更容易維護。
- 開閉原則。無需更改現有客戶端代碼, 你就可以在程序中引入新的產品類型。
缺點:
- 應用工廠方法模式需要引入許多新的子類,代碼可能會因此變得更復雜。 最好的情況是將該模式引入創建者類的現有層次結構中。
與其他模式的關系
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定制), 隨后演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加復雜)。
- 抽象工廠模式通常基于一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。
- 你可以同時使用工廠方法和迭代器模式來讓子類集合返回不同類型的迭代器, 并使得迭代器與集合相匹配。
- 原型并不基于繼承, 因此沒有繼承的缺點。 另一方面, 原型需要對被復制對象進行復雜的初始化。 工廠方法基于繼承, 但是它不需要初始化步驟。
- 工廠方法是模板方法模式的一種特殊形式。 同時, 工廠方法可以作為一個大型模板方法中的一個步驟。
概念示例
使用示例: 工廠方法模式在 C++ 代碼中得到了廣泛使用。 當你需要在代碼中提供高層次的靈活性時, 該模式會非常實用。
識別方法: 工廠方法可通過構建方法來識別, 它會創建具體類的對象, 但以抽象類型或接口的形式返回這些對象。
本例說明了工廠方法設計模式的結構并重點回答了下面的問題:
- 它由哪些類組成?
- 這些類扮演了哪些角色?
- 模式中的各個元素會以何種方式相互關聯?
/*** Product 接口聲明了所有具體產品必須實現的操作。*/class Product {public:virtual ~Product() {}virtual std::string Operation() const = 0;
};/*** 具體產品提供了產品接口的各種實現。*/
class ConcreteProduct1 : public Product {public:std::string Operation() const override {return "{Result of the ConcreteProduct1}";}
};
class ConcreteProduct2 : public Product {public:std::string Operation() const override {return "{Result of the ConcreteProduct2}";}
};/*** Creator 類聲明了一個工廠方法,該方法應該返回一個 Product 類的對象。* Creator 的子類通常提供該方法的實現。*/class Creator {/*** Note that the Creator may also provide some default implementation of the* factory method.*/public:virtual ~Creator(){};virtual Product* FactoryMethod() const = 0;/*** 另請注意,盡管名稱如此,Creator 的主要職責并非創建產品。*通常,它包含一些依賴于工廠方法返回的 Product 對象的核心業務邏輯。*子類可以通過重寫工廠方法并返回不同類型的產品來間接更改該業務邏輯。*/std::string SomeOperation() const {// Call the factory method to create a Product object.Product* product = this->FactoryMethod();// Now, use the product.std::string result = "Creator: The same creator's code has just worked with " + product->Operation();delete product;return result;}
};/**
*具體創建者會重寫工廠方法來更改最終產品的類型。
*/
class ConcreteCreator1 : public Creator {/*** 請注意,即使該方法實際返回的是具體產品,該方法的簽名仍然使用抽象產品類型。* 這樣,Creator 就可以獨立于具體產品類。*/public:Product* FactoryMethod() const override {return new ConcreteProduct1();}
};class ConcreteCreator2 : public Creator {public:Product* FactoryMethod() const override {return new ConcreteProduct2();}
};/**
*客戶端代碼可以通過其基接口與具體創建者的實例交互。
*只要客戶端繼續通過基接口與創建者交互,就可以將任何創建者的子類傳遞給它。
*/
void ClientCode(const Creator& creator) {// ...std::cout << "Client: I'm not aware of the creator's class, but it still works.\n"<< creator.SomeOperation() << std::endl;// ...
}/*** 應用程序根據配置或環境選擇創建者的類型。*/int main() {std::cout << "App: Launched with the ConcreteCreator1.\n";Creator* creator = new ConcreteCreator1();ClientCode(*creator);std::cout << std::endl;std::cout << "App: Launched with the ConcreteCreator2.\n";Creator* creator2 = new ConcreteCreator2();ClientCode(*creator2);delete creator;delete creator2;return 0;
}
抽象工廠
亦稱:Abstract Factory
意圖:
-
抽象工廠模式是一種創建型設計模式, 它能創建一系列相關的對象, 而無需指定其具體類。
-
抽象工廠
球形工廠
金字塔工廠
問題:
-
假設你正在開發一款家具商店模擬器。 你的代碼中包括一些類, 用于表示:
- 一系列相關產品, 例如
椅子Chair
、沙發ofa
和咖啡桌Coffee-Table
。 - 系列產品的不同變體。 例如, 你可以使用
現代Modern
、維多利亞Victorian
、裝飾風藝術Art-Deco
等風格生成椅子
、沙發
和咖啡桌
。
- 一系列相關產品, 例如
-
你需要設法單獨生成每件家具對象, 這樣才能確保其風格一致。 如果顧客收到的家具風格不一樣, 他們可不會開心。
-
如:
- 你好,我上周訂購了一些椅子,現在我還需要一款沙發
- 嗯… 看上去有些不對勁
- 現代風格的沙發和維多利亞風格的椅子不搭。
-
-
此外, 你也不希望在添加新產品或新風格時修改已有代碼。 家具供應商對于產品目錄的更新非常頻繁, 你不會想在每次更新時都去修改核心代碼的。
解決方案
-
首先, 抽象工廠模式建議為系列中的每件產品明確聲明接口 (例如椅子、 沙發或咖啡桌)。 然后, 確保所有產品變體都繼承這些接口。 例如, 所有風格的椅子都實現
椅子
接口; 所有風格的咖啡桌都實現咖啡桌
接口, 以此類推。-
每個產品申明接口
同一對象的所有變體都必須放置在同一個類層次結構之中。如:
-
<<interface>> ChairhasLegs()sitOn()VictorianChairhasLegs()sitOn()ModernChairhasLegs()sitOn()
-
-
-
接下來, 我們需要聲明抽象工廠——包含系列中所有產品構造方法的接口。 例如
create-Chair創建椅子
、create-Sofa創建沙發
和create-Coffee-Table 創建咖啡桌
。這些方法必須返回抽象產品類型, 即我們之前抽取的那些接口:椅子
,沙發
和咖啡桌
等等。-
每個具體工廠類都對應一個特定的產品變體。如:
-
<<interface>> furnitureFactorycreateChair(): ChaircreateCoffeeTable(): CoffeeTablecreateSofa(): SofaVictorianFurnitureFactorycreateChair(): ChaircreateCoffeeTable(): CoffeeTablecreateSofa(): SofaModernFurnitureFactorycreateChair(): ChaircreateCoffeeTable(): CoffeeTablecreateSofa(): Sofa
-
-
-
那么該如何處理產品變體呢? 對于系列產品的每個變體, 我們都將基于
抽象工廠
接口創建不同的工廠類。- 每個工廠類都只能返回特定類別的產品, 例如,現代家具工廠
Modern-Furniture-Factory
只能創建 現代椅子Modern-Chair
、現代沙發Modern-Sofa和
現代咖啡桌Modern-Coffee-Table
對象。 - 客戶端無需了解其所調用工廠的具體類信息。
- 每個工廠類都只能返回特定類別的產品, 例如,現代家具工廠
-
假設客戶端想要工廠創建一把椅子。 客戶端無需了解工廠類, 也不用管工廠類創建出的椅子類型。 無論是現代風格, 還是維多利亞風格的椅子, 對于客戶端來說沒有分別, 它只需調用抽象
椅子
接口就可以了。 這樣一來, 客戶端只需知道椅子以某種方式實現了sit-On
坐下方法就足夠了。 此外, 無論工廠返回的是何種椅子變體, 它都會和由同一工廠對象創建的沙發或咖啡桌風格一致。 -
最后一點說明: 如果客戶端僅接觸抽象接口, 那么誰來創建實際的工廠對象呢? 一般情況下, 應用程序會在初始化階段創建具體工廠對象。 而在此之前, 應用程序必須根據配置文件或環境設定選擇工廠類別。
抽象工廠模式結構
-
1、抽象產品(Abstract Product) 為構成系列產品的一組不同但相關的產品聲明接口。
-
AbstractProductAAbstractProductB
-
-
2、具體產品(Concrete Product) 是抽象產品的多種不同類型實現。 所有變體(維多利亞/現代) 都必須實現相應的抽象產品(椅子/沙發)。
-
ConcreateProductA1 ConcreateProductA2ConcreateProductB1 ConcreateProductB2
-
-
3、抽象工廠 (Abstract Factory) 接口聲明了一組創建各種抽象產品的方法。
-
<<interface>> AbstractFactory...createProductA(): ProductAcreateProductB(): ProductB
-
-
4、具體工廠 (Concrete Factory) 實現抽象工廠的構建方法。 每個具體工廠都對應特定產品變體, 且僅創建此種產品變體。
-
ConcreateFactory1...createProductA(): ProductAcreateProductB(): ProductBConcreateFactory2...createProductA(): ProductAcreateProductB(): ProductB
-
-
5、盡管具體工廠會對具體產品進行初始化, 其構建方法簽名必須返回相應的抽象產品。 這樣, 使用工廠類的客戶端代碼就不會與工廠創建的特定產品變體耦合。客戶端**(Client) 只需通過抽象接口調用工廠和產品對象, 就能與任何具體工廠/產品變體交互。
-
Clientfactory: AbstractFactory //具體產品的抽象+Client(f:AbstractFactory)+someOperation()
-
抽象工廠模式適合應用場景
- 如果代碼需要與多個不同系列的相關產品交互, 但是由于無法提前獲取相關信息, 或者出于對未來擴展性的考慮, 你不希望代碼基于產品的具體類進行構建, 在這種情況下, 你可以使用抽象工廠。
- 抽象工廠為你提供了一個接口, 可用于創建每個系列產品的對象。 只要代碼通過該接口創建對象, 那么你就不會生成與應用程序已生成的產品類型不一致的產品。
- 如果你有一個基于一組抽象方法的類, 且其主要功能因此變得不明確, 那么在這種情況下可以考慮使用抽象工廠模式。
- 在設計良好的程序中, 每個類僅負責一件事。 如果一個類與多種類型產品交互, 就可以考慮將工廠方法抽取到獨立的工廠類或具備完整功能的抽象工廠類中。
實現方式
- 以不同的產品類型與產品變體為維度繪制矩陣。
- 為所有產品聲明抽象產品接口。 然后讓所有具體產品類實現這些接口。
- 聲明抽象工廠接口, 并且在接口中為所有抽象產品提供一組構建方法。
- 為每種產品變體實現一個具體工廠類。
- 在應用程序中開發初始化代碼。 該代碼根據應用程序配置或當前環境, 對特定具體工廠類進行初始化。 然后將該工廠對象傳遞給所有需要創建產品的類。
- 找出代碼中所有對產品構造函數的直接調用, 將其替換為對工廠對象中相應構建方法的調用。
抽象工廠模式優缺點
優點:
- 你可以確保同一工廠生成的產品相互匹配。
- 你可以避免客戶端和具體產品代碼的耦合。
- 單一職責原則。 你可以將產品生成代碼抽取到同一位置, 使得代碼易于維護。
- 開閉原則。 向應用程序中引入新產品變體時, 你無需修改客戶端代碼。
缺點:
- 由于采用該模式需要向應用中引入眾多接口和類, 代碼可能會比之前更加復雜。
與其他模式的關系
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定制), 隨后演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加復雜)。
- 生成器重點關注如何分步生成復雜對象。 抽象工廠專門用于生產一系列相關對象。 抽象工廠會馬上返回產品, 生成器則允許你在獲取產品前執行一些額外構造步驟。
- 抽象工廠模式通常基于一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。
- 當只需對客戶端代碼隱藏子系統創建對象的方式時, 你可以使用抽象工廠來代替外觀模式。
- 你可以將抽象工廠和橋接模式搭配使用。 如果由橋接定義的抽象只能與特定實現合作, 這一模式搭配就非常有用。 在這種情況下, 抽象工廠可以對這些關系進行封裝, 并且對客戶端代碼隱藏其復雜性。
- 抽象工廠、 生成器和原型都可以用單例模式來實現。
代碼示例
使用示例: 抽象工廠模式在 C++ 代碼中很常見。 許多框架和程序庫會將它作為擴展和自定義其標準組件的一種方式。
識別方法: 我們可以通過方法來識別該模式——其會返回一個工廠對象。 接下來, 工廠將被用于創建特定的子組件。
本例說明了抽象工廠設計模式的結構并重點回答了下面的問題:
- 它由哪些類組成?
- 這些類扮演了哪些角色?
- 模式中的各個元素會以何種方式相互關聯?
/*** 產品系列中的每個不同產品都應具有一個基類接口。該產品的所有變體都必須實現此接口。*/
class AbstractProductA {public:virtual ~AbstractProductA(){};virtual std::string UsefulFunctionA() const = 0;
};/*** 具體產品是由相應的具體工廠創建的。*/
class ConcreteProductA1 : public AbstractProductA {public:std::string UsefulFunctionA() const override {return "The result of the product A1.";}
};class ConcreteProductA2 : public AbstractProductA {std::string UsefulFunctionA() const override {return "The result of the product A2.";}
};/*** 這是另一個產品的基礎接口。所有產品都可以相互交互,但只有同一種具體變體的產品之間才能進行正常的交互。*/
class AbstractProductB {/*** 產品 B 能夠做自己的事情......*/public:virtual ~AbstractProductB(){};virtual std::string UsefulFunctionB() const = 0;/*** ...但它也可以與 ProductA 協作。 * * 抽象工廠確保其創建的所有產品都屬于同一變體,因此兼容。*/virtual std::string AnotherUsefulFunctionB(const AbstractProductA &collaborator) const = 0;
};/*** 具體產品是由相應的具體工廠創建的。*/
class ConcreteProductB1 : public AbstractProductB {public:std::string UsefulFunctionB() const override {return "The result of the product B1.";}/*** 變體 Product B1 只能與變體 Product A1 正確配合使用。盡管如此,它仍然接受 AbstractProductA 的任何實例作為參數。*/std::string AnotherUsefulFunctionB(const AbstractProductA &collaborator) const override {const std::string result = collaborator.UsefulFunctionA();return "The result of the B1 collaborating with ( " + result + " )";}
};class ConcreteProductB2 : public AbstractProductB {public:std::string UsefulFunctionB() const override {return "The result of the product B2.";}/*** 變體 Product B2 只能與變體 Product A2 正確配合使用。盡管如此,它仍然接受 AbstractProductA 的任何實例作為參數。*/std::string AnotherUsefulFunctionB(const AbstractProductA &collaborator) const override {const std::string result = collaborator.UsefulFunctionA();return "The result of the B2 collaborating with ( " + result + " )";}
};/*** 抽象工廠接口聲明了一組返回不同抽象產品的方法。這些產品被稱為一個系列,它們通過一個高級主題或概念相互關聯。同一系列的產品通常能夠相互協作。一個產品系列可能有多個變體,但其中一個變體的產品與另一個變體的產品不兼容。*/
class AbstractFactory {public:virtual AbstractProductA *CreateProductA() const = 0;virtual AbstractProductB *CreateProductB() const = 0;
};/*** 具體工廠會生產屬于同一變體的一系列產品。工廠保證生成的產品兼容。注意:具體工廠方法的簽名返回的是抽象產品,而方法內部則會實例化一個具體產品。*/
class ConcreteFactory1 : public AbstractFactory {public:AbstractProductA *CreateProductA() const override {return new ConcreteProductA1();}AbstractProductB *CreateProductB() const override {return new ConcreteProductB1();}
};/*** 每個具體工廠都有一個對應的產品變體。*/
class ConcreteFactory2 : public AbstractFactory {public:AbstractProductA *CreateProductA() const override {return new ConcreteProductA2();}AbstractProductB *CreateProductB() const override {return new ConcreteProductB2();}
};/*** 客戶端代碼僅通過抽象類型:AbstractFactory和AbstractProduct與工廠和產品交互。這使得您可以將任何工廠或產品子類傳遞給客戶端代碼而不會破壞它。*/void ClientCode(const AbstractFactory &factory) {const AbstractProductA *product_a = factory.CreateProductA();const AbstractProductB *product_b = factory.CreateProductB();std::cout << product_b->UsefulFunctionB() << "\n";std::cout << product_b->AnotherUsefulFunctionB(*product_a) << "\n";delete product_a;delete product_b;
}int main() {std::cout << "Client: Testing client code with the first factory type:\n";ConcreteFactory1 *f1 = new ConcreteFactory1();ClientCode(*f1);delete f1;std::cout << std::endl;std::cout << "Client: Testing the same client code with the second factory type:\n";ConcreteFactory2 *f2 = new ConcreteFactory2();ClientCode(*f2);delete f2;return 0;
}
生成器模式
**亦稱:**建造者模式、Builder
意圖:
- 生成器模式是一種創建型設計模式, 使你能夠分步驟創建復雜對象。 該模式允許你使用相同的創建代碼生成不同類型和形式的對象。
問題:
-
假設有這樣一個復雜對象, 在對其進行構造時需要對諸多成員變量和嵌套對象進行繁復的初始化工作。 這些初始化代碼通常深藏于一個包含眾多參數且讓人基本看不懂的構造函數中; 甚至還有更糟糕的情況, 那就是這些代碼散落在客戶端代碼的多個位置。
-
如果為每種可能的對象都創建一個子類, 這可能會導致程序變得過于復雜。
-
HouseHouseWithGarageHouseWithSwimmingPoolHouseWithFancyStatues...
-
-
-
例如, 我們來思考如何創建一個
房屋
House對象。 建造一棟簡單的房屋, 首先你需要建造四面墻和地板, 安裝房門和一套窗戶, 然后再建造一個屋頂。 但是如果你想要一棟更寬敞更明亮的房屋, 還要有院子和其他設施 (例如暖氣、 排水和供電設備), 那又該怎么辦呢? -
最簡單的方法是擴展
房屋
基類, 然后創建一系列涵蓋所有參數組合的子類。 但最終你將面對相當數量的子類。 任何新增的參數 (例如門廊類型) 都會讓這個層次結構更加復雜。 -
另一種方法則無需生成子類。 你可以在
房屋
基類中創建一個包括所有可能參數的超級構造函數, 并用它來控制房屋對象。 這種方法確實可以避免生成子類, 但它卻會造成另外一個問題。-
擁有大量輸入參數的構造函數也有缺陷: 這些參數也不是每次都要全部用上的。
-
House...House(windows,doors,rooms,hasGarage,hasSwimPool,hasStatues,hasGarden,...)// 使用時 new House(4,2,4,true,null,null,null,...)new House(4,2,4,true,true,true,true,...)
-
-
-
通常情況下, 絕大部分的參數都沒有使用, 這使得對于構造函數的調用十分不簡潔。 例如, 只有很少的房子有游泳池, 因此與游泳池相關的參數十之八九是毫無用處的。
解決方案
-
生成器模式建議將對象構造代碼從產品類中抽取出來, 并將其放在一個名為生成器的獨立對象中。
-
生成器模式建議將對象構造代碼從產品類中抽取出來, 并將其放在一個名為生成器的獨立對象中。
HouseBuilderbuildWarlls()buildDoors()buildWindows()buildRoof()buildGarage()getResult(): House
-
-
該模式會將對象構造過程劃分為一組步驟, 比如
build-Walls
創建墻壁和build-Door
創建房門創建房門等。 每次創建對象時, 你都需要通過生成器對象執行一系列步驟。 重點在于你無需調用所有步驟, 而只需調用創建特定對象配置所需的那些步驟即可。 -
當你需要創建不同形式的產品時, 其中的一些構造步驟可能需要不同的實現。 例如, 木屋的房門可能需要使用木頭制造, 而城堡的房門則必須使用石頭制造。
-
在這種情況下, 你可以創建多個不同的生成器, 用不同方式實現一組相同的創建步驟。 然后你就可以在創建過程中使用這些生成器 (例如按順序調用多個構造步驟) 來生成不同類型的對象。
-
不同生成器以不同方式執行相同的任務。
- 根據 實際需求 進行生成。
-
-
例如, 假設第一個建造者使用木頭和玻璃制造房屋, 第二個建造者使用石頭和鋼鐵, 而第三個建造者使用黃金和鉆石。 在調用同一組步驟后, 第一個建造者會給你一棟普通房屋, 第二個會給你一座小城堡, 而第三個則會給你一座宮殿。 但是, 只有在調用構造步驟的客戶端代碼可以通過通用接口與建造者進行交互時, 這樣的調用才能返回需要的房屋。
主管
-
你可以進一步將用于創建產品的一系列生成器步驟調用抽取成為單獨的主管類。 主管類可定義創建步驟的執行順序, 而生成器則提供這些步驟的實現。
-
主管知道需要哪些創建步驟才能獲得可正常使用的產品。
-
-
嚴格來說, 你的程序中并不一定需要主管類。 客戶端代碼可直接以特定順序調用創建步驟。 不過, 主管類中非常適合放入各種例行構造流程, 以便在程序中反復使用。
-
此外, 對于客戶端代碼來說, 主管類完全隱藏了產品構造細節。 客戶端只需要將一個生成器與主管類關聯, 然后使用主管類來構造產品, 就能從生成器處獲得構造結果了。
生成器模式結構
-
1、生成器(Builder) 接口聲明在所有類型生成器中通用的產品構造步驟。
-
<<interface>> Builderreset()buildStepA()buildStepB()buildStepC()
-
-
2、具體生成器(Concrete Builders) 提供構造過程的不同實現。 具體生成器也可以構造不遵循通用接口的產品。
-
ConcreteBuilder1result: Product1buildStepA()buildStepB()buildStepC()getResult(): Product1ConcreteBuilder2result: Product2buildStepA()buildStepB()buildStepC()getResult(): Product2
-
-
3、產品(Products) 是最終生成的對象。 由不同生成器構造的產品無需屬于同一類層次結構或接口。
-
Product1Product2
-
-
4、主管(Director) 類定義調用構造步驟的順序, 這樣你就可以創建和復用特定的產品配置。
-
Director-builder: Builder+Director(builder)+changeBuilder(builder)+make(type)builder.reset()if(type == "simple"){builder.buildStepA()} else {builder.buildStepB()builder.buildStepC()}
-
-
5、客戶端(Client) 必須將某個生成器對象與主管類關聯。 一般情況下, 你只需通過主管類構造函數的參數進行一次性關聯即可。 此后主管類就能使用生成器對象完成后續所有的構造任務。 但在客戶端將生成器對象傳遞給主管類制造方法時還有另一種方式。 在這種情況下, 你在使用主管類生產產品時每次都可以使用不同的生成器。
生成器模式適合應用場景
-
使用生成器模式可避免 “重疊構造函數 (telescoping constructor)” 的出現。
-
假設你的構造函數中有十個可選參數, 那么調用該函數會非常不方便; 因此, 你需要重載這個構造函數, 新建幾個只有較少參數的簡化版。 但這些構造函數仍需調用主構造函數, 傳遞一些默認數值來替代省略掉的參數。
-
class Pizza {Pizza(int size) { …… }Pizza(int size, boolean cheese) { …… }Pizza(int size, boolean cheese, boolean pepperoni) { …… }// ……
-
只有在 C# 或 Java 等支持方法重載的編程語言中才能寫出如此復雜的構造函數。
-
生成器模式讓你可以分步驟生成對象, 而且允許你僅使用必須的步驟。 應用該模式后, 你再也不需要將幾十個參數塞進構造函數里了。
-
-
當你希望使用代碼創建不同形式的產品 (例如石頭或木頭房屋) 時, 可使用生成器模式。
- 如果你需要創建的各種形式的產品, 它們的制造過程相似且僅有細節上的差異, 此時可使用生成器模式。
- 基本生成器接口中定義了所有可能的制造步驟, 具體生成器將實現這些步驟來制造特定形式的產品。 同時, 主管類將負責管理制造步驟的順序。
-
使用生成器構造組合樹或其他復雜對象。
-
生成器模式讓你能分步驟構造產品。 你可以延遲執行某些步驟而不會影響最終產品。 你甚至可以遞歸調用這些步驟, 這在創建對象樹時非常方便。
-
生成器在執行制造步驟時, 不能對外發布未完成的產品。 這可以避免客戶端代碼獲取到不完整結果對象的情況。
實現方法
-
清晰地定義通用步驟, 確保它們可以制造所有形式的產品。 否則你將無法進一步實施該模式。
-
在基本生成器接口中聲明這些步驟。
-
為每個形式的產品創建具體生成器類, 并實現其構造步驟。
不要忘記實現獲取構造結果對象的方法。 你不能在生成器接口中聲明該方法, 因為不同生成器構造的產品可能沒有公共接口, 因此你就不知道該方法返回的對象類型。 但是, 如果所有產品都位于單一類層次中, 你就可以安全地在基本接口中添加獲取生成對象的方法。
-
考慮創建主管類。 它可以使用同一生成器對象來封裝多種構造產品的方式。
-
客戶端代碼會同時創建生成器和主管對象。 構造開始前, 客戶端必須將生成器對象傳遞給主管對象。 通常情況下, 客戶端只需調用主管類構造函數一次即可。 主管類使用生成器對象完成后續所有制造任務。 還有另一種方式, 那就是客戶端可以將生成器對象直接傳遞給主管類的制造方法。
-
只有在所有產品都遵循相同接口的情況下, 構造結果可以直接通過主管類獲取。 否則, 客戶端應當通過生成器獲取構造結果。
生成器模式優缺點
優點:
- 你可以分步創建對象, 暫緩創建步驟或遞歸運行創建步驟。
- 生成不同形式的產品時, 你可以復用相同的制造代碼。
- 單一職責原則。 你可以將復雜構造代碼從產品的業務邏輯中分離出來。
缺點:
- 由于該模式需要新增多個類, 因此代碼整體復雜程度會有所增加。
與其他模式的關系
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定制), 隨后演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加復雜)。
- 生成器重點關注如何分步生成復雜對象。 抽象工廠專門用于生產一系列相關對象。 抽象工廠會馬上返回產品, 生成器則允許你在獲取產品前執行一些額外構造步驟。
- 你可以在創建復雜組合模式樹時使用生成器, 因為這可使其構造步驟以遞歸的方式運行。
- 你可以結合使用生成器和橋接模式: 主管類負責抽象工作, 各種不同的生成器負責實現工作。
- 抽象工廠、 生成器和原型都可以用單例模式來實現。
概念示例
使用示例: 生成器模式是 C++ 世界中的一個著名模式。 當你需要創建一個可能有許多配置選項的對象時, 該模式會特別有用。
識別方法: 生成器模式可以通過類來識別, 它擁有一個構建方法和多個配置結果對象的方法。 生成器方法通常支持方法鏈 (例如
someBuilder->setValueA(1)->setValueB(2)->create()
)。
本例說明了生成器設計模式的結構并重點回答了下面的問題:
- 它由哪些類組成?
- 這些類扮演了哪些角色?
- 模式中的各個元素會以何種方式相互關聯?
/*** 只有當你的產品非常復雜且需要大量配置時,使用建造者模式才有意義。
*
* 與其他創建型模式不同,不同的具體建造者可以生成不相關的產品。
換句話說,不同建造者的結果可能并不總是遵循相同的接口。*/class Product1{public:std::vector<std::string> parts_;void ListParts()const{std::cout << "Product parts: ";for (size_t i=0;i<parts_.size();i++){if(parts_[i]== parts_.back()){std::cout << parts_[i];}else{std::cout << parts_[i] << ", ";}}std::cout << "\n\n"; }
};/*** Builder 接口指定了創建 Product 對象不同部分的方法。*/
class Builder{public:virtual ~Builder(){}virtual void ProducePartA() const =0;virtual void ProducePartB() const =0;virtual void ProducePartC() const =0;
};
/*** 具體建造者類遵循建造者接口,并提供建造步驟的具體實現。您的程序可能包含多個不同的建造者變體,且實現方式也不同。*/
class ConcreteBuilder1 : public Builder{private:Product1* product;/*** 新的構建器實例應包含一個空白產品對象,用于進一步組裝。*/public:ConcreteBuilder1(){this->Reset();}~ConcreteBuilder1(){delete product;}void Reset(){this->product= new Product1();}/*** 所有生產步驟均使用相同的產品實例。*/void ProducePartA()const override{this->product->parts_.push_back("PartA1");}void ProducePartB()const override{this->product->parts_.push_back("PartB1");}void ProducePartC()const override{this->product->parts_.push_back("PartC1");}/*** 具體構建器應該提供自己的方法來檢索結果。這是因為不同類型的構建器可能會創建完全不同的、不遵循相同接口的產品。因此,此類方法不能在基礎構建器接口中聲明(至少在靜態類型編程語言中是這樣)。請注意,PHP 是動態類型語言,此方法可以存在于基礎接口中。但為了清晰起見,我們不會在那里聲明它。* 通常,在將最終結果返回給客戶端后,構建器實例應該準備好開始生成另一個產品。因此,通常的做法是在 `getProduct` 方法主體的末尾調用 reset 方法。但是,此行為并非強制性的,您可以讓構建器等待來自客戶端代碼的顯式 reset 調用,然后再處理先前的結果。*//*** 請注意內存所有權。一旦調用 GetProduct,此函數的用戶就有責任釋放這塊內存。使用智能指針來避免內存泄漏可能是一個更好的選擇。*/Product1* GetProduct() {Product1* result= this->product;this->Reset();return result;}
};/*** Director只負責按照特定的順序執行構建步驟。當按照特定的順序或配置生產產品時,這很有幫助。嚴格來說,Director類是可選的,因為客戶端可以直接控制構建器。*/
class Director{/*** @var Builder*/private:Builder* builder;/*** Director 可以與客戶端代碼傳遞給它的任何構建器實例協同工作。這樣,客戶端代碼可能會改變新組裝產品的最終類型。 **/public:void set_builder(Builder* builder){this->builder=builder;}/*** 總監可以使用相同的構建步驟構建多個產品變體。*/void BuildMinimalViableProduct(){this->builder->ProducePartA();}void BuildFullFeaturedProduct(){this->builder->ProducePartA();this->builder->ProducePartB();this->builder->ProducePartC();}
};
/*** 客戶端代碼創建一個構建器對象,將其傳遞給主管,然后 啟動構造過程。最終結果將從構建器對象中檢索。*/
/*** 為了簡單起見,我使用了原始指針,但您可能更喜歡在這里使用智能指針。*/
void ClientCode(Director& director)
{ConcreteBuilder1* builder = new ConcreteBuilder1();director.set_builder(builder);std::cout << "Standard basic product:\n"; director.BuildMinimalViableProduct();Product1* p= builder->GetProduct();p->ListParts();delete p;std::cout << "Standard full featured product:\n"; director.BuildFullFeaturedProduct();p= builder->GetProduct();p->ListParts();delete p;// Remember, the Builder pattern can be used without a Director class.std::cout << "Custom product:\n";builder->ProducePartA();builder->ProducePartC();p=builder->GetProduct();p->ListParts();delete p;delete builder;
}int main(){Director* director= new Director();ClientCode(*director);delete director;return 0;
}
原型模式
亦稱:克隆、Clone、Prototype
意圖:
- 原型模式是一種創建型設計模式, 使你能夠復制已有對象, 而又無需使代碼依賴它們所屬的類。
問題:
-
如果你有一個對象, 并希望生成與其完全相同的一個復制品, 你該如何實現呢? 首先, 你必須新建一個屬于相同類的對象。 然后, 你必須遍歷原始對象的所有成員變量, 并將成員變量值復制到新對象中。
-
不錯! 但有個小問題。 并非所有對象都能通過這種方式進行復制, 因為有些對象可能擁有私有成員變量, 它們在對象本身以外是不可見的。
-
“從外部” 復制對象并非總是可行。
-
-
直接復制還有另外一個問題。 因為你必須知道對象所屬的類才能創建復制品, 所以代碼必須依賴該類。 即使你可以接受額外的依賴性, 那還有另外一個問題: 有時你只知道對象所實現的接口, 而不知道其所屬的具體類, 比如可向方法的某個參數傳入實現了某個接口的任何對象。
解決方案
-
原型模式將克隆過程委派給被克隆的實際對象。 模式為所有支持克隆的對象聲明了一個通用接口, 該接口讓你能夠克隆對象, 同時又無需將代碼和對象所屬類耦合。 通常情況下, 這樣的接口中僅包含一個
克隆
方法。 -
所有的類對
克隆
方法的實現都非常相似。 該方法會創建一個當前類的對象, 然后將原始對象所有的成員變量值復制到新建的類中。 你甚至可以復制私有成員變量, 因為絕大部分編程語言都允許對象訪問其同類對象的私有成員變量。 -
支持克隆的對象即為原型。 當你的對象有幾十個成員變量和幾百種類型時, 對其進行克隆甚至可以代替子類的構造。
-
預生成原型可以代替子類的構造。
-
-
其運作方式如下: 創建一系列不同類型的對象并不同的方式對其進行配置。 如果所需對象與預先配置的對象相同, 那么你只需克隆原型即可, 無需新建一個對象。
真實世界類比
- 現實生活中, 產品在得到大規模生產前會使用原型進行各種測試。 但在這種情況下, 原型只是一種被動的工具, 不參與任何真正的生產活動。
- 由于工業原型并不是真正意義上的自我復制, 因此細胞有絲分裂 (還記得生物學知識嗎?) 或許是更恰當的類比。 有絲分裂會產生一對完全相同的細胞。 原始細胞就是一個原型, 它在復制體的生成過程中起到了推動作用。
原型模式結構
-
1、原型 (Prototype) 接口將對克隆方法進行聲明。 在絕大多數情況下, 其中只會有一個名為
clone
克隆的方法。-
<<interface>> Prototype+clone():Prototype
-
-
2、具體原型 (Concrete Prototype) 類將實現克隆方法。 除了將原始對象的數據復制到克隆體中之外, 該方法有時還需處理克隆過程中的極端情況, 例如克隆關聯對象和梳理遞歸依賴等等。
-
ConcretePrototype-field1+ConcretePrototype(Prototype)+clone():PrototypeSubClassPrototype-field1+SubClassPrototype(Prototype)+clone():Prototype
-
-
3、客戶端 (Client) 可以復制實現了原型接口的任何對象。
-
Clientcopy = exist.clone()
-
原型模式適合應用場景
- 如果你需要復制一些對象, 同時又希望代碼獨立于這些對象所屬的具體類, 可以使用原型模式。
- 這一點考量通常出現在代碼需要處理第三方代碼通過接口傳遞過來的對象時。 即使不考慮代碼耦合的情況, 你的代碼也不能依賴這些對象所屬的具體類, 因為你不知道它們的具體信息。
- 原型模式為客戶端代碼提供一個通用接口, 客戶端代碼可通過這一接口與所有實現了克隆的對象進行交互, 它也使得客戶端代碼與其所克隆的對象具體類獨立開來。
- 如果子類的區別僅在于其對象的初始化方式, 那么你可以使用該模式來減少子類的數量。 別人創建這些子類的目的可能是為了創建特定類型的對象。
- 在原型模式中, 你可以使用一系列預生成的、 各種類型的對象作為原型。
- 客戶端不必根據需求對子類進行實例化, 只需找到合適的原型并對其進行克隆即可。
實現方式
-
創建原型接口, 并在其中聲明
克隆
方法。 如果你已有類層次結構, 則只需在其所有類中添加該方法即可。 -
原型類必須另行定義一個以該類對象為參數的構造函數。 構造函數必須復制參數對象中的所有成員變量值到新建實體中。 如果你需要修改子類, 則必須調用父類構造函數, 讓父類復制其私有成員變量值。
如果編程語言不支持方法重載, 那么你可能需要定義一個特殊方法來復制對象數據。 在構造函數中進行此類處理比較方便, 因為它在調用
new
運算符后會馬上返回結果對象。 -
克隆方法通常只有一行代碼: 使用
new
運算符調用原型版本的構造函數。 注意, 每個類都必須顯式重寫克隆方法并使用自身類名調用new
運算符。 否則, 克隆方法可能會生成父類的對象。 -
你還可以創建一個中心化原型注冊表, 用于存儲常用原型。
你可以新建一個工廠類來實現注冊表, 或者在原型基類中添加一個獲取原型的靜態方法。 該方法必須能夠根據客戶端代碼設定的條件進行搜索。 搜索條件可以是簡單的字符串, 或者是一組復雜的搜索參數。 找到合適的原型后, 注冊表應對原型進行克隆, 并將復制生成的對象返回給客戶端。
最后還要將對子類構造函數的直接調用替換為對原型注冊表工廠方法的調用。
原型模式優缺點
優點:
- 你可以克隆對象, 而無需與它們所屬的具體類相耦合。
- 你可以克隆預生成原型, 避免反復運行初始化代碼。
- 你可以更方便地生成復雜對象。
- 你可以用繼承以外的方式來處理復雜對象的不同配置。
缺點:
- 克隆包含循環引用的復雜對象可能會非常麻煩。
與其他模式的關系
- 在許多設計工作的初期都會使用工廠方法模式 (較為簡單, 而且可以更方便地通過子類進行定制), 隨后演化為使用抽象工廠模式、 原型模式或生成器模式 (更靈活但更加復雜)。
- 抽象工廠模式通常基于一組工廠方法, 但你也可以使用原型模式來生成這些類的方法。
- 原型可用于保存命令模式的歷史記錄。
- 大量使用組合模式和裝飾模式的設計通常可從對于原型的使用中獲益。 你可以通過該模式來復制復雜結構, 而非從零開始重新構造。
- 原型并不基于繼承, 因此沒有繼承的缺點。 另一方面, 原型需要對被復制對象進行復雜的初始化。 工廠方法基于繼承, 但是它不需要初始化步驟。
- 有時候原型可以作為備忘錄模式的一個簡化版本, 其條件是你需要在歷史記錄中存儲的對象的狀態比較簡單, 不需要鏈接其他外部資源, 或者鏈接可以方便地重建。
- 抽象工廠、 生成器和原型都可以用單例模式來實現。
概念示例
識別方法: 原型可以簡單地通過
clone
或copy
等方法來識別。
本例說明了原型設計模式的結構并重點回答了下面的問題:
- 它由哪些類組成?
- 這些類扮演了哪些角色?
- 模式中的各個元素會以何種方式相互關聯?
using std::string;// 原型設計模式
// 目的:允許你復制現有對象,而無需使代碼依賴于它們的類。enum Type {PROTOTYPE_1 = 0,PROTOTYPE_2
};/*** 具有克隆功能的示例類。我們將看到不同類型的字段值如何被克隆。*/class Prototype {protected:string prototype_name_;float prototype_field_;public:Prototype() {}Prototype(string prototype_name): prototype_name_(prototype_name) {}virtual ~Prototype() {}virtual Prototype *Clone() const = 0;virtual void Method(float prototype_field) {this->prototype_field_ = prototype_field;std::cout << "Call Method from " << prototype_name_ << " with field : " << prototype_field << std::endl;}
};/*** ConcretePrototype1 是 Prototype 的子類,并實現了 Clone 方法。在本例中,Prototype 類的所有數據成員都在 Stack 中。如果你的屬性中包含指針,例如 String* name_,則需要實現復制構造函數,以確保從 clone 方法獲得深層復制。*/class ConcretePrototype1 : public Prototype {private:float concrete_prototype_field1_;public:ConcretePrototype1(string prototype_name, float concrete_prototype_field): Prototype(prototype_name), concrete_prototype_field1_(concrete_prototype_field) {}/*** 注意,Clone 方法返回一個指向新的 ConcretePrototype1 副本的指針。因此,客戶端(調用 clone 方法的客戶端)有責任釋放該內存。如果您了解智能指針,您可能更喜歡在這里使用 unique_pointer。*/Prototype *Clone() const override {return new ConcretePrototype1(*this);}
};class ConcretePrototype2 : public Prototype {private:float concrete_prototype_field2_;public:ConcretePrototype2(string prototype_name, float concrete_prototype_field): Prototype(prototype_name), concrete_prototype_field2_(concrete_prototype_field) {}Prototype *Clone() const override {return new ConcretePrototype2(*this);}
};/*** 在原型工廠中,您有兩個具體的原型,每個具體的原型類一個,因此每次您想要創建一個項目符號時,您都可以使用現有的原型并克隆它們。*/class PrototypeFactory {private:std::unordered_map<Type, Prototype *, std::hash<int>> prototypes_;public:PrototypeFactory() {prototypes_[Type::PROTOTYPE_1] = new ConcretePrototype1("PROTOTYPE_1 ", 50.f);prototypes_[Type::PROTOTYPE_2] = new ConcretePrototype2("PROTOTYPE_2 ", 60.f);}/*** 小心釋放所有分配的內存。再次強調,如果你有智能指針,最好在這里使用它。*/~PrototypeFactory() {delete prototypes_[Type::PROTOTYPE_1];delete prototypes_[Type::PROTOTYPE_2];}/*** 請注意,您只需指定所需的原型類型即可。 該方法將根據此類型的對象創建。*/Prototype *CreatePrototype(Type type) {return prototypes_[type]->Clone();}
};void Client(PrototypeFactory &prototype_factory) {std::cout << "Let's create a Prototype 1\n";Prototype *prototype = prototype_factory.CreatePrototype(Type::PROTOTYPE_1);prototype->Method(90);delete prototype;std::cout << "\n";std::cout << "Let's create a Prototype 2 \n";prototype = prototype_factory.CreatePrototype(Type::PROTOTYPE_2);prototype->Method(10);delete prototype;
}int main() {PrototypeFactory *prototype_factory = new PrototypeFactory();Client(*prototype_factory);delete prototype_factory;return 0;
}
單例模式
亦稱: 單件模式、Singleton
意圖:
- 單例模式是一種創建型設計模式, 讓你能夠保證一個類只有一個實例, 并提供一個訪問該實例的全局節點。
問題:
-
單例模式同時解決了兩個問題, 所以違反了單一職責原則:
-
1、保證一個類只有一個實例。 為什么會有人想要控制一個類所擁有的實例數量? 最常見的原因是控制某些共享資源 (例如數據庫或文件) 的訪問權限。
-
它的運作方式是這樣的: 如果你創建了一個對象, 同時過一會兒后你決定再創建一個新對象, 此時你會獲得之前已創建的對象, 而不是一個新對象。
-
注意, 普通構造函數無法實現上述行為, 因為構造函數的設計決定了它必須總是返回一個新對象。
-
單例模式:
- 客戶端甚至可能沒有意識到它們一直都在使用同一個對象。
-
-
2、為該實例提供一個全局訪問節點。 還記得你 (好吧, 其實是我自己) 用過的那些存儲重要對象的全局變量嗎? 它們在使用上十分方便, 但同時也非常不安全, 因為任何代碼都有可能覆蓋掉那些變量的內容, 從而引發程序崩潰。
- 和全局變量一樣, 單例模式也允許在程序的任何地方訪問特定對象。 但是它可以保護該實例不被其他代碼覆蓋。
- 還有一點: 你不會希望解決同一個問題的代碼分散在程序各處的。 因此更好的方式是將其放在同一個類中, 特別是當其他代碼已經依賴這個類時更應該如此。
-
如今, 單例模式已經變得非常流行, 以至于人們會將只解決上文描述中任意一個問題的東西稱為單例。
解決方案
-
將默認構造函數設為私有, 防止其他對象使用單例類的
new
運算符。 -
新建一個靜態構建方法作為構造函數。 該函數會 “偷偷” 調用私有構造函數來創建對象, 并將其保存在一個靜態成員變量中。 此后所有對于該函數的調用都將返回這一緩存對象。
-
如果你的代碼能夠訪問單例類, 那它就能調用單例類的靜態方法。 無論何時調用該方法, 它總是會返回相同的對象。
真實世界類比
- 政府是單例模式的一個很好的示例。 一個國家只有一個官方政府。 不管組成政府的每個人的身份是什么, “某政府” 這一稱謂總是鑒別那些掌權者的全局訪問節點。
單例模式結構
-
1、單例 (Singleton) 類聲明了一個名為
get-Instance
獲取實例的靜態方法來返回其所屬類的一個相同實例。-
單例的構造函數必須對客戶端 (Client) 代碼隱藏。 調用
獲取實例
方法必須是獲取單例對象的唯一方式。 -
SingLeton-instance: Singleton // 靜態實例-SingLeton()+getInstance():SingLetonif(instance==nullptr) {instance = new Singleton()}return instance
-
單例模式適合應用場景
- 如果程序中的某個類對于所有客戶端只有一個可用的實例, 可以使用單例模式。
- 單例模式禁止通過除特殊構建方法以外的任何方式來創建自身類的對象。 該方法可以創建一個新對象, 但如果該對象已經被創建, 則返回已有的對象。
- 如果你需要更加嚴格地控制全局變量,可以使用單例模式。
- 單例模式與全局變量不同, 它保證類只存在一個實例。 除了單例類自己以外, 無法通過任何方式替換緩存的實例。
- 請注意, 你可以隨時調整限制并設定生成單例實例的數量, 只需修改
獲取實例
方法, 即 getInstance 中的代碼即可實現。
實現方式
- 在類中添加一個私有靜態成員變量用于保存單例實例。
- 聲明一個公有靜態構建方法用于獲取單例實例。
- 在靜態方法中實現"延遲初始化"。 該方法會在首次被調用時創建一個新對象, 并將其存儲在靜態成員變量中。 此后該方法每次被調用時都返回該實例。
- 將類的構造函數設為私有。 類的靜態方法仍能調用構造函數, 但是其他對象不能調用。
- 檢查客戶端代碼, 將對單例的構造函數的調用替換為對其靜態構建方法的調用。
單例模式優缺點
優點
- 你可以保證一個類只有一個實例。
- 你獲得了一個指向該實例的全局訪問節點。
- 僅在首次請求單例對象時對其進行初始化。
缺點
- 違反了單一職責原則。 該模式同時解決了兩個問題。
- 單例模式可能掩蓋不良設計, 比如程序各組件之間相互了解過多等。
- 該模式在多線程環境下需要進行特殊處理, 避免多個線程多次創建單例對象。
- 單例的客戶端代碼單元測試可能會比較困難, 因為許多測試框架以基于繼承的方式創建模擬對象。 由于單例類的構造函數是私有的, 而且絕大部分語言無法重寫靜態方法, 所以你需要想出仔細考慮模擬單例的方法。 要么干脆不編寫測試代碼, 或者不使用單例模式。
與其他模式的關系
- 外觀模式類通常可以轉換為單例模式類, 因為在大部分情況下一個外觀對象就足夠了。
- 如果你能將對象的所有共享狀態簡化為一個享元對象, 那么享元模式就和單例類似了。 但這兩個模式有兩個根本性的不同。
- 只會有一個單例實體, 但是享元類可以有多個實體, 各實體的內在狀態也可以不同。
- 單例對象可以是可變的。 享元對象是不可變的。
- 抽象工廠模式、 生成器模式和原型模式都可以用單例來實現。
偽代碼
// 數據庫類會對`getInstance(獲取實例)`方法進行定義以讓客戶端在程序各處
// 都能訪問相同的數據庫連接實例。
class Database is// 保存單例實例的成員變量必須被聲明為靜態類型。private static field instance: Database// 單例的構造函數必須永遠是私有類型,以防止使用`new`運算符直接調用構// 造方法。private constructor Database() is// 部分初始化代碼(例如到數據庫服務器的實際連接)。// ……// 用于控制對單例實例的訪問權限的靜態方法。public static method getInstance() isif (Database.instance