學習目標
讀完本章並完成練習後,你應該能夠:
- 解釋 SOLID 五大原則,並用 C++ 範例說明每一條如何改善設計
- 區分創建型、結構型、行為型三大類設計模式
- 實作執行緒安全的 Singleton(Meyers' Singleton),並說出它的批評與替代方案(依賴注入)
- 實作 Observer 模式(push/pull、傳統繼承版與
std::function現代版) - 實作 Strategy 模式(類別繼承版與
std::function版),並判斷何時用哪種 - 說明 Factory Method 與 Abstract Factory 的差異與用途
- 理解 RAII 是「C++ 最重要的慣用法」,並能用它管理任意資源
- 認識 PIMPL 慣用法、組合優於繼承、Rule of Zero/Five、const 正確性、值語意
- 偏好標準演算法、智慧指標而非裸迴圈與裸指標
- 對照 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 雖然方便,但是最常被濫用、也最受批評的模式之一:
- 本質上是全域狀態——增加耦合,任何地方都能偷偷依賴它
- 隱藏依賴關係——函式簽章看不出它用了哪些 Singleton,難以理解
- 難以單元測試——無法替換成 mock,測試之間還會互相污染狀態
- 初始化順序問題——多個跨檔案全域物件的解構順序不確定
替代方案:依賴注入(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*,若觀察者先被銷毀卻沒 unsubscribe,notify 時就會存取已釋放記憶體(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_guard、fstream、std::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/delete。R.20/R.21:用unique_ptr或shared_ptr表達所有權,優先unique_ptr。
值語意(value semantics)
C++ 鼓勵把物件當「值」傳遞與複製(像 int 一樣),而非到處傳指標。配合移動語意,值語意既安全(無別名、無懸空指標)又高效。std::string、std::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 virtual 或 protected non-virtual |
| Enum.3 | 偏好 enum class |
取代未限定範圍的 enum |
| ES.50 | 不要 const_cast 去除 const |
違反 const 契約是 UB 來源 |
| 雜項 | override / nullptr / [[nodiscard]] |
明確標記覆寫、用 nullptr、標註不可忽略的回傳值 |
十、撰寫可維護的現代 C++(綜合建議)
- 先求正確與簡單,再談模式:YAGNI(You Aren't Gonna Need It),不要為了想像中的需求預先抽象。
- 讓不合法的狀態無法表示:用型別系統表達約束(強型別、
enum class、不變式建構子)。 - 預設
const、預設private:需要時才放寬,縮小可變範圍與可見範圍。 - 小函式、小類別、單一職責:好命名勝過好註解。
- 資源一律 RAII:絕不手動
new/delete、lock/unlock、open/close。 - 偏好值語意與不可變資料:減少別名與共享狀態,天然更安全。
- 介面用抽象、實作靠注入:依賴反轉讓程式可測試、可替換。
- 善用標準函式庫:容器、演算法、
<chrono>、<random>、智慧指標都比自己造輪子可靠。 - 打開警告並當錯誤:
-Wall -Wextra -Werror,搭配 sanitizers(-fsanitize=address,undefined)。 - 寫測試:可測試性本身就會逼你做出鬆耦合的好設計。
重點整理
| 模式 / 慣用法 | 分類 | 用途 | 關鍵特徵 |
|---|---|---|---|
| Singleton | 創建型 | 全域唯一實例 | 私有建構子、Meyers' static instance |
| Factory Method | 創建型 | 封裝單一物件建立 | 回傳基底類別智慧指標 |
| Abstract Factory | 創建型 | 建立整族相關物件 | 工廠介面 + 多套具體工廠 |
| Observer | 行為型 | 一對多事件通知 | subscribe / notify / unsubscribe |
| Strategy | 行為型 | 可互換演算法 | 策略介面或 std::function + Context |
| RAII | 慣用法 | 資源管理 | 建構取得、解構釋放,例外安全 |
| PIMPL | 結構型慣用法 | 隱藏實作、降低編譯依賴 | 前向宣告 Impl + unique_ptr |
常見錯誤與陷阱
- 過度使用設計模式:為了用模式而用模式,反而增加複雜度。只在真正需要時引入。
- Singleton 濫用:把它當全域變數的藉口。優先考慮依賴注入。
- C++11 前的雙重檢查鎖定(DCLP):容易出錯,直接用 Meyers' Singleton。
- Observer 的懸空指標:觀察者被銷毀卻沒退訂 → 存取已釋放記憶體。用 RAII 自動退訂或
weak_ptr。 - 忘記虛擬解構子:多型基底類別缺
virtual ~Base(),透過基底指標delete衍生物件是 UB。 - 在建構子/解構子中呼叫虛擬函式:不會分派到衍生類別版本(此時物件尚未/已不再是衍生型別)。
- 為了重用而繼承:違反里氏替換。優先組合。
- 違反 Rule of Five:自定義了解構子卻忘了處理複製/移動,導致淺複製、重複釋放。
- 遺漏 const:getter 沒標 const,導致 const 物件無法使用;或介面意圖不清。
- 回傳區域物件的參考/指標:懸空參考,UB。回傳值(靠移動/RVO 不會慢)。
- 用裸
new/delete管理所有權:例外路徑洩漏。用智慧指標。 - 把
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::vector或std::queue,acquire取出、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()),實作 Circle、Rectangle、Triangle。寫一個 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 也能正確關檔。
- 提示:
ScopedTimer用std::chrono::steady_clock::now();遵守 Rule of Five(通常 delete 複製、視需要處理移動);在 try 區塊內丟例外,觀察解構子仍被呼叫。
練習 6:套用 SOLID 重構(挑戰)
給你一個「上帝類別」OrderManager,它同時負責計算金額、套用折扣、寫日誌、寄通知。請依 SOLID 重構:拆出 IDiscountStrategy(Strategy + 開放封閉)、ILogger、INotifier(依賴反轉 + 介面隔離),透過建構子注入。寫出至少兩種折扣策略,並示範替換策略不需改動 OrderManager。
- 提示:
OrderManager只依賴抽象介面,具體實作從外部傳入(DI);折扣策略可用std::function<double(double)>或繼承介面;測試時可注入一個「記錄到字串」的假 logger,體會可測試性。
練習 7:綜合——可插拔的日誌系統(挑戰)
結合三種模式實作一個日誌系統:全域唯一的 Logger(Singleton)、可註冊多個輸出目的地如主控台/檔案/記憶體(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&)>;示範執行期切換格式並新增/移除輸出目的地。