C++ 設計模式 - 享元模式 Flyweight Pattern

當你玩大型多人線上遊戲(MMO)時,畫面上有成百上千個玩家角色,擁有著不同的裝備、武器和坐騎,你有沒有想過,遊戲引擎是如何管理這麼多個體的?如果每一個角色都佔據完整的記憶體空間,電腦恐怕早已爆掉!這背後的祕密之一,就是我們今天要講的「享元模式」。

什麼是享元模式?

享元模式(Flyweight Pattern)是一種結構型設計模式,目的在減少物件建立時佔用的記憶體空間。它透過共享大量相似物件中的共通部分來降低記憶體使用,特別適合用在場景中有大量細微差異的物件。這樣的做法不僅節省了資源,也讓系統執行更有效率。

在享元模式中,會將可共享的部分抽出,放到單一實體中進行管理,而非每個物件都擁有自己的複本。對於那些不可共享的部分,則會透過引數進行傳遞,這樣就能避免重複建立類似的物件。

享元(Flyweight)這個術語源自拳擊界,在拳擊比賽中,Flyweight 指的是一個較輕量級(57kg以下)的選手級別。這個詞在設計模式中被借用,是因為享元模式的核心理念與輕量的概念相吻合:目的是減輕系統的記憶體負擔,讓程式變得更「輕」,更高效。享元模式透過共享物件的內部狀態,達到了減少記憶體佔用的效果。

享元模式在圖形系統中的應用

最常見的享元模式例子之一,就是在圖形系統中處理複雜的場景。想像你正在開發一個繪圖應用程式,必須在畫布上顯示上千個相同的圓形。每個圓的顏色、大小和座標可能不同,但圓的輪廓和基本形狀是相同的。我們可以利用享元模式,來共享這些圓的形狀,只根據需要改變其他屬性來展示不同的圓形。

首先我們需要定義一個共享的「圓形」物件,

1
2
3
4
5
6
7
8
9
10
11
12
// 圓形基類,提供繪製方法
class Circle {
public:
Circle(std::string color) : color(color) {}

void draw(int x, int y, int radius) {
std::cout << "Drawing " << color << " circle at (" << x << ", " << y << ") with radius " << radius << std::endl;
}

private:
std::string color;
};

然後我們可以建立一個「圓形工廠」,負責管理和提供共享的圓形物件,

1
2
3
4
5
6
7
8
9
10
11
12
13
class CircleFactory {
public:
std::shared_ptr<Circle> getCircle(const std::string& color) {
if (circles.find(color) == circles.end()) {
circles[color] = std::make_shared<Circle>(color);
std::cout << "Creating circle of color: " << color << std::endl;
}
return circles[color];
}

private:
std::unordered_map<std::string, std::shared_ptr<Circle>> circles;
};

接下來客戶端可以透過 CircleFactory 來取得不同顏色的圓形,並指定其座標和半徑,這樣便可以有效地共享相同顏色的圓形實體,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
CircleFactory factory;

auto redCircle = factory.getCircle("Red");
redCircle->draw(10, 10, 5);

auto blueCircle = factory.getCircle("Blue");
blueCircle->draw(20, 20, 10);

auto anotherRedCircle = factory.getCircle("Red");
anotherRedCircle->draw(30, 30, 15);

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
5
Creating circle of color: Red
Drawing Red circle at (10, 10) with radius 5
Creating circle of color: Blue
Drawing Blue circle at (20, 20) with radius 10
Drawing Red circle at (30, 30) with radius 15

這裡我們重複使用了紅色圓形,而不必每次都建立新的紅色圓形。程式輸出將顯示紅色圓形只建立了一次,這就是享元模式的精髓——重複利用可共享的部分,節省記憶體。

享元模式的優缺點

享元模式最大的優點,當然就是節省記憶體。當我們需要建立大量相似的物件時,這種模式可以極大減少不必要的記憶體消耗,提升程式的效能。例如在繪圖系統、文字處理器,甚至是遊戲引擎中,都可以看到享元模式的影子。只要那些物件之間有許多相同的部分,就能採用享元模式進行共享。

然而這種共享資源的方式也不是沒有代價。享元模式的複雜性較高,因為需要將物件分為共享的內部狀態與不可共享的外部狀態。開發人員在使用時必須額外考慮如何管理這些狀態,避免產生混淆。另外在某些情況下,為了維護享元物件的內部狀態一致性,可能會導致更多的同步問題,特別是在多執行緒的環境下。

總結

享元模式是一種在需要大量建立相似物件的場景下非常有用的設計模式,透過共享物件來大幅減少記憶體消耗。在像是繪圖系統、遊戲開發等領域,我們可以利用這個模式來有效管理大量物件。然而,享元模式的實現需要仔細設計和管理內部狀態與外部狀態的區別,以避免增加不必要的複雜度。這個模式不會讓你的程式「飛起來」,但絕對能讓它執行得更順暢。

C++ 設計模式 - 解釋器模式 Interpreter Pattern

大家有沒有想過當你在終端機輸入指令時,電腦是如何理解並執行這些指令的?或者當你使用正則表達式搜尋文字時,背後的機制是什麼?這些看似神奇的功能背後,其實都隱藏著一個強大而優雅的設計模式,那就是解釋器模式 Interpreter Pattern,今天就來聊聊這個模式,幫助我們理解當電腦「讀懂」我們的指令時背後的魔法。

什麼是解釋器模式?

解釋器模式是一種行為型設計模式,主要用來解析語言、處理簡單語法規則的。這個模式背後的想法其實很簡單:我們定義一套「語法規則」,然後讓每個「字元」或「符號」有自己專屬的解釋方式。這樣我們就能依照語法,逐步分析出我們想要的結果。

打個比方,就像我們學習一種新語言時,會先學習單字,接著組合句子,最後可以用來進行溝通。解釋器模式就像是這個過程的數位版,先定義每個「單字」的意思,再教你如何解釋這些字。

解釋器模式在計算機中的應用

計算機應該是最能體現解釋器模式的例子之一。想像你有一個簡單的計算機,輸入表達式如「2 + 3 * 4」,然後計算出結果。解釋器模式就可以幫助我們將這些數字與運算子號組合成一個能夠「理解」的語法樹。

首先先定義一個 Expression 介面,它會有一個 interpret() 方法。這個方法負責對數字和符號進行解釋,

1
2
3
4
5
6
// 抽象表達式
class Expression {
public:
virtual int interpret() = 0;
virtual ~Expression() = default;
};

