[107] C++ 類別設計手法系列一

[107] C++ 類別設計手法系列一

設計類別時需要考量呼叫端的「使用體驗」,讓呼叫端用起來「輕鬆無負擔」乃最高指導原則,「以客為尊」亦適用於類別設計者。這個系列討論 C++ 語言「常見的類別設計手法」,也許會跟 Design Pattern(設計範式)沾一點邊。

這篇先舉兩個常見的類別設計手法:

  1. 滿足條件才能生成物件
  2. 物件先建,材料事後補

滿足條件才能生成物件

以泡沫紅茶機(TeaMaker)為例,底下類別僅提供一建構式,需要三種「材料」—Tea, Sugar and Ice。三種材料缺一不可,湊齊了才能建出 TeaMaker 物件:

class TeaMaker
{
  TeaMaker(Tea, Sugar, Ice);
  TheTea Make();
};

這種設計的好處有:

  1. 類別明確定出**「前置條件」**,要用可以,先把東西準備好。
  2. TeaMaker 類別的「使用契約」以程式碼清楚呈現,錯用的機率低。(只有一個 Make 成員函式,建出物件後呼叫它就對了)

缺點是不提供預設建構式(Default Constructor),更明確地說是沒有不含任何參數的建構式,限縮了使用時機,而且物件再利用的機會少了很多。

我喜歡這種設計手法。

物件先建,材料事後補

另一種泡沫紅茶機(TeaMaker)類別的設計手法是提供沒有參數的建構式(Default Constructor),搭配成員函式來讓呼叫端加入材料,最後再呼叫 Make 完成操作:

class TeaMaker
{
  TeaBuilder();
  void AddTea(Tea);
  void AddSugar(Sugar);
  void AddIce(Ice);
  TheTea Make();
};

這種設計的好處有:

  1. 較有彈性。呼叫端可先建出「無作用」的物件,之後再呼叫相應的成員函式來加材料。
  2. 容易搭配設計範式(Design Pattern)中的 Builder Pattern

缺點是容易遇到奧客

  1. 使用契約不明確。呼叫端很難一眼看出使用該類別的前置條件。通常要搭配文件或註解才知道正確用法。
  2. 承上。不讀「使用手冊」的呼叫端多不勝數,導致「錯用」的機會高,例如沒呼叫 AddTea 就呼叫 Make(沒茶葉是要怎麼泡?)。
  3. 容易產生疑慮。是要先加茶(AddTea)還是先加冰(AddIce);做無糖的還需要呼叫 AddSugar 嗎?
  4. 由於沒辦法一開始就取得所有材料,類別實作勢必要把已經取得的材料「暫存」在類別內部,增加實作複雜度。

「物件先建,材料事後補」的設計手法又衍生出幾種常用的技巧:

  • Fluent Interface
  • Method Chaining

Fluent Interface

加材料的成員函式回傳物件本身的參考(Reference, *this),讓呼叫端可以將成員函式「串」起來,一氣呵成:

class TeaMaker
{
  TeaMaker();
  TeaMaker& AddTea(Tea);
  TeaMaker& AddSugar(Sugar);
  TeaMaker& AddIce(Ice);
  TheTea Make();
};

這樣的設計手法叫「Fluent Interface」。JUCE 的 Rectangle 類別大量使用了此設計技巧。使用的方式如下:

TeaMaker tm;
tm.AddTea("Black Tea").AddSugar(0.3).AddIce(0.5).Make();

非 Fluent Interface 設計的使用方式當作對照組:

TeaMaker tm;
tm.AddTea("Black Tea");
tm.AddSugar(0.3);
tm.AddIce(0.5);
tm.Make();

Fluent Interface 的設計讓呼叫端的程式碼更簡潔,用起來更順暢。不過,有一點要特別留意。由於成員函式的回傳值皆為物件本身,那類別設計者如何讓呼叫端得知成員函式的執行結果?最直覺的用法是透過 Exception(例外處理),不過,這樣一來似乎又更複雜了...


查找資料的過程中發現另外兩個名詞:Method Chaining and Method Cascading,然後這兩個有點一樣又不太一樣


Method Chaining

與「Fluent Interface」類似的設計,但成員函式傳回物件的副本而不是本尊,每呼叫一個成員函式即產生一新物件,新物件擁有原物件的所有屬性加上原物件沒有的新材料。而之後呼叫的是新物件的成員函式,然後產生新物件:

class TeaMaker
{
  TeaMaker();
  TeaMaker AddTea(Tea) const; 
  TeaMaker AddSugar(Sugar) const;
  TeaMaker AddIce(Ice) const;
  TheTea Make();
};

特別留意每個成員函式尾巴都多了 const 關鍵字,表示呼叫該函式不會改變物件本身的狀態。這樣的設計適合促成 Const Correctness

「物件複製」的成本可高可低,類別設計者必須針對類別的屬性與用途,挑選合適的設計手法,讓呼叫端用得安心。🔚