願戎馬一生,歸來仍少年
設計模式簡介
設計模式是軟體設計中常見問題的一般可重用的解決方案或模板。模式通常顯示類或物件之間的關係和互動。這個想法是通過提供經過驗證的開發範例來加快開發過程。
例如,在許多現實世界的情況下,我們只想建立一個類的一個例項。例如,無論個人身份如何,一次只能有一個國家主席。這種模式被稱為單例模式。其他例子可以是由多個物件共享的單個數據庫連線,因為為每個物件建立單獨的資料庫連線可能是昂貴的。類似地,在應用程式中可以有一個配置管理器或錯誤管理器來處理所有問題,而不是建立多個管理器。
型別的設計模式 主要有三種設計模式:
1、建立型
這些設計模式都是關於類例項化或物件建立的。這些模式可以進一步分類為類創作模式和物件創作模式。而類建立模式在例項化過程中有效地使用繼承,物件建立模式可以有效地使用委託來完成工作。 建立型設計模式是工廠方法,抽象工廠,建造者,單例,物件池,原型和單例。
2、結構型
這些設計模式是關於組織不同的類和物件以形成更大的結構並提供新的功能。 結構型設計模式是介面卡,橋接,組合,裝飾器,外觀,享元,私有類資料和代理。
3、行為型
行為模式是關於識別物件之間的常見通訊模式並實現這些模式。 行為模式是責任鏈,命令,直譯器,迭代器,中介者,備忘錄,空物件,觀察者,狀態,策略,模板方法,訪問者
常用的幾個設計模式
1、工廠模式
工廠模式是一個建立型模式,它與物件的建立有關。在工廠模式中, 我們建立物件時,沒有把建立的邏輯暴露給客戶端。客戶端使用相同的公共介面建立新型別的物件。
使用靜態成員函式(靜態工廠方法)來建立一個新物件的引用或指標。
工廠模式是建立一個物件的核心設計原則,允許客戶端建立庫的物件,使其不與庫的類層次結構緊密耦合。
庫是由某些第三方提供的,它暴露了一些公開的APIs,並且客戶呼叫給這些公開的APIs來完成它的任務。一個非常簡單的例子可以是android OS提供的不同種類的Views。
// A design without factory pattern
#include <iostream>
using namespace std;
// Library classes
class Vehicle {
public:
virtual void printVehicle() = 0;
};
class TwoWheeler : public Vehicle {
public:
void printVehicle() {
cout << "I am two wheeler" << endl;
}
};
class FourWheeler : public Vehicle {
public :
void printVehicle() {
cout << "I am four wheeler" << endl;
}
};
// Client (or user) class
class Client {
public:
Client(int type) {
// Client explicitly creates classes according to type
if (type == 1)
pVehicle = new TwoWheeler();
else if (type == 2)
pVehicle = new FourWheeler();
else
pVehicle = NULL;
}
~Client() {
if (pVehicle)
{
delete[] pVehicle;
pVehicle = NULL;
}
}
Vehicle* getVehicle() {
return pVehicle;
}
private:
Vehicle *pVehicle;
};
// Driver program
int main() {
Client *pClient = new Client(1);
Vehicle * pVehicle = pClient->getVehicle();
pVehicle->printVehicle();
return 0;
}
上述設計模式的是有些問題的,客戶端在建立新物件時,必須依賴於某個輸入。意味著,如果庫引入一個新的類ThreeWheeler。那麼會發生什麼呢?Client類最終需要增加一個if else判斷分支來建立ThreeWheeler物件,這將導致客戶端程式需要重新編譯。每次庫改動一次,Client類就需要做出相應的改變然後重新編譯。這嚴重違反了對修改封閉-對擴充套件開放的設計原則。
為了避免這個問題,可以建立一個靜態的方法,實現工廠的功能。
// C++ program to demonstrate factory method design pattern
#include <iostream>
using namespace std;
enum VehicleType {
VT_TwoWheeler, VT_ThreeWheeler, VT_FourWheeler
};
// Library classes
class Vehicle {
public:
virtual void printVehicle() = 0;
static Vehicle* Create(VehicleType type);
};
class TwoWheeler : public Vehicle {
public:
void printVehicle() {
cout << "I am two wheeler" << endl;
}
};
class ThreeWheeler : public Vehicle {
public:
void printVehicle() {
cout << "I am three wheeler" << endl;
}
};
class FourWheeler : public Vehicle {
public:
void printVehicle() {
cout << "I am four wheeler" << endl;
}
};
// Factory method to create objects of different types.
// Change is required only in this function to create a new object type
Vehicle* Vehicle::Create(VehicleType type) {
if (type == VT_TwoWheeler)
return new TwoWheeler();
else if (type == VT_ThreeWheeler)
return new ThreeWheeler();
else if (type == VT_FourWheeler)
return new FourWheeler();
else return NULL;
}
// Client class
class Client {
public:
// Client doesn't explicitly create objects
// but passes type to factory method "Create()"
Client()
{
VehicleType type = VT_ThreeWheeler;
pVehicle = Vehicle::Create(type);
}
~Client() {
if (pVehicle) {
delete[] pVehicle;
pVehicle = NULL;
}
}
Vehicle* getVehicle() {
return pVehicle;
}
private:
Vehicle *pVehicle;
};
// Driver program
int main() {
Client *pClient = new Client();
Vehicle * pVehicle = pClient->getVehicle();
pVehicle->printVehicle();
return 0;
}
上述最大的改動就是,建立了一個靜態的工廠函式,Client類不顯式的建立物件,而是傳遞引數給靜態工廠函式Create()。這樣完全把型別的選擇與客戶端的物件建立解耦了。庫現在負責根據輸入來決定要建立的物件型別。客戶端只需要呼叫庫的工廠建立方法並傳遞所需的型別,而不必擔心實際建立物件的實現。
2、單例模式
單體模式是最簡單的設計模式之一。有時候,我們只需要有一個類的例項,例如由多個物件共享的單個數據庫連線,因為為每個物件建立一個單獨的資料庫連線可能是昂貴的。類似地,在應用程式中可以有一個配置管理器或錯誤管理器來處理所有問題,而不是建立多個管理器。
定義:
單例模式是將類的例項化限制為一個物件的設計模式。
實現的方法
方法一:經典的實現方法是將建構函式,複製建構函式,operator=過載函式全宣告為私有,宣告一個靜態的自身物件,然後定義一個靜態方法,返回物件的引用。如果返回物件的指標,有可能客戶程式會意外delete掉這個物件。補救方法是,將解構函式也宣告為私有。因此,返回物件的引用被認為是較安全的實現。
// Classical Java implementation of singleton
// design pattern
class Singleton
{
private static Singleton obj;
// private constructor to force use of
// getInstance() to create Singleton object
private Singleton() {}
public static Singleton getInstance()
{
if (obj==null)
obj = new Singleton();
return obj;
}
}
這裡我們已經聲明瞭getInstance()為static,以便我們可以在不例項化該類的情況下呼叫它。 getInstance()第一次被呼叫時,建立一個新的單例物件,之後它只返回相同的物件。請注意,在我們需要它並呼叫getInstance()方法之前,不建立單例obj。這被稱為懶惰例項化。
上面的方法的主要問題是它不是執行緒安全的。考慮以下執行順序。
執行緒1和執行緒2同時訪問getInstance(),這種執行順序建立了兩個Singleton物件。
可以給程序加鎖來處理。這裡不得不介紹一下Double-Check Locking 技術
// Classical Java implementation of singleton
// design pattern
class Singleton
{
private static Singleton obj;
// private constructor to force use of
// getInstance() to create Singleton object
private Singleton() {}
public static Singleton getInstance()
{
if(obj==null)
{
lock();
if (obj==null)
obj = new Singleton();
return obj;
unlock();
}
}
}
lock確保當一個程序位於程式碼的臨界區時,另一個程序不能進入臨界區。這樣就保證了在同一時刻,加了鎖的那部分程式碼只有一個執行緒可以進入。第一個if是為了不讓多個執行緒訪問Singleton類時每次都加鎖,而只是在例項未建立的情況下才加鎖,減少呼叫花銷。第二個if是為了讓最先進入臨界區的執行緒來建立物件例項,後面進入的執行緒進入時不會再去建立例項,也就是說,當obj為null並且有兩個執行緒同時呼叫getInstance()時,第二個if就起作用了。
單例類的實現應具有以下屬性:
1、 它應該只有一個例項:這是通過從類中提供類的例項來完成的。應該防止外部類或子類建立例項。這是通過使建構函式私有化的,以便沒有類可以訪問建構函式,因此無法例項化。
2、例項應該是全域性可訪問的:單例類的例項應該是全域性可訪問的,以便每個類都可以使用它。在java中,可以通過使public instance的訪問說明符來實現。
單例模式的應用程式很多,其中一些主要是:
1、硬體介面訪問:使用單例取決於要求。單例類也用於防止對類的併發訪問。實際上單例可以用於例如限制外部硬體資源使用。硬體印表機可以將列印後臺處理程式作為單例,以避免多次併發訪問併產生死鎖。
2、日誌:單例類用於日誌檔案生成。日誌檔案由logger類物件建立。假設一個應用程式,其中日誌工具必須基於從使用者接收到的訊息來生成一個日誌檔案。如果有多個客戶端應用程式使用此日誌實用程式類,它們可能會建立此類的多個例項,並且可能會在併發訪問同一個日誌記錄檔案時導致問題。我們可以使用日誌實用程式類作為單例,並提供全域性引用點,以便每個使用者可以使用此實用程式,並且沒有2個使用者在同一時間訪問它。
3、配置檔案:這是單例模式的另一個潛在候選者,因為它具有效能優勢,因為它可以防止多個使用者重複訪問和讀取配置檔案或屬性檔案。它建立一個配置檔案的單個例項,可以同時由多個呼叫訪問,因為它將提供載入到記憶體物件中的靜態配置資料。應用程式只能從第一次從配置檔案中讀取,並且從第二次呼叫開始,客戶端應用程式從記憶體中的物件讀取資料。
4、快取:我們可以將快取用作單例物件,因為它可以具有全域性引用點,並且對於將來對快取物件的所有呼叫,客戶端應用程式將使用記憶體中物件。
3、裝飾器模式
假設我們正在為比薩餅商店建立一個應用程式,我們需要對比薩餅類進行建模。假設他們提供四種類型的比薩餅,即胡椒餅,農舍,瑪格麗特和雞節。最初我們只是使用繼承,並抽象出披薩類中的常用功能。
每個比薩餅都有不同的成本。我們已經子類中的getcost()來找到合適的成本。現在假設一個新的要求,除了比薩,客戶還可以要求幾個澆頭,如新鮮的番茄,面具,辣椒,辣椒,燒烤等。讓我們考慮一下,我們如何適應上述課程的變化使客戶可以選擇比薩餅與澆頭,我們得到客戶選擇的比薩餅和澆頭的總成本。
選項1
為每個澆頭的匹薩建立一個新的子類。類圖如下
這看起來很複雜。有太多的類,是一個維護噩夢。如果我們想新增一個新的澆頭或比薩餅,我們必須新增這麼多類。這顯然是非常糟糕的設計。
選項2:
讓我們將例項變數新增到比薩餅基類中,以表示每個比薩餅是否有澆頭。類圖將如下所示:
這個設計首先看起來不錯,但是讓我們來看看與之相關的問題。 補品價格變動將導致現有程式碼的變更。 新的澆頭將迫使我們新增新方法並在超類中更改getcost()方法。 對於一些比薩餅,一些澆頭可能不合適,但子類繼承它們。 如果客戶想要雙辣椒或雙重乳酪呢?
1、針對一個匹薩物件
2、用辣椒物件“裝飾”它。
3、用乳酪物件“裝飾”它。
4、呼叫getcost()並使用委託代替繼承來計算澆頭成本。
我們最後得到的是一個有乳酪和辣椒澆頭的匹薩。視覺化“裝飾器”物件,如包裝器。這裡是裝飾器的一些屬性:
● 裝飾器具有與其裝飾物體相同的超型別。
● 您可以使用多個裝飾器來包裝物件。
● 因為裝飾器與物件具有相同的型別,所以我們可以傳遞裝飾物件而不是原始物件。
● 我們可以在執行時裝飾物件。
定義: 裝飾器模式動態地附加物件的附加責任。裝飾器為擴充套件功能提供了子類化的靈活替代方法。
每個元件可以單獨使用,也可以由裝飾器包裝。
每個裝飾器都有一個例項變數,它儲存對其裝飾的元件的引用(has-a 的關係)。
該混合組件是我們要動態裝飾的物件。
優點:
可以使用裝飾器模式來在執行時擴充套件(裝飾)特定物件的功能。
裝飾器模式是子類化的替代方案。子類在編譯時新增行為,更改影響原始類的所有例項;裝飾可以在執行時為單個物件提供新的行為。
裝飾器提供按現收現付的方式增加責任。而不是嘗試支援複雜的可定製類中的所有可預見的功能,您可以定義一個簡單的類,並使用裝飾器物件逐步新增功能。
缺點:
裝飾器可能使例項化元件的過程變得複雜,因為您不僅必須例項化元件,而且將其包裝在多個裝飾器中。
裝飾器跟蹤其他裝飾工具可能很複雜,因為回顧裝飾鏈的多層開始讓裝飾器模式超出其真實意圖。
最終使用了裝飾器模式的匹薩成本計算類圖如下
4、介面卡模式
考慮一個usb到乙太網介面卡。當我們有一個乙太網介面和另一端的usb時,我們需要這個。因為他們是不相容的。這個例子非常類似於面向物件的介面卡。在設計中,當我們有一個類(客戶端)期望某種型別的物件時,使用介面卡,並且我們有一個提供相同功能但暴露不同介面的物件(adapttee)。
使用介面卡:
1、客戶端通過使用目標介面呼叫其上的方法向介面卡發出請求。
2、介面卡使用介面卡介面將該請求轉換為被適配者。
3、客戶端接收到呼叫返回的結果,並不知道介面卡的存在。
定義: 介面卡模式將類的介面轉換為客戶端期望的另一個介面。介面卡使類可以協同工作,不受不相容的介面影響。
客戶端只看到目標介面而不是介面卡。介面卡實現目標介面。介面卡將所有請求委託給被適配者。
假設你有一個帶有fly()和bird()方法的鳥類。還有一個具有squeak()方法的玩具類。讓我們假設你缺少玩具物件,而你想在使用玩具物件的地方使用鳥類物件或者鳥類物件的方法。鳥類具有一些類似的功能,但實現了不同的介面,所以我們不能直接使用它們。所以我們將使用介面卡模式。這裡我們的客戶將是玩具,被適應者將是鳥。我們要將鳥類物件的介面轉換為玩具物件的介面。
#include <iostream>
using namespace std;
class Bird
{
//Bird is a base class,the method is implemented by its subclass
public:
virtual void fly(){};
virtual void makeSound(){};
};
class Sparrow:public Bird
{
// a concrete implementation of bird
public:
void fly()
{
cout<<"Flying"<<endl;
}
void makeSound()
{
cout<<"Chirp Chirp"<<endl;
}
};
class ToyDuck
{
// target interface
// toyducks don't fly they just make
// squeaking sound
public:
virtual void squeak() = 0;
};
class PlasticToyDuck:public ToyDuck
{
public:
void squeak()
{
cout<<"Squeak"<<endl;
}
};
class BirdAdapter:public PlasticToyDuck
{
// You need to implement the interface your
// client expects to use.
Bird* bird;
public:
BirdAdapter(Bird* b)
{
// we need reference to the object we
// are adapting
bird = b;
}
void squeak()
{
// translate the methods appropriately
bird->makeSound();
}
};
int main()
{
Sparrow* sparrow = new Sparrow();
PlasticToyDuck* toyDuck = new PlasticToyDuck();
// Wrap a bird in a birdAdapter so that it
// behaves like toy duck
ToyDuck* birdAdapter = new BirdAdapter(sparrow);
cout<<"Sparrow..."<<endl;
sparrow->fly();
sparrow->makeSound();
cout<<"ToyDuck..."<<endl;
toyDuck->squeak();
// bird behaving like a toy duck
cout<<"BirdAdapter..."<<endl;
birdAdapter->squeak();
delete sparrow;
delete toyDuck;
delete birdAdapter;
return 0;
}
物件介面卡vs類介面卡
們上面實現的介面卡模式稱為物件介面卡模式,因為介面卡持有介面卡的例項。還有另一種型別稱為類介面卡模式,它使用繼承而不是組合,但是需要多個繼承來實現它。
類介面卡模式的類圖:
不是使用介面卡(組合)中的被適配者物件,而是使用支配器繼承被適配者的功能。 由於許多語言(包括java)不支援多重繼承,並且與許多問題相關聯,我們還沒有使用類介面卡模式顯示實現。
優點: 有助於實現可重用性和靈活性。 客戶端類不再需要使用不同的介面,並且可以使用多型來在介面卡的不同實現之間進行交換。
缺點: 所有請求都被轉發,所以開銷略有增加。 有時需要沿著介面卡鏈進行許多修改以達到所需的型別。
5、策略模式
假設我們正在建立一個遊戲“街頭霸王”。為了簡單起見,假設角色可能有四個移動,即踢,打,滾,跳。每個角色都有踢和打的動作,但是滾動和跳躍是可選的。你如何建立你的類?假設最初你使用繼承並抽出一個Fighter類中的共同特徵,並讓其他角色子類繼承Fighter類。
我們的Fighter類我們會預設執行正常的動作。任何具有專門移動的角色都可以在其子類中覆蓋該動作。類圖將如下:
以上設計有什麼問題?
如果一個角色不執行jump呢?它仍然繼承了父類的jump行為。雖然在這種情況下你可以重寫jump,但是您可能需要為許多現有的類執行此操作,也可以為未來的類進行處理。這也將使維護困難。所以我們不能在這裡使用繼承。
如果使用介面呢?
它更簡潔。我們從Fighter中取出了一些動作(某些角色可能無法執行),併為它們建立了介面。這樣只有應該jump的角色才能實現jump行為。以上設計有什麼問題? 上述設計的主要問題是程式碼重用。因為沒有預設的跳和滾動行為實現,我們可能會有程式碼重複。您可能需要在許多子類中重複重寫相同的跳行為。
我們該如何避免呢?
如果我們做出JumpBehavior類和RollBehavior類而不是介面呢?那麼我們必須使用許多語言不支援的多重繼承,因為它有許多問題。
策略模式定義:
“在計算機程式設計中,策略模式是一種軟體設計模式,可以在執行時選擇演算法的行為。戰略格局 定義了一系列演算法, 封裝每個演算法, 使演算法在該家族內互換。“
這裡我們依靠組合而不是繼承來重用。上下文由策略組成。上下文將執行行為委託給策略。上下文將是需要改變行為的類。我們可以動態地改變行為。策略被實現為介面,以便我們可以改變行為而不影響我們的上下文。
優點:
一系列演算法可以定義為類層次結構,可以互換使用,以改變應用程式行為而不改變其架構。
通過分別封裝演算法,可以很容易地引入符合相同介面的新演算法。
應用程式可以在執行時切換策略。
策略使客戶端可以選擇所需的演算法,而不使用“switch”語句或一系列“if-else”語句。
用於實現演算法的資料結構完全封裝在策略類中。因此,可以改變演算法的實現而不影響上下文類。
缺點:
應用程式必須意識到所有的策略才能為正確的情況選擇正確的策略。
上下文和策略類通常通過抽象策略基類指定的介面進行通訊。策略基類必須暴露所有必需行為的介面,有些具體的策略類可能無法實現。
在大多數情況下,應用程式使用所需的策略物件配置上下文。因此,應用程式需要建立和維護兩個物件來代替一個物件。
使用策略模式實現“接頭霸王”模型
第一步是確定未來不同類別可能會有所不同的行為,並將其與其他類別分開。這個例子中是指踢和跳的行為。為了分離這些行為,我們將把這兩種方法從Fighter類中拉出來並建立一組新的類來表示每個行為。
Fighter類現在委託其踢和跳躍行為,而不是使用在Fighter類或其子類中定義的踢和跳方法
將我們的設計與策略模式的定義進行比較,封裝的踢和跳行為是兩個系列的演算法。並且這些演算法在實現中是可以互換的。
6、觀察者模式
要了解觀察者模式,首先需要了解主題和觀察者物件。 主題和觀察者之間的關係可以很容易被理解為類似於雜誌訂閱。
雜誌出版社(主題)在商業中出版雜誌(資料)。
如果您(資料/觀察者的使用者)對您訂閱(註冊)的雜誌感興趣,並且如果釋出了新版本,則會將其傳送給您。
如果您取消訂閱(登出),您將停止獲取新版本。
發行商不知道你是誰,以及你如何使用這本雜誌,它只是把它交給你,因為你是訂閱者(鬆耦合)。
定義: 觀察者模式定義物件之間的一對多依賴關係,以便一個物件改變狀態,所有的依賴項都會被自動通知和更新。
說明 :
1、一對多依賴關係是在主題(一)和觀察者(多)之間。
2、依賴關係是由於觀察者們自己無法訪問資料,他們依賴主題提供資料。
● 這裡的觀察者和主題是介面(可以是任何抽象超類)。
● 所有需要資料的觀察者都需要重新實現觀察者介面。
● 觀察者介面中的notify()方法定義當主題提供資料時要採取的操作。
● 該主題維持一個觀察者聚集,這是當前註冊(訂閱)觀察員的列表。
● registerobserver(觀察者)和unregisterobserver(觀察者)分別是新增和刪除觀察者的方法。
● 當資料更改並且觀察者需要新資料時,呼叫notifyobservers()
優點: 在互動的物件之間提供鬆散耦合的設計。鬆散耦合的物件靈活隨需求變化。這裡鬆耦合意味著相互作用的物件應該有更少的關於彼此的資訊。
觀察者模式提供了這種鬆耦合:
● 主體只知道觀察者實現觀察者介面。
● 沒有必要修改主題以新增或刪除觀察者。
● 我們可以互相重用主題和觀察者類。
缺點: 由於監視器故障導致的記憶體洩漏,因為顯式註冊和登出觀察者。當觀察者不再需要訂閱,但卻無法取消訂閱主題時,會發生洩漏。因此,主體仍然持有對觀察者的引用,阻止它被垃圾回收 - 包括所指向的所有其他物件 - 只要主體是活著的,直到應用結束。
何時使用這種模式?
當多個物件依賴於一個物件的狀態時,您應該考慮在應用程式中使用此模式,因為它為同一個物件提供了一個整潔且經過良好測試的設計
現實生活用途:
它在gui工具包和事件監聽器中大量使用。
在java中,按鈕(subject)和onclicklistener(觀察者)用觀察者模式建模。
社交媒體,RSS訂閱,電子郵件訂閱,您可以選擇關注或訂閱,並收到最新通知。
播放商店上的應用程式的所有使用者都會收到通知,如果有更新。
7、命令模式和狀態模式
命令模式
定義:命令模式將請求封裝為物件,從而讓我們用不同請求,佇列或日誌請求引數化其他物件,並支援可撤銷操作。
主要特點是將一個函式或者物件作為一個引數傳遞一個動作給接收者,並將做些動作放入動作佇列,因此實現上,經常會使用迭代器,遍歷動作物件的佇列
優點: 使我們的程式碼可擴充套件,因為我們可以新增新的命令而不改變現有的程式碼。 減少了命令的呼叫者和接收者的耦合。
缺點: 增加每個命令的類數
狀態模式
狀態模式產生一個可以改變其類的物件,當發現在大多數或者所有函式中都存在條件的程式碼時,這種模式就派上用場。
前端物件將對應分支的操作委派給狀態物件。