接著定義具體的 NumberExpression(數字表達式)和 OperatorExpression(運算子表達式)。數字表達式會簡單地回傳它自己的值,而運算子表達式則會對兩個子表達式進行計算,比如加法、乘法等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 數字表達式
class NumberExpression : public Expression {
int number;
public:
NumberExpression(int num) : number(num) {}
int interpret() override {
return number;
}
};

// 加法表達式
class AddExpression : public Expression {
Expression* left;
Expression* right;
public:
AddExpression(Expression* l, Expression* r) : left(l), right(r) {}
int interpret() override {
return left->interpret() + right->interpret();
}
};

// 乘法表達式
class MultiplyExpression : public Expression {
Expression* left;
Expression* right;
public:
MultiplyExpression(Expression* l, Expression* r) : left(l), right(r) {}
int interpret() override {
return left->interpret() * right->interpret();
}
};

在前面已經定義了基礎的 NumberExpression、AddExpression 和 MultiplyExpression 類別。接下來我們在客戶端來實際使用這些表達式,讓整個流程完整起來。

我們會有一個解析器來遍歷表達式,並一步步解釋每個部分。假設我們要處理一個簡單的表達式「2 + 3」,客戶端的邏輯會像下面這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main() {
// 建立一個表示數字2的NumberExpression
Expression* num1 = new NumberExpression(2);

// 建立一個表示數字3的NumberExpression
Expression* num2 = new NumberExpression(3);

// 建立一個表示加法的AddExpression,將兩個數字作為參數傳入
Expression* addExpr = new AddExpression(num1, num2);

int result = addExpr->interpret();
std::cout << "2 + 3 = " << result << std::endl;

Expression* mulExpr = new MultiplyExpression(num1, num2);
result = mulExpr->interpret();
std::cout << "2 * 3 = " << result << std::endl;

// 釋放記憶體
delete num1;
delete num2;
delete addExpr;
delete mulExpr;

return 0;
}

執行上述程式碼,我們會得到以下輸出,當我們將表達式「2 + 3」傳給計算機時,解釋器就會根據我們定義的語法規則一步步處理,最終給出結果。

1
2
2 + 3 = 5
2 * 3 = 6

在這個範例中,客戶端的角色就是負責將表達式拼接在一起,然後使用解釋器模式的物件來完成運算。這樣任何新加入的運算子或表達式,只需要擴展 Expression 類別,而不用改動客戶端的邏輯,保持程式的靈活性和可維護性。

解釋器模式的優缺點

說到解釋器模式的優點,它最大的魅力就在於可以輕鬆地擴展和修改語法規則。比如我們可以在不改變原有架構的情況下,輕鬆加入新的運算子號或功能,這讓解釋器模式在處理語法解析時非常靈活。

解釋器模式的缺點也很明顯。當語法規則變得越來越複雜時,類別的數量會激增,這可能導致程式碼的維護和理解變得困難。另外解釋器模式的效能並不高,尤其是當處理大規模語法時,效能的瓶頸會更加明顯。

總結

解釋器模式就像一個小型語法解析器,適合用來處理簡單的語法規則,像是計算機這樣的場景。它的擴展性讓我們可以在程式中輕鬆加入更多規則,但同時也要留意其效能和維護性。對於那些需要頻繁變更規則、處理複雜語法的情況,解釋器模式提供了一個簡潔且靈活的解決方案。

現在使用解釋器模式的情境比以前少了很多。主要是因為解釋器模式通常用在處理自定義的語法或簡單的語言規則上,而現在有很多成熟的解析工具和函式庫可以直接使用,例如正則表達式、Lex、Yacc 等等,這些工具提供了更強大、效能更高的解決方案,因此不再需要自己從頭設計和實現一個語法解析系統。

解釋器模式的缺點是當語法規則變得複雜時,維護成本會上升,因此現代系統往往採用專用解析器或虛擬機器(例如 Java 虛擬機器或 JavaScript 引擎),因此手動實現解釋器模式的必要性變少了。

儘管如此,解釋器模式在某些特定情境中仍有用武之地,特別是在需要處理簡單且可擴展的語法時,或者需要快速實現自定義的 DSL(Domain-Specific Language)時,它仍是一個不錯的選擇。

C++ 設計模式 - 橋接模式 Bridge Pattern

想像一下你家裡的電視遙控器,這個遙控器可以控制不同品牌的電視,遙控器本身的功能可能有:開機、關機、調整音量等,甚至隨著需求增加新的按鈕或功能。而電視的功能可能會隨著品牌或型號不同而有所變化。遙控器就像橋接這些不同電視的中介,讓使用者不必考慮每台電視內部的具體細節,只需要知道按下按鈕會有什麼效果。這樣的設計就是「橋接模式」的精髓:把「操作」與「具體實現」分開,讓兩者可以獨立發展。

什麼是橋接模式?

橋接模式是一種結構型設計模式,它的主要目的是將抽象部分與實現部分分離,使它們都可以獨立地變化。簡單來說這個模式可以讓你將「抽象層次」和「具體實作」分離開來,以便兩者可以各自演化而不必互相依賴。當你需要擴充系統時,無論是修改抽象部分還是實作部分,都能簡單且獨立進行。

在程式設計中,當你遇到一個類別因為實作過於複雜或多樣化而導致結構僵化時,你可以透過橋接模式將這些實作細節封裝到一個「實作介面」中,並讓抽象的類別持有該介面,從而達到靈活擴展的效果。橋接模式就像是在兩個獨立變化的維度之間搭建了一座橋樑。這座橋樑使得這兩個維度可以各自獨立地變化,而不會相互影響。

這個模式的基本角色包括:

  1. 抽象部分(Abstraction):定義了抽象的介面。維護一個對實現者(Implementor)物件的引用。
  2. 精煉抽象(Refined Abstraction):擴展抽象類,可以新增更多的功能。
  3. 實現者(Implementor):定義實現類的介面,該介面不一定要與抽象類的介面完全一致。通常只提供基本操作,而抽象類定義的介面可能會做更多更複雜的操作。
  4. 具體實現(Concrete Implementor):實現實現者介面並提供具體實現。

橋接模式在遊戲開發中的應用

橋接模式經常出現在需要靈活擴展功能的場景中。例如,在圖形繪製的應用中,我們可以有不同的形狀(比如圓形、方形),而每一種形狀可能需要用不同的方式呈現(比如螢幕顯示或列印)。在跨平台GUI系統中,不同作業系統有不同的實現方式。在資料庫程式中,可以切換不同的資料庫系統,對應到不同的資料庫介面,而不影響上層業務邏輯。

這邊我們以一個遊戲開發的例子來說明橋接模式。假設我們正在開發一個角色扮演遊戲,遊戲中有不同的角色(如戰士、法師)和不同的武器(如劍、法杖)。我們希望能夠自由地組合角色和武器,而不是為每種組合都建立一個新的類別。透過橋接模式,我們可以將「角色」與「武器」分開,未來當我們需要新增角色或武器方式時,就不必重新組合所有的類別,達到更好的擴展性。

