為何需要單一性?#

通常類別與實例是一對多的關係,但有些類別應該只有一個實例——例如應用程式的根物件、工廠、管理器等。如果這些物件存在多個,可能導致邏輯錯誤、控制衝突或併發問題。

補充: 作者稱最簡單的做法為 JUST CREATE ONE 模式——初始化應用程式時只建立一個,不需要任何強制機制。在沒有立即且重要的需求時,這通常是最好的做法。但如果強制單一性的機制成本很低,其溝通意圖的好處可能超過機制的成本。

本章探討兩種強制單一性的模式:SingletonMonostate,它們有截然不同的成本效益取捨。

Singleton 模式#

結構#

Singleton 透過私有建構函式靜態變數來強制只能存在一個實例:

public class Singleton
{
  private static Singleton theInstance = null;
  private Singleton() {}

  public static Singleton Instance
  {
    get
    {
      if (theInstance == null)
        theInstance = new Singleton();
      return theInstance;
    }
  }
}
  • 建構函式是 private,外部無法直接建立實例
  • 只能透過 Singleton.Instance 靜態屬性取得唯一實例
  • 多次呼叫 Instance 回傳的是同一個物件參考

優點#

  • 跨平台(Cross-platform):透過適當的中介軟體(如 Remoting),可以擴展為跨多個 CLR 和多台電腦
  • 適用於任何類別:任何類別都可以透過私有化建構函式加上靜態屬性變成 Singleton
  • 可透過衍生建立:可以建立 Singleton 類別的子類別
  • 延遲求值(Lazy Evaluation):如果 Singleton 從未被使用,就不會被建立

缺點#

  • 銷毀未定義:沒有好的方式銷毀 Singleton。如果添加 decommission 方法將 theInstance 設為 null,其他模組可能仍持有參考,後續呼叫 Instance 會建立新實例,導致兩個實例同時存在
  • 不可繼承:從 Singleton 衍生的類別不是 Singleton,必須自行添加靜態變數和方法
  • 效率:每次呼叫 Instance 都要執行 if 判斷
  • 不透明:使用者知道自己在使用 Singleton,因為必須呼叫 Instance 方法

實際應用:UserDatabase#

以 Web 系統的使用者資料庫為例——用 Facade 模式建立 UserDatabase 類別封裝第三方 API,再用 Singleton 確保所有資料庫存取都經過單一實例,方便加入檢查、計數器和鎖定:

public class UserDatabaseSource : UserDatabase
{
  private static UserDatabase theInstance =
    new UserDatabaseSource();

  public static UserDatabase Instance
  {
    get { return theInstance; }
  }

  private UserDatabaseSource() {}
  // ReadUser, WriteUser 實作...
}

Monostate 模式#

結構#

Monostate 用完全不同的機制達到單一性——所有變數都是 static,但方法都不是 static

public class Monostate
{
  private static int itsX;

  public int X
  {
    get { return itsX; }
    set { itsX = value; }
  }
}
  • 無論建立多少個 Monostate 實例,它們都表現得像同一個物件
  • 在一個實例上設定 X 的值,從另一個實例讀取 X 會得到相同的值
  • 可以銷毀所有實例而不遺失資料

重點: Singleton 強制的是結構的單一性——阻止建立多於一個實例。Monostate 強制的是行為的單一性——所有實例表現相同,但不限制實例的數量。值得注意的是,Monostate 的測試案例對 Singleton 類別也有效,但反過來不行。

優點#

  • 透明性(Transparency):使用者不需要知道物件是 Monostate,使用方式與普通物件完全相同
  • 可衍生性(Derivability):Monostate 的衍生類別也是 Monostate,它們共享同一組靜態變數
  • 多型性(Polymorphism):因為方法不是靜態的,可以在衍生類別中覆寫,提供同一組靜態變數上的不同行為
  • 明確的建立與銷毀:靜態變數有明確的建立和銷毀時間

缺點#

  • 不可轉換(No Conversion):非 Monostate 的類別無法透過衍生變成 Monostate
  • 效率:因為是真實物件,可能經歷多次建立和銷毀,這些操作成本較高
  • 持續佔用(Presence):即使 Monostate 從未使用,靜態變數仍佔用空間
  • 平台限制(Platform Local):無法讓 Monostate 跨多個 CLR 實例或多個平台運作

實際應用:Turnstile FSM#

以地鐵旋轉門的有限狀態機(FSM)為例——Turnstile 設計為 Monostate,因為旋轉門永遠只有一個:

Figure 24.1: Subway turnstile finite state machine

Turnstile 基底類別將 Coin()Pass() 事件委派到兩個衍生類別 LockedUnlocked,它們代表 FSM 的狀態:

public class Turnstile
{
  private static bool isLocked = true;
  private static bool isAlarming = false;
  private static int itsCoins = 0;
  private static int itsRefunds = 0;
  protected static readonly Turnstile LOCKED = new Locked();
  protected static readonly Turnstile UNLOCKED = new Unlocked();
  protected static Turnstile itsState = LOCKED;

  public virtual void Coin() { itsState.Coin(); }
  public virtual void Pass() { itsState.Pass(); }
  // ...
}

技巧: 測試方法假設 Turnstile 是 Monostate,可以從不同實例發送事件並收集查詢結果。這很合理,因為旋轉門永遠只有一個。Monostate 讓衍生類別(LockedUnlocked)自然地共享同一組狀態,很好地展示了 Monostate 的多型優勢。

Singleton vs. Monostate 比較#

面向SingletonMonostate
強制方式結構(限制實例數量)行為(所有實例共享狀態)
建立限制只能一個實例可建立多個實例
使用者認知必須知道(呼叫 Instance)不需知道(透明使用)
衍生衍生類別不是 Singleton衍生類別也是 Monostate
多型不支援支援(方法非靜態)
銷毀困難且不安全可安全銷毀所有實例
跨平台可以(透過中介軟體)不行(平台受限)
延遲建立支援不支援(靜態變數立即建立)