學習目標

讀完本章並完成練習後,你應該能夠:

  1. 解釋 SOLID 五大原則,並用 C++ 範例說明每一條如何改善設計
  2. 區分創建型、結構型、行為型三大類設計模式
  3. 實作執行緒安全的 Singleton(Meyers' Singleton),並說出它的批評與替代方案(依賴注入)
  4. 實作 Observer 模式(push/pull、傳統繼承版與 std::function 現代版)
  5. 實作 Strategy 模式(類別繼承版與 std::function 版),並判斷何時用哪種
  6. 說明 Factory Method 與 Abstract Factory 的差異與用途
  7. 理解 RAII 是「C++ 最重要的慣用法」,並能用它管理任意資源
  8. 認識 PIMPL 慣用法、組合優於繼承、Rule of Zero/Five、const 正確性、值語意
  9. 偏好標準演算法、智慧指標而非裸迴圈與裸指標
  10. 對照 C++ Core Guidelines 寫出可維護的現代 C++

一、設計原則的價值:為什麼要學這些?

寫出「能跑」的程式很容易;寫出「容易理解、容易修改、不容易出錯」的程式才是工程。設計原則與設計模式就是前人累積下來、用來對抗軟體複雜度的詞彙與工具。它們的共同目標是:

  • 降低耦合(coupling):模組之間的牽連越少,改一處就不會牽動全身
  • 提高內聚(cohesion):相關的東西放在一起,職責清楚
  • 隔離變化:把「會變的部分」與「穩定的部分」分開,讓變更局部化

重要心態:設計模式是「解決特定問題的範本」,不是「越多越好的徽章」。過度設計(over-engineering)和缺乏設計一樣有害。先讓程式正確、簡單,當複雜度真的浮現時,才引入對應的模式。


二、SOLID 原則(逐條詳解)

SOLID 是五個物件導向設計原則的首字母縮寫,由 Robert C. Martin 整理。

原則 全名 一句話
S 單一職責(Single Responsibility) 一個類別只應有一個改變的理由
O 開放封閉(Open/Closed) 對擴充開放,對修改封閉
L 里氏替換(Liskov Substitution) 子型別必須能無縫替換父型別
I 介面隔離(Interface Segregation) 不要強迫客戶端依賴它用不到的方法
D 依賴反轉(Dependency Inversion) 依賴抽象,不要依賴具體

S — 單一職責原則

一個類別若同時負責「業務邏輯」「存檔」「列印」,那麼這三者任一改變都得改它。拆開來:

// ✗ 一個類別做太多事
class Report {
    void compute();       // 業務
    void saveToFile();    // 持久化
    void printToConsole();// 呈現
};

// ✓ 各司其職
class Report     { /* 只負責資料與計算 */ };
class ReportSaver{ void save(const Report&); };
class ReportPrinter{ void print(const Report&); };

O — 開放封閉原則

新增功能時,應該「加程式碼」而不是「改既有程式碼」。每次回去改 switch 都是違反此原則的訊號:

// ✗ 每新增一種圖形就要改這個函式
double area(const Shape& s) {
    switch (s.type) {
        case CIRCLE:    return ...;
        case RECTANGLE: return ...;
        // 新增三角形 → 又得回來改這裡
    }
}

// ✓ 用多型:新增圖形只需新增一個類別,不動既有程式碼
struct Shape { virtual double area() const = 0; virtual ~Shape() = default; };
struct Circle    : Shape { double area() const override { /* ... */ } };
struct Rectangle : Shape { double area() const override { /* ... */ } };

L — 里氏替換原則

子類別必須真正「是一個」父類別,能在任何用到父類別的地方替換而不破壞正確性。經典反例是「正方形繼承長方形」:若 setWidth 同時改了高度,就違反了長方形的契約。

// ✗ Square 繼承 Rectangle,但改寫 setWidth/setHeight 破壞了基底契約
//   「設定寬度不影響高度」這個假設被打破 → 用 Rectangle& 的程式碼會出錯

判斷準則:不要為了重用程式碼而繼承。若子類別需要「拿掉」或「違反」父類別的行為,就不該繼承。

I — 介面隔離原則

寧可有多個小而專一的介面,也不要一個肥大的介面逼迫實作者去實作用不到的方法:

// ✗ 肥介面:不會列印的機器也被迫實作 print()
struct IMachine { virtual void print()=0; virtual void scan()=0; virtual void fax()=0; };

// ✓ 拆成小介面,依需求組合
struct IPrinter { virtual void print() = 0; virtual ~IPrinter() = default; };
struct IScanner { virtual void scan()  = 0; virtual ~IScanner() = default; };

D — 依賴反轉原則

高階模組不該依賴低階模組,兩者都該依賴抽象

// ✗ 高階的 NotificationService 直接依賴具體的 EmailSender
class NotificationService { EmailSender sender_; /* 想換成簡訊就得改這裡 */ };

// ✓ 依賴抽象介面,具體實作從外部注入(這就是「依賴注入」)
struct IMessageSender { virtual void send(const std::string&) = 0; virtual ~IMessageSender() = default; };
class NotificationService {
    IMessageSender& sender_;   // 依賴抽象
public:
    explicit NotificationService(IMessageSender& s) : sender_(s) {}
};

依賴反轉是後面 Singleton 替代方案、Strategy 模式的理論基礎。


三、設計模式三大分類

GoF(Gang of Four)把 23 個經典模式分成三類:

分類 解決的問題 代表模式
創建型(Creational) 物件如何被建立 Singleton、Factory Method、Abstract Factory、Builder、Prototype
結構型(Structural) 物件如何組合成更大的結構 Adapter、Decorator、Facade、Proxy、Composite、PIMPL(C++ 特有)
行為型(Behavioral) 物件之間如何溝通與分配職責 Observer、Strategy、Command、State、Iterator、Visitor

本章深入講解最常用的 Singleton、Observer、Strategy、Factory,其餘點到為止。


四、Singleton 模式(創建型)

目的

確保一個類別只有一個實例,並提供全域存取點。常見於 Logger、設定管理器、連線池。

演進:從錯誤示範到正確做法

Naive 版本(請勿使用)

class Singleton {
    static Singleton* instance_;
public:
    static Singleton* get() {
        if (instance_ == nullptr)        // 多執行緒下兩個執行緒可能同時通過
            instance_ = new Singleton(); // → 建立多個實例 + 記憶體洩漏
        return instance_;
    }
};

問題:非執行緒安全、用了 new 卻沒人 delete、可被複製。

Meyers' Singleton(推薦)

利用 C++11 標準保證的「靜態區域變數初始化是執行緒安全的」:

class Singleton {
public:
    static Singleton& instance() {
        static Singleton inst;   // C++11 保證:只初始化一次,且執行緒安全
        return inst;
    }
    Singleton(const Singleton&) = delete;             // 禁止複製
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;                  // 禁止移動
    Singleton& operator=(Singleton&&) = delete;
private:
    Singleton() = default;
    ~Singleton() = default;
};

優點:簡潔、執行緒安全、自動解構(無記憶體洩漏)、回傳參考避免被誤刪。

對 Singleton 的批評

Singleton 雖然方便,但是最常被濫用、也最受批評的模式之一:

  1. 本質上是全域狀態——增加耦合,任何地方都能偷偷依賴它
  2. 隱藏依賴關係——函式簽章看不出它用了哪些 Singleton,難以理解
  3. 難以單元測試——無法替換成 mock,測試之間還會互相污染狀態
  4. 初始化順序問題——多個跨檔案全域物件的解構順序不確定

替代方案:依賴注入(DI)

與其讓類別自己去抓全域實例,不如把它需要的東西當參數傳進去

// ✗ 內部抓 Singleton:隱藏依賴、難測試
class OrderService {
    void process() { Logger::instance().info("..."); }
};

// ✓ 依賴注入:依賴明確、測試時可傳入 mock logger
class OrderService {
    ILogger& logger_;
public:
    explicit OrderService(ILogger& logger) : logger_(logger) {}
    void process() { logger_.info("..."); }
};

準則:先問「這個東西真的必須全域唯一嗎?」多數情況下,依賴注入更靈活、更好測試。Monostate(所有實例共享 static 狀態)也是一種替代。

對應程式碼singleton.cpp 示範 naive → Meyers' 的演進、Logger 與 ConfigManager 實例,以及 Monostate / DI 等替代方案。


五、Observer 模式(行為型)

目的

定義物件間的一對多依賴:當「主題」(Subject)狀態改變時,所有「觀察者」(Observer)自動收到通知。事件系統、GUI、MVC 架構的核心。

push vs pull

模型 做法 取捨
push(推) Subject 通知時把資料一起送出 update(data) 觀察者拿到現成資料,但 Subject 得決定送什麼
pull(拉) Subject 只通知「變了」,觀察者自己回頭問 subject.getState() 觀察者只取需要的,但需保留 Subject 參考

傳統實作(類別繼承)

struct Observer {
    virtual void update(const std::string& event, const std::string& data) = 0;
    virtual ~Observer() = default;
};

class Subject {
    std::vector<Observer*> observers_;
public:
    void subscribe(Observer* o)   { observers_.push_back(o); }
    void unsubscribe(Observer* o) {
        observers_.erase(std::remove(observers_.begin(), observers_.end(), o),
                         observers_.end());
    }
    void notify(const std::string& ev, const std::string& data) {
        for (auto* o : observers_) o->update(ev, data);
    }
};
        ┌──────────► Observer A (室內顯示)
Subject │ notify()
(氣象站)─┼──────────► Observer B (手機 App)
        │
        └──────────► Observer C (警報系統)

現代 C++ 實作(std::function 回呼)

不必為每個觀察者定義一個類別,直接註冊 lambda——更靈活、更少樣板:

class EventSystem {
public:
    using Id = int;
    using Callback = std::function<void(const std::string&)>;

    Id on(const std::string& ev, Callback cb) {       // 訂閱,回傳 id
        Id id = next_++;
        listeners_[ev].push_back({id, std::move(cb)});
        return id;
    }
    void off(const std::string& ev, Id id) { /* 依 id 移除 */ }
    void emit(const std::string& ev, const std::string& data) {
        if (auto it = listeners_.find(ev); it != listeners_.end())
            for (auto& l : it->second) l.cb(data);
    }
private:
    struct Listener { Id id; Callback cb; };
    std::unordered_map<std::string, std::vector<Listener>> listeners_;
    Id next_ = 0;
};

// 使用:
events.on("user:login", [](const std::string& u){ std::cout << u << " 登入\n"; });
events.emit("user:login", "Alice");

懸空觀察者陷阱

傳統版本用裸指標 Observer*,若觀察者先被銷毀卻沒 unsubscribenotify 時就會存取已釋放記憶體(UB)。解法:讓觀察者在解構時自動退訂(RAII,見對應程式碼的 ManagedObserver),或用 std::weak_ptr 持有觀察者。

對應程式碼observer.cpp 涵蓋傳統氣象站、std::function 版 EventSystem、型別安全的 Signal<Args...> Signal/Slot 系統,以及自動退訂的生命週期管理。


六、Strategy 模式(行為型)

目的

定義一系列演算法,各自封裝起來使其可互換。讓演算法的變化獨立於使用它的客戶端。

傳統實作(類別繼承)

struct SortStrategy {
    virtual void sort(std::vector<int>&) = 0;
    virtual ~SortStrategy() = default;
};
struct BubbleSort : SortStrategy { void sort(std::vector<int>& d) override { /* ... */ } };
struct QuickSort  : SortStrategy { void sort(std::vector<int>& d) override { /* ... */ } };

class Sorter {                       // Context
    std::unique_ptr<SortStrategy> strategy_;
public:
    void set_strategy(std::unique_ptr<SortStrategy> s) { strategy_ = std::move(s); }
    void sort(std::vector<int>& d) { strategy_->sort(d); }
};

現代 C++ 替代(std::function)

當策略很簡單(一兩個函式)時,用 std::function 省去定義一堆類別:

class Sorter {
    std::function<void(std::vector<int>&)> strategy_;
public:
    void set_strategy(std::function<void(std::vector<int>&)> s) { strategy_ = std::move(s); }
    void sort(std::vector<int>& d) { strategy_(d); }
};

Sorter s;
s.set_strategy([](std::vector<int>& v){ std::sort(v.begin(), v.end()); });          // 升序
s.set_strategy([](std::vector<int>& v){ std::sort(v.rbegin(), v.rend()); });        // 降序

兩種做法的比較

比較項 類別繼承版 std::function
需要定義類別 否(直接 lambda)
持有狀態 自然(成員變數) 可(lambda 捕獲)
適合 複雜策略、多個方法 簡單策略、一兩個函式
執行期多型 直接支援 透過 std::function
組合靈活度 較低 較高

Strategy 其實就是「依賴反轉 + 把演算法當參數」的具體化。std::sort 的比較器、std::function 計算器都是 Strategy 的日常應用。

對應程式碼strategy.cpp 涵蓋排序策略(繼承版與 function 版)、付款策略、壓縮策略、可擴充計算器,以及兩種做法的比較。


七、Factory 模式(創建型)

把「物件的建立邏輯」封裝起來,讓客戶端不必知道具體類別。

Factory Method(工廠方法)

用一個函式根據參數決定建立哪個具體型別,回傳基底類別的智慧指標:

std::unique_ptr<Shape> create_shape(const std::string& type) {
    if (type == "circle")    return std::make_unique<Circle>();
    if (type == "rectangle") return std::make_unique<Rectangle>();
    return nullptr;
}

Abstract Factory(抽象工廠)

提供一個介面,用來建立「一整族相關的物件」,而不指定具體類別。經典例子是跨平台 UI:

struct GuiFactory {                            // 抽象工廠
    virtual std::unique_ptr<Button>   create_button()   = 0;
    virtual std::unique_ptr<Checkbox> create_checkbox() = 0;
    virtual ~GuiFactory() = default;
};
struct WindowsFactory : GuiFactory { /* 產生 Windows 風格元件 */ };
struct MacFactory     : GuiFactory { /* 產生 macOS 風格元件 */ };
// 客戶端只依賴 GuiFactory,換工廠就換整套風格
模式 建立的東西 用途
Factory Method 單一產品(依參數決定具體型別) 隱藏 new、集中建立邏輯
Abstract Factory 一整族相關產品 確保產品彼此搭配、整批替換

八、C++ 慣用法與最佳實踐

RAII —— C++ 的「主模式」(master pattern)

Resource Acquisition Is Initialization:在建構子取得資源、在解構子釋放。這是 C++ 異於其他語言、最重要的慣用法——它讓「資源管理」搭上「物件生命週期」的順風車,即使發生例外也保證釋放

// ✓ RAII:自動釋放,例外安全
{
    std::lock_guard<std::mutex> lock(mtx);          // 自動解鎖
    auto w = std::make_unique<Widget>();            // 自動 delete
    std::ifstream file("data.txt");                  // 自動關檔
}   // ← 離開作用域,全部自動清理(即使中途拋例外)

// ✗ 手動管理:任何一處拋例外就洩漏
mtx.lock();
Widget* w = new Widget();
// ... 若這裡拋例外,下面兩行不會執行 → 洩漏 + 死鎖
delete w;
mtx.unlock();

鎖、記憶體、檔案、socket、資料庫連線——全部都該用 RAII 包裝。智慧指標、lock_guardfstreamstd::vector 都是 RAII 的體現。

Rule of Zero / Rule of Five

規則 內容
Rule of Zero 若你的類別不直接管理資源(用智慧指標、容器等 RAII 成員),就不要自定義解構子、複製/移動操作,全部交給編譯器
Rule of Five 若你必須自定義其中一個(解構子、複製建構、複製賦值、移動建構、移動賦值),通常五個都要處理
// ✓ Rule of Zero:成員都是 RAII 型別,編譯器自動產生正確的特殊函式
class Person {
    std::string name_;
    std::vector<int> scores_;
    std::unique_ptr<Address> address_;
    // 不需要寫任何解構子/複製/移動!
};

C++ Core Guidelines C.20:能不自定義特殊成員函式就不要寫(Rule of Zero)。

const 正確性

凡是不修改物件的成員函式都標 const,凡是不需修改的參數都用 const& 傳遞。這讓編譯器幫你抓錯、讓介面意圖清晰、也是執行緒安全的基礎(多執行緒同讀 const 物件不需鎖)。

class Container {
    std::vector<int> data_;
public:
    std::size_t size() const { return data_.size(); }   // 不改狀態 → const
    const int& at(std::size_t i) const { return data_[i]; }  // const 版
    int& at(std::size_t i)             { return data_[i]; }  // 非 const 版(可改)
};
void process(const std::vector<int>& data);   // 只讀 → const&

組合優於繼承

繼承是 C++ 最緊的耦合之一。除非真的需要執行期多型(is-a 關係),否則優先用組合(has-a):

// ✗ 繼承:強耦合、易違反里氏替換
class LoggingSocket : public Socket { /* ... */ };

// ✓ 組合:鬆耦合、更靈活
class Connection {
    Socket socket_;     // 「有一個」socket
    Logger logger_;     // 「有一個」logger
};

C++ Core Guidelines C.129:設計類別階層時,區分「介面繼承」與「實作繼承」;需要重用時優先用組合。

智慧指標優於裸指標

std::unique_ptr(獨佔所有權)、std::shared_ptr(共享所有權)取代 new/delete。裸指標只用於「不擁有」的觀察用途。

auto w  = std::make_unique<Widget>();   // 獨佔
auto sp = std::make_shared<Widget>();   // 共享(有引用計數)
Widget* observer = w.get();             // 觀察用,不負責釋放

C++ Core Guidelines R.11:避免顯式呼叫 new/deleteR.20/R.21:用 unique_ptrshared_ptr 表達所有權,優先 unique_ptr

值語意(value semantics)

C++ 鼓勵把物件當「值」傳遞與複製(像 int 一樣),而非到處傳指標。配合移動語意,值語意既安全(無別名、無懸空指標)又高效。std::stringstd::vector 都是值型別的典範。

偏好標準演算法與 ranges

No raw loops」——能用標準演算法表達的就別手寫迴圈,意圖更清楚、更不易出錯:

// ✗ 手寫迴圈
int sum = 0;
for (std::size_t i = 0; i < v.size(); ++i) sum += v[i];

// ✓ 標準演算法
int sum = std::accumulate(v.begin(), v.end(), 0);

// ✓ C++20 ranges(更易讀、可組合)
// auto evens = v | std::views::filter([](int x){ return x % 2 == 0; });

C++ Core Guidelines SL.alg / ES.1:優先使用標準函式庫與演算法,而非手寫迴圈。

PIMPL 慣用法(Pointer to IMPLementation)

把實作細節藏到一個前向宣告的 Impl 類別,標頭只放一個指標。好處:降低編譯依賴(改實作不必重編譯用戶端)、隱藏細節、穩定 ABI。

// widget.h —— 對外只暴露介面,不洩漏實作細節
class Widget {
public:
    Widget();
    ~Widget();                       // 必須在 .cpp 定義(此時 Impl 才是完整型別)
    void draw();
private:
    struct Impl;                     // 前向宣告
    std::unique_ptr<Impl> pimpl_;    // 指向實作
};

代價:多一次堆積配置與間接存取。適合用在編譯時間長、或需穩定 ABI 的函式庫邊界。


九、C++ Core Guidelines 重點摘要

編號 建議 說明
C.20 Rule of Zero 能不自定義特殊成員函式就不要寫
C.21 Rule of Five 要自定義其中一個,就處理全部五個
C.129 組合優於實作繼承 區分介面繼承與實作繼承
R.11 避免 new/delete 用智慧指標或容器
R.20/R.21 用智慧指標表達所有權 優先 unique_ptr
ES.1 偏好標準函式庫 而非手刻
ES.20 一律初始化物件 避免未定義值
F.16 只讀參數用 const& 不需修改就傳 const reference
Con.1~4 const 正確性 預設 const,需要才放寬
C.35 多型基底類別的解構子 應為 public virtualprotected non-virtual
Enum.3 偏好 enum class 取代未限定範圍的 enum
ES.50 不要 const_cast 去除 const 違反 const 契約是 UB 來源
雜項 override / nullptr / [[nodiscard]] 明確標記覆寫、用 nullptr、標註不可忽略的回傳值

十、撰寫可維護的現代 C++(綜合建議)

  1. 先求正確與簡單,再談模式:YAGNI(You Aren't Gonna Need It),不要為了想像中的需求預先抽象。
  2. 讓不合法的狀態無法表示:用型別系統表達約束(強型別、enum class、不變式建構子)。
  3. 預設 const、預設 private:需要時才放寬,縮小可變範圍與可見範圍。
  4. 小函式、小類別、單一職責:好命名勝過好註解。
  5. 資源一律 RAII:絕不手動 new/deletelock/unlockopen/close
  6. 偏好值語意與不可變資料:減少別名與共享狀態,天然更安全。
  7. 介面用抽象、實作靠注入:依賴反轉讓程式可測試、可替換。
  8. 善用標準函式庫:容器、演算法、<chrono><random>、智慧指標都比自己造輪子可靠。
  9. 打開警告並當錯誤-Wall -Wextra -Werror,搭配 sanitizers(-fsanitize=address,undefined)。
  10. 寫測試:可測試性本身就會逼你做出鬆耦合的好設計。

重點整理

模式 / 慣用法 分類 用途 關鍵特徵
Singleton 創建型 全域唯一實例 私有建構子、Meyers' static instance
Factory Method 創建型 封裝單一物件建立 回傳基底類別智慧指標
Abstract Factory 創建型 建立整族相關物件 工廠介面 + 多套具體工廠
Observer 行為型 一對多事件通知 subscribe / notify / unsubscribe
Strategy 行為型 可互換演算法 策略介面或 std::function + Context
RAII 慣用法 資源管理 建構取得、解構釋放,例外安全
PIMPL 結構型慣用法 隱藏實作、降低編譯依賴 前向宣告 Impl + unique_ptr

常見錯誤與陷阱

  1. 過度使用設計模式:為了用模式而用模式,反而增加複雜度。只在真正需要時引入。
  2. Singleton 濫用:把它當全域變數的藉口。優先考慮依賴注入。
  3. C++11 前的雙重檢查鎖定(DCLP):容易出錯,直接用 Meyers' Singleton。
  4. Observer 的懸空指標:觀察者被銷毀卻沒退訂 → 存取已釋放記憶體。用 RAII 自動退訂或 weak_ptr
  5. 忘記虛擬解構子:多型基底類別缺 virtual ~Base(),透過基底指標 delete 衍生物件是 UB。
  6. 在建構子/解構子中呼叫虛擬函式:不會分派到衍生類別版本(此時物件尚未/已不再是衍生型別)。
  7. 為了重用而繼承:違反里氏替換。優先組合。
  8. 違反 Rule of Five:自定義了解構子卻忘了處理複製/移動,導致淺複製、重複釋放。
  9. 遺漏 const:getter 沒標 const,導致 const 物件無法使用;或介面意圖不清。
  10. 回傳區域物件的參考/指標:懸空參考,UB。回傳值(靠移動/RVO 不會慢)。
  11. 用裸 new/delete 管理所有權:例外路徑洩漏。用智慧指標。
  12. std::function 當萬靈丹:策略複雜、需要多個方法或繼承體系時,類別版更合適。

編譯注意

# 一般編譯(本章 singleton.cpp 也用到 mutex,建議一律加 -pthread)
g++ -std=c++17 -Wall -Wextra -pthread filename.cpp -o output

# 開發時建議開啟 sanitizers 抓記憶體與未定義行為
g++ -std=c++17 -Wall -Wextra -pthread -fsanitize=address,undefined -g filename.cpp -o output

練習題

提示:以 g++ -std=c++17 -Wall -Wextra -pthread 編譯;除錯時可加 -fsanitize=address,undefined

練習 1:用 Meyers' Singleton 實作連線池(基礎)

實作一個執行緒安全的 ConnectionPool 單例,內部維護固定數量(如 3 條)的模擬連線。提供 acquire() 借出一條連線、release() 歸還。用 instance() 取得唯一實例,並禁止複製與移動。

  • 提示:用 Meyers' Singleton(static ConnectionPool inst;);用 std::mutex 保護可用連線清單;把連線編號存進 std::vectorstd::queueacquire 取出、release 放回。

練習 2:股票價格 Observer(基礎)

用 Observer 模式實作股價監控:一個 Stock(Subject)持有價格,多個顯示面板(如「目前價格面板」「漲跌幅面板」)訂閱更新。當 set_price() 被呼叫時,所有面板自動收到通知並印出。

  • 提示:可用傳統繼承版(Observer 介面 + update(price)),或現代 std::function 版(on("price", cb))。記得提供 unsubscribe/off 並示範退訂後不再收到通知。

練習 3:文字轉換 Strategy(基礎)

設計一個 TextProcessor,可在執行期切換不同的文字處理策略:全部大寫、全部小寫、反轉字串。用 std::function<std::string(const std::string&)> 存放策略,並提供 set_strategy()process()

  • 提示:策略用 lambda 表示;大小寫可用 std::transform 搭配 ::toupper/::tolower;反轉用 std::reverse。比較「換策略只需換一個 lambda」相對於 if/switch 的優勢。

練習 4:用 Factory 建立圖形(中級)

定義抽象基底 Shape(含純虛擬 area()name()),實作 CircleRectangleTriangle。寫一個 Factory 函式 create_shape(type, params...) 回傳 std::unique_ptr<Shape>。讀入一串圖形描述,建立它們並印出各自面積與總面積。

  • 提示:基底類別記得 virtual ~Shape() = default;;用 std::vector<std::unique_ptr<Shape>> 存多型物件;總面積用 std::accumulate 搭配 lambda 累加。

練習 5:用 RAII 管理資源(中級)

實作一個 ScopedTimer 類別:建構時記錄起始時間,解構時自動印出「此區塊耗時 X 毫秒」。用它來量測一段運算的耗時,體會 RAII「離開作用域自動執行收尾」的威力。再實作一個 FileHandle RAII 包裝(建構開檔、解構關檔),並示範即使中途 throw 也能正確關檔。

  • 提示ScopedTimerstd::chrono::steady_clock::now();遵守 Rule of Five(通常 delete 複製、視需要處理移動);在 try 區塊內丟例外,觀察解構子仍被呼叫。

練習 6:套用 SOLID 重構(挑戰)

給你一個「上帝類別」OrderManager,它同時負責計算金額、套用折扣、寫日誌、寄通知。請依 SOLID 重構:拆出 IDiscountStrategy(Strategy + 開放封閉)、ILoggerINotifier(依賴反轉 + 介面隔離),透過建構子注入。寫出至少兩種折扣策略,並示範替換策略不需改動 OrderManager

  • 提示OrderManager 只依賴抽象介面,具體實作從外部傳入(DI);折扣策略可用 std::function<double(double)> 或繼承介面;測試時可注入一個「記錄到字串」的假 logger,體會可測試性。

練習 7:綜合——可插拔的日誌系統(挑戰)

結合三種模式實作一個日誌系統:全域唯一的 LoggerSingleton)、可註冊多個輸出目的地如主控台/檔案/記憶體(Observer)、可切換格式化方式如純文字/JSON/帶時間戳(Strategy)。呼叫 Logger::instance().log(level, msg) 時,先用當前格式化策略產生字串,再廣播給所有已註冊的輸出目的地。

  • 提示:Singleton 用 Meyers' 寫法並用 std::mutex 保護;輸出目的地用 std::vector<std::function<void(const std::string&)>>;格式化策略用 std::function<std::string(Level, const std::string&)>;示範執行期切換格式並新增/移除輸出目的地。

對應程式碼