以下是使用橋接模式實現這個需求的步驟:

首先我們定義武器的抽象介面,

1
2
3
4
5
6
// 抽象部分 Abstraction
class Weapon {
public:
virtual void use() = 0;
virtual ~Weapon() {}
};

然後我們實現具體的武器類別,

1
2
3
4
5
6
7
8
9
10
11
12
13
class Sword : public Weapon {
public:
void use() override {
std::cout << "揮舞劍攻擊" << std::endl;
}
};

class Staff : public Weapon {
public:
void use() override {
std::cout << "使用法杖施法" << std::endl;
}
};

接下來我們定義角色的抽象類別,

1
2
3
4
5
6
7
8
class Character {
protected:
Weapon* weapon;
public:
Character(Weapon* w) : weapon(w) {}
virtual void fight() = 0;
virtual ~Character() {}
};

最後我們實現具體的角色類別,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Warrior : public Character {
public:
Warrior(Weapon* w) : Character(w) {}
void fight() override {
std::cout << "戰士";
weapon->use();
}
};

class Mage : public Character {
public:
Mage(Weapon* w) : Character(w) {}
void fight() override {
std::cout << "法師";
weapon->use();
}
};

我們可以在客戶端程式碼中自由組合角色和武器,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main() {
Weapon* sword = new Sword();
Weapon* staff = new Staff();

Character* warrior = new Warrior(sword);
Character* mage = new Mage(staff);

warrior->fight(); // 輸出: 戰士揮舞劍攻擊
mage->fight(); // 輸出: 法師使用法杖施法

// 甚至可以讓戰士使用法杖,或讓法師使用劍
Character* warriorWithStaff = new Warrior(staff);
Character* mageWithSword = new Mage(sword);

warriorWithStaff->fight(); // 輸出: 戰士使用法杖施法
mageWithSword->fight(); // 輸出: 法師揮舞劍攻擊

// 釋放記憶體
delete sword;
delete staff;
delete warrior;
delete mage;
delete warriorWithStaff;
delete mageWithSword;

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
戰士揮舞劍攻擊
法師使用法杖施法
戰士使用法杖施法
法師揮舞劍攻

在這個遊戲開發的例子中,就像是在角色(Character)和武器(Weapon)之間搭建一座「橋樑」。

橋的一端是抽象部分(Abstraction),這一端是由 Character 類別及其子類(Warrior 和 Mage)所組成。這代表了角色的抽象,定義了角色應該具有的行為(如 fight() 方法)。

橋的另一端是實現部分(Implementation),這一端是由 Weapon 介面及其具體實現(Sword 和 Staff)組成。這代表了武器的實現,定義了武器應該如何被使用(如 use() 方法)。

橋樑本身的核心是在 Character 類中的 Weapon* weapon 成員變數。這個成員變數將抽象部分(角色)和實現部分(武器)連接起來。

透過橋接模式,我們可以新增新的角色(如弓箭手)或新的武器(如弓箭),而不需要修改現有的程式碼,實現了角色和武器可以獨立變化。任何角色都可以使用任何武器,這種組合是在執行時決定的,而不是在編譯時。透過橋接模式還能減少類別數量,如果不使用橋接模式,我們可能需要為每種角色與武器組合建立一個類別(如 WarriorWithSword, MageWithStaff 等),這會導致類別數量的急劇增加。

所以這個橋接模式實際上是一種靈活的連接機制,它允許角色和武器這兩個維度獨立變化,同時又能靈活地組合在一起。這就是橋接模式的核心思想:將抽象部分與其實現部分分離,使它們都可以獨立地變化。

橋接模式的優缺點

橋接模式最大的優點在於它讓抽象與實作分離,因此可以靈活地新增或修改兩者中的任意一方,而不會影響到另一方。這種鬆耦合的設計使得系統的擴展性大大提高,特別是在面對複雜的物件結構或多樣化需求時,它能有效地減少程式碼重複,並且讓系統維護變得更加容易。

橋接模式也並非在所有情況下都是最佳選擇。當你的系統結構簡單、需求較為固定時,這種設計可能會增加不必要的複雜度。另外,由於引入了額外的抽象層,開發初期可能會需要更多的規劃和設計,這在某些情況下會稍微增加開發的難度。

總結

橋接模式特別適合那些需要同時處理多個維度變化的系統。它讓抽象和實作分離,使得系統可以更靈活地演進。在現實世界中,當你面對多重需求的擴展時,橋接模式能讓你輕鬆應對各種變化,避免系統變得難以維護或過於複雜。透過合理的使用橋接模式,你可以在保持系統簡潔的同時,獲得更大的靈活性與可擴展性。

C++ 設計模式 - 責任鏈模式 Chain of Responsibility Pattern

想像你在公司工作,遇到一個問題,你先向主管反映,如果主管無法解決,再往上報告給更高層級的主管,直到有人能解決問題。這就是「責任鏈模式」的核心概念。這種模式允許你把任務逐層傳遞,直到某個物件可以處理它。在軟體設計中,這可以讓系統更加靈活,避免固定的條件判斷,提升維護性。

什麼是責任鏈模式?

責任鏈模式是一種行為型設計模式,允許多個物件按順序處理請求。每個物件有機會決定是否處理請求,或者將其轉交給下一個物件。這樣一來我們可以將不同的職責模組化,讓每個物件專注於自己的職責範圍,而不用關心整個處理流程。

想像你正在玩一個熱門的多人線上遊戲。當你遇到問題需要幫助時,你可能會先詢問遊戲內的自動客服系統。如果自動客服無法解決,問題會被轉到人工客服。如果人工客服也無法處理,問題可能會被上報給技術支援團隊。這就是一個典型的責任鏈模式應用場景。

責任鏈模式在客戶支援系統中的應用

一個常見的應用例子是客戶支援系統,當客戶送出問題時,問題可能由多個不同層級的支援人員來處理:自動客服、人工客服、技術支援團隊等。每個層級都會檢查問題,如果無法解決,就會向上級遞交,直到問題被解決。

首先我們定義一個抽象的處理者類,每個處理者 Handler(如客服)都有一個處理請求的方法,

1
2
3
4
5
6
7
8
9
10
11
// 處理者 Handler
class CustomerServiceHandler {
protected:
CustomerServiceHandler* nextHandler;
public:
virtual ~CustomerServiceHandler() = default;
virtual void setNext(CustomerServiceHandler* handler) {
nextHandler = handler;
}
virtual void handleRequest(const std::string& request) = 0;
};

然後我們實現具體的處理者類,具體的 Handler 如自動客服、人工客服和技術支援團隊繼承這個介面,並實現處理方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 具體處理者 Concrete Handlers
class AutomatedSystem : public CustomerServiceHandler {
public:
void handleRequest(const std::string& request) override {
if (request == "簡單問題") {
std::cout << "自動系統:我可以回答這個簡單問題。" << std::endl;
} else if (nextHandler != nullptr) {
nextHandler->handleRequest(request);
}
}
};

class HumanAgent : public CustomerServiceHandler {
public:
void handleRequest(const std::string& request) override {
if (request == "複雜問題") {
std::cout << "人工客服:我來處理這個複雜問題。" << std::endl;
} else if (nextHandler != nullptr) {
nextHandler->handleRequest(request);
}
}
};

class TechnicalSupport : public CustomerServiceHandler {
public:
void handleRequest(const std::string& request) override {
if (request == "技術問題") {
std::cout << "技術支援團隊:我們會深入研究並解決這個問題。" << std::endl;
} else if (nextHandler != nullptr) {
nextHandler->handleRequest(request);
} else {
std::cout << "無法解決這個問題。" << std::endl;
}
}
};

最後客戶端可以這樣使用,當客戶發出請求時,系統會從自動客服開始,如果無法處理,請求會傳遞到下一個層級,直到問題解決,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main() {
// 建立三個處理者
AutomatedSystem* autoSystem = new AutomatedSystem();
HumanAgent* humanAgent = new HumanAgent();
TechnicalSupport* techSupport = new TechnicalSupport();

// 設定責任鏈
autoSystem->setNext(humanAgent);
humanAgent->setNext(techSupport);

// 測試不同請求
autoSystem->handleRequest("簡單問題");
autoSystem->handleRequest("複雜問題");
autoSystem->handleRequest("技術問題");

delete autoSystem;
delete humanAgent;
delete techSupport;

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
自動系統:我可以回答這個簡單問題。
人工客服:我來處理這個複雜問題。
技術支援團隊:我們會深入研究並解決這個問題。

責任鏈模式的優缺點

責任鏈模式的一大優點是它讓系統更加靈活。新增一個處理者 Handler 不需要修改其他 Handler 的程式碼,只需要在責任鏈中插入新的 Handler 即可,這極大地提高了擴展性。同時 Handler 之間的耦合度降低,每個 Handler 只專注於自己能解決的問題。

然而這個模式也有一些的缺點。如果責任鏈過長請求可能會經過很多 Handler,增加了系統的處理時間。此外如果沒有適當的終止條件,請求可能會在處理鏈中無限傳遞。

總結

責任鏈模式讓我們能夠將職責分散到不同的處理者 Handler 中,提升了系統的可擴展性和維護性。在某些需要多層處理的場景,像是客戶支援系統或是事件處理系統中,這是一種非常有效的解決方案。這個模式避免了一堆 if-else 的條件判斷,讓系統更靈活但也需要注意過長的處理鏈條可能帶來的效率問題。

C++ 設計模式 - 備忘錄模式 Memento Pattern

假如你正在玩一個遊戲,過了一個困難的關卡,突然間手滑點錯選項,整個進度被重置,你會不會希望有個「存檔」功能讓你可以回到那個關卡?這正是備忘錄模式 Memento Pattern 可以幫助我們解決的問題。備忘錄模式讓我們可以在需要時保存某個物件的狀態,並在必要時恢復到之前的狀態,像是一個系統的「後悔藥」。

什麼是備忘錄模式?

備忘錄模式是一種行為型設計模式,用來保存物件的狀態,這樣就可以在未來的某個時刻還原這些狀態。這個模式提供了一個方法讓物件能夠保存自己的狀態,並且在不違反封裝原則的情況下將其恢復。備忘錄模式可以想像成一個「快照」,讓我們可以回到某個特定的時間點,而不用擔心破壞系統的其他部分。

這個模式的基本角色包括:

  1. Originator(發起者):擁有狀態並可以建立或恢復備忘錄。
  2. Memento(備忘錄):保存Originator的內部狀態,並且只有Originator可以訪問它。
  3. Caretaker(照顧者):負責管理備忘錄,但不會修改其內容,只會在需要時請求Originator恢復狀態。

備忘錄模式在文字編輯器中的應用

一個很常見的應用例子就是文字編輯器中的「復原」與「重做」功能。當你編輯檔案時,每一次的修改都可以被記錄下來,這樣當你需要復原時,可以回到先前的狀態。讓我們用一個簡單的C++範例來解釋。

首先我們需要定義一個保存狀態的備忘錄類別,

1
2
3
4
5
6
7
8
9
// Memento 備忘錄介面
class Memento {
public:
Memento(const std::string& content) : content(content) {}
std::string getContent() const { return content; }

private:
std::string content;
};

接下來我們定義發起者類別(文字編輯器),這個類別會保存當前的狀態,並且能夠建立或恢復備忘錄,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Originator 發起者類別
class TextEditor {
public:
void appendText(const std::string& text) {
content += text;
}

void setText(const std::string& text) {
content = text;
}

std::string getText() const {
return content;
}

Memento save() const {
return Memento(content);
}

void restore(const Memento& memento) {
content = memento.getContent();
}

private:
std::string content;
};

照顧者會管理這些備忘錄,但它不會直接操作它們的內容,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 照顧者類別 History
class History {
public:
void push(const Memento& memento) {
mementos.push_back(memento);
}

Memento pop() {
if (mementos.empty()) {
return Memento(""); // 若無備忘錄可恢復,回傳空狀態
}
Memento lastMemento = mementos.back();
mementos.pop_back();
return lastMemento;
}

private:
std::vector<Memento> mementos;
};

在客戶端,讓我們看看如何使用這個文字編輯器系統,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main() {
TextEditor editor;
History history;

editor.appendText("123");
history.push(editor.save());
std::cout << "Current Text: " << editor.getText() << std::endl;

editor.appendText("456");
history.push(editor.save());
std::cout << "Current Text: " << editor.getText() << std::endl;

editor.appendText("789");
std::cout << "Current Text: " << editor.getText() << std::endl;

// 恢復到之前的狀態
editor.restore(history.pop());
std::cout << "After Undo: " << editor.getText() << std::endl;

editor.restore(history.pop());
std::cout << "After Undo: " << editor.getText() << std::endl;

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
5
Current Text: 123
Current Text: 123456
Current Text: 123456789
After Undo: 123456
After Undo: 123

這個範例示範了如何使用備忘錄模式來實現文字編輯器中的復原功能。發起者(文字編輯器)負責建立和恢復備忘錄,而照顧者則管理這些狀態。

備忘錄模式的優缺點

備忘錄模式最明顯的優點是它允許我們在不影響物件封裝的前提下保存和恢復狀態,這對於系統的靈活性和可維護性非常重要。舉例來說,文字編輯器的「復原」功能就是一個非常實用的情境,它可以讓我們安心編輯,隨時恢復先前狀態,減少操作錯誤的風險。

備忘錄模式也有的缺點,當系統需要保存的狀態非常龐大時,備忘錄會佔用大量的記憶體,進而影響效能。尤其在大型系統中,若狀態保存頻繁,可能會導致記憶體使用量過高。

總結

備忘錄模式特別適合應用於那些需要保存和恢復狀態的系統中,例如文字編輯器、遊戲或設定管理工具等。它讓我們可以在操作中放心地進行改變,隨時可以回到之前的狀態。然而我們也應該根據具體情況來決定是否使用,避免不必要的記憶體消耗。

C++ 設計模式 - 中介者模式 Mediator Pattern

有時候我們在開發大型系統時,會發現各個物件之間的溝通漸漸變得複雜無比,像是織了一張錯綜複雜的蜘蛛網。每個物件彼此依賴,要新增或修改功能時牽一髮而動全身。這時候中介者模式 Mediator Pattern 就像是一個「協調者」,能讓物件之間的溝通變得有條不紊,不用每個物件都互相認識。今天我們來聊聊這個神奇的模式!

什麼是中介者模式?

中介者模式是一種行為型設計模式,用來定義一個物件,負責協調其他多個物件之間的互動。這個模式的核心概念是:物件之間不直接溝通,而是透過一個「中介者」來傳遞訊息或協調行為。這樣做的好處是,我們可以減少物件之間的耦合度,使系統更易於維護與擴展。

打個比方,想像你在公司裡面工作,當你需要和不同部門溝通時,不需要直接找每個部門的人,而是透過人力資源部門(HR)。HR 就是這裡的「中介者」,它負責協調你與各部門之間的溝通,讓事情變得更簡單明瞭。

中介者模式在聊天室應用中的實例

最經典的中介者模式應用場景之一就是「聊天室」。在一個聊天室中,每個使用者都可以發訊息給其他使用者。如果沒有中介者模式,所有使用者都需要相互認識才能互相溝通,這會讓系統變得非常混亂且難以擴展。而有了中介者後,每個使用者只需要和中介者溝通,中介者再把訊息傳遞給其他相關的使用者。

首先我們先定義中介者介面,中介者需要有一個統一的介面,定義訊息如何在物件間傳遞,

1
2
3
4
5
6
7
8
class ChatUser; // 前向宣告

// 中介者介面
class ChatRoom {
public:
virtual void sendMessage(const ChatUser* sender, const std::string& message) = 0;
virtual ~ChatRoom() = default;
};

這個具體的中介者負責管理聊天室中的使用者,並將訊息傳遞給正確的接收者,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 具體中介者
class ConcreteChatRoom : public ChatRoom {
private:
std::vector<ChatUser*> chatUsers;

public:
void addUser(ChatUser* user) {
chatUsers.push_back(user);
}

void sendMessage(const ChatUser* sender, const std::string& message) override {
for (ChatUser* user : chatUsers) {
if (user != sender) {
user->receive(sender->getName() + ": " + message);
}
}
}
};

聊天室的使用者實作一個可以和中介者溝通的介面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 參與者
class ChatUser {
private:
std::string name;
ChatRoom* chatRoom;

public:
ChatUser(const std::string& name, ChatRoom* room) : name(name), chatRoom(room) {}

void send(const std::string& message) const {
std::cout << name << " 發送消息: " << message << std::endl;
chatRoom->sendMessage(this, message);
}

void receive(const std::string& message) {
std::cout << name << " 收到消息: " << message << std::endl;
}

std::string getName() const {
return name;
}
};

在客戶端,使用者透過中介者來發送和接收訊息,而不需要知道其他使用者的存在,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 客戶端使用
int main() {
ConcreteChatRoom room;

ChatUser john("John", &room);
ChatUser alice("Alice", &room);

room.addUser(&john);
room.addUser(&alice);

john.send("Hello, Alice!");
alice.send("Hi, John!");

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
John 發送消息: Hello, Alice!
Alice 收到消息: John: Hello, Alice!
Alice 發送消息: Hi, John!
John 收到消息: Alice: Hi, John!

中介者模式的優缺點

中介者模式最大的優勢是「解耦」,它避免了各個物件直接互相依賴,這使得系統結構更加清晰,新增或移除物件時,不需要修改其他物件的程式碼。同時,這也讓我們可以更靈活地修改中介者的行為,達到自訂溝通規則的目的。

中介者模式也有其潛在的缺點。隨著系統的擴展,中介者本身可能會變得非常複雜,尤其當有很多物件要透過中介者溝通時,這個中介者可能會變成一個大型的、難以維護的類別,反而會增加系統的負擔。

總結

中介者模式特別適合應用於多個物件需要互相溝通的情況下,比如聊天室、事件處理系統等。透過中介者可以有效地減少物件之間的耦合,讓系統更加靈活。當然和其他設計模式一樣,我們也要根據具體的需求謹慎使用,避免讓中介者本身變得過於複雜。

C++ 設計模式 - 迭代器模式 Iterator Pattern

你是否曾經面對過需要遍歷一個集合、而又不希望暴露它的內部結構的情況?這就像我們在看一本書時,並不需要知道書本是如何裝訂的,只需要翻頁就好。這時迭代器模式就派上用場了。迭代器模式提供了一種簡單且一致的方式來訪問集合中的元素,讓你不必為了遍歷資料而大費周章。

什麼是迭代器模式?

迭代器模式是一種行為設計模式,它允許你逐一訪問集合物件的元素,而不需要暴露其底層的表示方式。想像你有一個儲存很多物件的容器,但你並不想揭露這些物件如何被儲存的細節。透過迭代器模式,你可以透過一個統一的界面來訪問這些元素,無論是陣列、鏈結串列還是其他資料結構,都可以輕鬆遍歷。

迭代器模式在音樂播放清單中的應用

假設我們有一個音樂播放清單,這個清單內部可能是用陣列或鏈結串列來儲存歌曲,但作為使用者,你只想能夠播放下一首歌,不關心它的儲存結構。我們可以使用迭代器模式來實現這個功能。

首先我們需要定義一個迭代器介面,這個介面會告訴使用者是否有下一首歌,並且允許他們拿到下一首歌曲,

1
2
3
4
5
6
7
// 迭代器介面
class Iterator {
public:
virtual bool hasNext() = 0;
virtual std::string next() = 0;
virtual ~Iterator() {}
};

接下來我們建立一個音樂清單類別,並且實作一個迭代器,這個迭代器會知道如何遍歷歌曲清單,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 音樂清單迭代器
class PlaylistIterator : public Iterator {
private:
std::vector<std::string> songs;
int position;
public:
PlaylistIterator(const std::vector<std::string>& songs)
: songs(songs), position(0) {}

bool hasNext() override {
return position < songs.size();
}

std::string next() override {
if (hasNext()) {
return songs[position++];
}
return "";
}
};

// 音樂清單類別
class Playlist {
private:
std::vector<std::string> songs;
public:
void addSong(const std::string& song) {
songs.push_back(song);
}

Iterator* createIterator() {
return new PlaylistIterator(songs);
}
};

在客戶端使用時,使用者不需要知道清單如何儲存歌曲,他們只需要使用迭代器提供的 next() 方法來依序播放,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
Playlist playlist;
playlist.addSong("Song 1");
playlist.addSong("Song 2");
playlist.addSong("Song 3");

Iterator* it = playlist.createIterator();
while (it->hasNext()) {
std::cout << "Playing: " << it->next() << std::endl;
}
delete it;

return 0;
}

在這個例子中,我們定義了一個 Iterator 介面,並且在 PlaylistIterator 中實作了這個介面,用來遍歷歌曲清單。使用者只需要呼叫 hasNext()next() 方法來逐步訪問清單中的歌曲,完全不用擔心清單的內部實現。

迭代器模式的優缺點

迭代器模式的優點在於它提供了一種統一的方式來遍歷集合,不論集合的內部結構如何變化。就像你可以用同樣的方法翻閱紙質書或電子書一樣,迭代器讓不同類型的集合能夠提供相同的操作方式。這使得程式碼變得更加靈活,因為你不需要修改訪問邏輯來適應不同的集合類型。

迭代器模式也有一些缺點。由於每一種集合類型都需要自己的迭代器實現,這會增加一些額外的程式碼。並且在某些情況下,如果集合的大小或複雜度較大,迭代器的操作效能可能會受到影響。除此之外,記憶體管理也需要謹慎處理。

總結

迭代器模式在需要遍歷複雜集合結構時非常實用,它提供了一個簡單、統一的操作介面。透過將遍歷行為封裝在迭代器中,程式設計師能夠專注於如何使用集合,而不是關心其內部細節。正如我們看書時只需專注於閱讀,不必擔心書本的裝訂一樣,迭代器模式讓操作集合變得更加直觀。

C++ 設計模式 - 狀態模式 State Pattern

當你在使用某些應用程式時,是否曾經發現它們的行為會隨著某些條件或狀態的改變而改變?例如音樂播放器,當它處於「播放」狀態時去按下「播放」按鈕是沒有反應的,但當處於「暫停」狀態時,按下相同按鈕就會繼續播放音樂。這樣的設計就是狀態模式的核心概念。

什麼是狀態模式?

狀態模式是一種行為型設計模式,用於當物件內部狀態發生改變時,自動改變其行為。這個模式的關鍵在於將行為與狀態解耦,讓物件在不同的狀態下有不同的表現方式。換句話說,物件的行為是由當前所處的狀態決定的,而不是依賴於外部的控制邏輯。

透過狀態模式,我們可以避免大量的 if-elseswitch 判斷,讓程式碼更加清晰並易於擴展。

狀態模式在音樂播放器中的應用

以音樂播放器為例,我們可以用狀態模式來處理不同的播放狀態,如「播放」、「暫停」和「停止」。每一個狀態都代表著播放器不同的行為,而狀態的切換可以由使用者操作或其他事件來驅動。

讓我們來看看如何一步步定義這個音樂播放器的狀態系統。

我們首先需要定義一個狀態的介面,讓不同的具體狀態類別去實現。例如,我們定義了 play()pause()stop() 這三個動作,每個狀態可以根據自己的邏輯來實現這些動作,

1
2
3
4
5
6
7
8
9
10
// 前向宣告,避免迴圈引用
class MusicPlayer;

// 狀態基底類別
class State {
public:
virtual void play(MusicPlayer* player) = 0;
virtual void pause(MusicPlayer* player) = 0;
virtual void stop(MusicPlayer* player) = 0;
};

接著我們可以定義具體的狀態類別,例如「播放中」和「暫停中」狀態。每一個具體狀態都會根據當前的情況來決定該如何處理使用者的操作,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 具體狀態:「播放中」
class PlayingState : public State {
public:
void play(MusicPlayer* player) override;

void pause(MusicPlayer* player) override;

void stop(MusicPlayer* player) override;
};

// 具體狀態:「暫停中」
class PausedState : public State {
public:
void play(MusicPlayer* player) override;

void pause(MusicPlayer* player) override;

void stop(MusicPlayer* player) override;
};

// 具體狀態:「停止中」
class StoppedState : public State {
public:
void play(MusicPlayer* player) override;

void pause(MusicPlayer* player) override;

void stop(MusicPlayer* player) override;
};

void PlayingState::play(MusicPlayer* player) {
std::cout << "Already playing." << std::endl;
}

void PlayingState::pause(MusicPlayer* player) {
std::cout << "Pausing the music." << std::endl;
player->setState(new PausedState()); // 切換到暫停狀態
}

void PlayingState::stop(MusicPlayer* player) {
std::cout << "Stopping the music." << std::endl;
player->setState(new StoppedState()); // 切換到停止狀態
}

void PausedState::play(MusicPlayer* player) {
std::cout << "Resuming the music." << std::endl;
player->setState(new PlayingState()); // 切換回播放狀態
}

void PausedState::pause(MusicPlayer* player) {
std::cout << "Already paused." << std::endl;
}

void PausedState::stop(MusicPlayer* player) {
std::cout << "Stopping the music." << std::endl;
player->setState(new StoppedState()); // 切換到停止狀態
}

void StoppedState::play(MusicPlayer* player) {
std::cout << "Starting to play music." << std::endl;
player->setState(new PlayingState()); // 切換到播放狀態
}

void StoppedState::pause(MusicPlayer* player) {
std::cout << "Can't pause. The music is stopped." << std::endl;
}

void StoppedState::stop(MusicPlayer* player) {
std::cout << "Already stopped." << std::endl;
}

音樂播放器本身只需要依賴狀態物件來決定它的行為,當狀態發生變化時,播放器自動切換狀態。這樣播放器不需要自己處理具體行為,所有的行為交給不同狀態來處理,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 播放器類別
class MusicPlayer {
private:
State* state; // 當前狀態
public:
MusicPlayer(State* initState) : state(initState) {}

~MusicPlayer() {
delete state; // 釋放當前狀態
}

void setState(State* newState) {
delete state; // 切換狀態前釋放舊狀態
state = newState;
}

void play() {
state->play(this);
}

void pause() {
state->pause(this);
}

void stop() {
state->stop(this);
}
};

現在我們可以在客戶端直接使用這個播放器來進行播放、暫停、停止操作,

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
// 建立播放器並初始化為停止狀態
MusicPlayer musicPlayer(new StoppedState());

// 嘗試播放、暫停、停止操作
musicPlayer.play(); // 開始播放
musicPlayer.pause(); // 暫停播放
musicPlayer.play(); // 恢復播放
musicPlayer.stop(); // 停止播放

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
Starting to play music.
Pausing the music.
Resuming the music.
Stopping the music.

狀態模式的優缺點

狀態模式的優點是它可以讓物件的行為與其狀態緊密相關,當狀態變化時,行為也會隨之變化,這讓系統變得更加靈活且容易擴展。舉例來說,如果未來想新增「快進」狀態,我們只需定義新的 FastForwardState 類別,然後無需改動其他類別的邏輯。

狀態模式的缺點在於會增加類別的數量。每個狀態都必須有一個對應的類別,這在狀態非常多的情況下,可能會讓程式變得繁瑣。另外動態切換狀態的過程中,可能會有一定的記憶體管理問題,需要小心處理。

總結

狀態模式特別適合那些行為會隨著狀態改變而改變的系統。透過將狀態封裝成獨立的類別,系統可以變得更具擴展性,且更加容易維護。雖然可能會引入更多的類別和稍微複雜的結構,但它能有效地減少程式中繁瑣的判斷邏輯,讓程式碼更加清晰易懂。

C++ 設計模式 - 門面模式 Facade Pattern

想像你剛買了一台全新的家電,同時是一台智慧家居控制中心。這台裝置可以控制家裡的燈光、空調、音響,甚至幫你泡咖啡!不過當你打開說明書,發現需要安裝十幾個不同的應用程式來單獨操控每個裝置時,這一切變得繁瑣起來。於是你會開始想如果有一個簡單的按鈕讓你輕鬆掌控所有裝置那該多好!這就是門面模式 Facade Pattern 的可以發揮的地方。門面模式讓我們可以簡化複雜系統的使用介面,將多個操作包裝成一個簡單的介面,讓使用者感到舒適和便利。

什麼是門面模式?

門面模式是一種結構型設計模式,目的是為複雜系統提供一個簡單的介面,透過一個門面類別來隱藏系統的細節,讓使用者無需關心底層的運作方式。這個模式的核心思想就是隱藏系統的內部複雜度,對外提供一個簡單、統一的操作入口。

具體來說,門面模式並不改變系統內部的運作邏輯,而是提供一個統一的「門面」來簡化與系統的互動。使用者只需透過這個門面進行操作,而無需了解系統內部的多個模組是如何協同工作的。

門面模式在視訊轉換中的應用

視訊轉換是一個非常複雜的過程,牽涉到解碼、轉碼、編碼、壓縮等多個步驟。假設你正在開發一個應用程式,目標是將一段視訊從一種格式轉換成另一種格式。如果讓使用者手動操作每個步驟,這對非技術背景的使用者來說無疑是個大挑戰。而門面模式非常適合這樣的應用場景。

首先我們可以想像這個視訊轉換系統內部包含了多個模組,比如 DecoderEncoderCompressorAudioProcessorSubtitleHandlerResolutionAdjuster,它們負責不同的視訊處理任務。為了讓使用者不需要關心這些細節,我們可以建立一個門面 VideoConverterFacade,讓這些模組協同工作。

內部模組可能是這樣的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Decoder {
public:
void decode(const std::string& file) {
std::cout << "Decoding " << file << std::endl;
}
};

class Encoder {
public:
void encode(const std::string& format) {
std::cout << "Encoding to " << format << std::endl;
}
};

class Compressor {
public:
void compress() {
std::cout << "Compressing video" << std::endl;
}
};

class AudioProcessor {
public:
void processAudio() {
std::cout << "Processing audio..." << std::endl;
}
};

class SubtitleHandler {
public:
void addSubtitle(const std::string& subtitleFile) {
std::cout << "Adding subtitle: " << subtitleFile << std::endl;
}
};

class ResolutionAdjuster {
public:
void adjustResolution(int width, int height) {
std::cout << "Adjusting resolution to " << width << "x" << height << std::endl;
}
};

接著我們來實現門面類別,讓它來協調這些模組的工作,並提供一個簡單的操作方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class VideoConverterFacade {
private:
Decoder decoder;
Encoder encoder;
Compressor compressor;
AudioProcessor audioProcessor;
SubtitleHandler subtitleHandler;
ResolutionAdjuster resolutionAdjuster;

public:
void convertVideo(const std::string& file, const std::string& format,
const std::string& subtitleFile, int width, int height) {

decoder.decode(file); // 解碼
audioProcessor.processAudio(); // 處理音訊
subtitleHandler.addSubtitle(subtitleFile); // 新增字幕
resolutionAdjuster.adjustResolution(width, height); // 調整解析度
compressor.compress(); // 壓縮影片
encoder.encode(format); // 編碼
}
};

現在我們可以在客戶端直接使用這個門面來進行視訊轉換,而無需了解內部的解碼、音訊、新增字幕、調整解析度、編碼和壓縮步驟,

1
2
3
4
5
6
int main() {
VideoConverterFacade converter;
converter.convertVideo("movie.mp4", "avi", "movie.srt", 1920, 1080);

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
5
6
Decoding movie.mp4
Processing audio...
Adding subtitle: movie.srt
Adjusting resolution to 1920x1080
Compressing video
Encoding to avi

在這個例子中,我們把多個步驟簡化成一個 convertVideo 方法,這讓使用者無需關心背後的具體操作,只需要指定原始檔案和目標格式即可完成任務。

門面模式的優缺點

門面模式的好處顯而易見,它簡化了使用者的操作體驗,讓他們不需要理解系統的複雜內部結構。這種方式提高了系統的可用性,尤其是當你有一個由多個子系統組成的複雜應用時,門面可以提供一個統一的入口,減少使用者的學習成本和操作壓力。

同時門面模式也提升了系統的模組化,因為每個子系統依然保持相對獨立。如果需要修改內部實現或新增新功能,只需要改動子系統,門面本身不必大幅修改,這增強了系統的可擴展性。

但門面模式也有缺點。當系統需求變得更加複雜時,門面的簡單操作可能無法滿足高階使用者的需求。他們可能希望能夠更深入地控制系統,這時門面反而可能會限制他們的靈活性。另外如果過度依賴門面來隱藏內部邏輯,可能會導致某些使用者忽視了系統真正的運作方式,導致未來難以進行細微的調整和除錯。

總結

門面模式特別適合應用在那些需要隱藏系統複雜性的場景中。它透過提供一個簡單的介面,讓使用者能夠方便地與系統互動,無需深入了解內部細節。無論是在視訊轉換這樣的多步驟過程中,還是在其他複雜系統中,門面模式都能讓使用體驗更加友好且直觀。

使用門面模式時也要權衡簡化操作與系統靈活性之間的平衡,確保系統在簡單易用的同時,也能滿足高階使用者的進階需求。畢竟簡單不代表犧牲功能,這就是門面模式所帶來的設計智慧。

C++ 設計模式 - 組合模式 Composite Pattern

你有沒有遇過要同時處理單一物件與一群物件?就像在資料夾裡,你可以打開一個單獨的檔案,也可以打開一個資料夾,裡面可能包含了更多檔案或其他資料夾。這樣的層級結構看似複雜,但對使用者來說,我們希望能像操作單一檔案那樣簡單。這就是組合模式 Composite Pattern 的魔力所在,讓我們可以將單一物件與多個物件統一處理,創造出一個靈活且可擴展的層級架構。

什麼是組合模式?

組合模式是一種結構型設計模式,它讓你能夠像處理單一物件一樣去操作物件的集合。在這個模式中,我們可以將物件組織成樹狀結構,透過將單一物件和組合物件視為同一個介面,實現對單一物件和複合物件的統一操作。

簡單來說,組合模式解決了『如何讓樹狀結構中的物件與物件集合能被同樣對待』的問題。例如在一個UI系統中,按鈕、文字框等單一元素是葉子節點,而 Window、Panel 則是組合節點。我們希望能夠以相同的方式新增、移除和操作這些不同的元素,而不必區分它們是單一物件還是組合物件。

組合模式通常包含以下角色:

  1. 元件(Component):為所有具體元件和組合類定義共同的介面。
  2. 葉節點(Leaf):表示組合中的葉節點對象,葉節點沒有子節點。
  3. 組合(Composite):表示複雜元件,包含其他元件(可以是葉節點或其他複雜元件)。
  4. 客戶端(Client):通過元件介面與所有對象進行交互。

組合模式在圖形編輯器中的應用

讓我們來看一個圖形編輯器的例子。在圖形編輯器中,舉例:小畫家或其他類似工具,你可能會有單一的基本圖形(如圓形、矩形),也可能會有由多個基本圖形組成的複合圖形。組合模式讓我們可以使用相同的方式來操作這些基本圖形和複合圖形,無需關心它們的具體類型。

我們先定義一個圖形的基礎介面 Graphic,讓所有圖形(單一圖形或組合圖形)都可以遵循相同的操作方式,例如:drawmove

1
2
3
4
5
6
class Graphic {
public:
virtual void draw() const = 0;
virtual void move(int x, int y) = 0;
virtual ~Graphic() = default;
};

接著我們來實現單一的圖形類別,比如 CircleRectangle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle : public Graphic {
public:
void draw() const override {
std::cout << "Drawing Circle\n";
}

void move(int x, int y) override {
std::cout << "Move the Circle to (" << x << ", " << y << ")\n";
}
};

class Rectangle : public Graphic {
public:
void draw() const override {
std::cout << "Drawing Rectangle\n";
}

void move(int x, int y) override {
std::cout << "Move the Rectangle to (" << x << ", " << y << ")\n";
}
};

現在我們實現一個組合圖形類別 CompositeGraphic,它可以包含多個圖形,不管是單一圖形還是其他複合圖形,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class CompositeGraphic : public Graphic {
private:
std::vector<Graphic*> graphics;

public:
void add(Graphic* graphic) {
graphics.push_back(graphic);
}

void draw() const override {
for (const auto& graphic : graphics) {
graphic->draw();
}
}

void move(int x, int y) override {
std::cout << "Move graphic to (" << x << ", " << y << "):\n";
for (auto& graphic : graphics) {
graphic->move(x, y);
}
}
};

在這裡我們將建立單一的圖形物件和組合的圖形物件,並使用相同的方式來操作它們,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main() {
// 建立單一圖形
Circle circle;
Rectangle rectangle;

// 建立組合圖形
CompositeGraphic composite;
composite.add(&circle);
composite.add(&rectangle);

// 再次建立一個複合圖形並巢狀
CompositeGraphic complexComposite;
complexComposite.add(&composite);
complexComposite.add(&circle);

// 繪製所有圖形
std::cout << "Drawing composite graphic:\n";
complexComposite.draw();

// 移動所有圖形
std::cout << "Move composite graphic:" << std::endl;
complexComposite.move(10, 20);

return 0;
}

執行上述程式碼,我們會得到以下輸出:

1
2
3
4
5
6
7
8
9
10
Drawing composite graphic:
Drawing Circle
Drawing Rectangle
Drawing Circle
Move composite graphic:
Move graphic to (10, 20):
Move graphic to (10, 20):
Move the Circle to (10, 20)
Move the Rectangle to (10, 20)
Move the Circle to (10, 20)

在這個例子中,CompositeGraphic 允許我們將單一圖形和其他組合圖形一起處理,無需區分它們是單一圖形還是由多個圖形組成的組合。這樣一來整個圖形編輯器的結構變得更加靈活,讓我們能夠輕鬆管理複雜的圖形組合。

組合模式的優缺點

組合模式的最大優勢在於它讓系統結構更加簡單和靈活。無論我們處理的是單一物件還是複合物件,都可以使用同樣的介面,這降低了處理不同類型物件時的複雜性。另外組合模式讓我們可以輕鬆地擴展系統,只需新增更多類型的物件或組合物件,而不必修改現有的程式碼。

組合模式也帶來了額外的設計複雜度。當我們的物件結構變得非常複雜時,理解和管理這些層級可能會變得困難。此外對於只需要處理單一物件的情況來說,組合模式的額外靈活性可能是多餘的,反而會增加系統的負擔。

總結

組合模式讓我們能夠用統一的方式處理單一物件和複合物件。它廣泛應用於各種層級結構中,例如圖形編輯器、檔案系統、UI 元素等。雖然這個模式能夠簡化對複雜物件結構的處理,但它也可能帶來一些設計上的挑戰,因此在使用時需要謹慎考量其適用性。

當你下次面對需要同時處理單一和多個物件的情況時,組合模式可能正是解決問題的鑰匙。無論是單一物件還是複合物件,最終它們都應該能夠被簡單而一致地操作,這就是組合模式的魅力所在。