Design Pattern (G4) - Singleton Pattern

在設計一套系統時,有時會遇到某些資源需要嚴格控管使用,這時候就要限制只有一個實體可存取此資源,例如我想限制DB的Connection數量,可以寫一個Connection Pool來控制DB Connection的數量,當沒有空閒的DB Connection可以用時就要等待。像上面的那種情況,Connection Pool的實體就不能多個,不然會變成多個實體在控制。這時候Connection Pool就能套用Singleton Pattern。


目的(Intent)

確保此Singleton物件在應用程式的生命週期中只有一個Instance,此Singleton Class須提供一個介面讓外部取得此Singleton Instance。

動機(Motivation)

如前面所說,當需要嚴格控管資源時,或該Class在應用程式中只需要一個Instance時可使用。為了確保自身只有一個Instance,Singleton Class必須自己負責檢查確保沒有多個Instance產生,如果沒有就自己產生一個,故Singleton Class要負責以下三個工作:

  1. 檢查自己手中是否有Instance
  2. 產生自身Instance
  3. 提供Public方法讓外部取得該Instance

使用時機

當你想要你的Class在整個應用程式生命週期中只有一個Instance。

結構(Structure)


Singleton必須實作一個Instance()方法,在內部判斷private static欄位instance是否已經有產生,如果沒有就產生,接著回傳自身Instance。

實作(Implementation)


限量寫了一個範例來解釋Singleton,上圖是一個Singleton Factory的UML Class Diagram。此範例主要目的為針對Product類型的Instance要交由Factory來產生,而各種Factory在整個生命週期中只能有一個,因為考慮到往後可以控制Product的產生數量,所以將Factory做成Singleton。再來因為要增加彈性,所以在Product與Factory皆各抽兩層,分別為抽象類別與介面,BaseFactroy實作IFactory<T>泛型介面,只能由IProduct類型的Factory類別來繼承。DrinkFactory與FoodFactory繼承BaseFactory,其中實作CreateProduct回傳Drink與Food的Instance,因為DrinkFactory與FoodFactory為Singleton,所以外部要使用DrinkFactory與FoodFactory時要用其提供的Instance()方法,例如:var drink = DrinkFactroy.Instance().CreateProduct();可以利用DrinkFactory產生一個Drink Instance以下為範例程式碼:


using System;

namespace ConsoleTest
{

 public class Program
 {
  static void Main( string[] args )
  {
   var drink = DrinkFactory.Instance.CreateProduct( x =>
   {
    x.Price = 100;
    x.Cost = 50;
    x.Amount = 100;
    x.Name = "MyBeer";
    x.CreateDate = DateTime.Now;
    x.EndDate = x.CreateDate.AddYears( 1 );
   } );
   var drink2 = DrinkFactory.Instance.CreateProduct();
   var food = FoodFactory.Instance.CreateProduct();
  }
 }

 public interface IProduct
 {
  // 判斷是否可賣
  bool IsSellable { get; }
 }

 public abstract class Product : IProduct
 {
  // 售價
  public double Price { get; set; }
  // 成本價
  public double Cost { get; set; }
  // 製造日期
  public DateTime CreateDate { get; set; }
  // 有效日期
  public DateTime EndDate { get; set; }
  // 數量
  public int Amount { get; set; }
  // 商品名稱
  public string Name { get; set; }
  // 單位
  public abstract string Unit { get; }

  // 有效期限(剩多久過期)
  public virtual Tuple<int, int, int> DateDuration
  {
   get
   {
    return TimespanToDate( CreateDate, EndDate );
   }
  }

  // 計算基本損益
  public virtual double Profit
  {
   get
   {
    return ( Price - Cost ) * Amount;
   }
  }

  // 計算兩個日期的時間差
  private Tuple<int, int, int> TimespanToDate( DateTime fromDate, DateTime toDate )
  {
   int years, months, days;
   if ( fromDate < toDate )
   {
    DateTime tmp = toDate;
    toDate = fromDate;
    fromDate = tmp;
   }

   months = 12 * ( fromDate.Year - toDate.Year ) + ( fromDate.Month - toDate.Month );

   if ( fromDate.Day < toDate.Day )
   {
    months--;
    days = DateTime.DaysInMonth( toDate.Year, toDate.Month ) - toDate.Day + fromDate.Day;
   }
   else
   {
    days = fromDate.Day - toDate.Day;
   }

   years = months / 12;
   months = months % 12;

   return Tuple.Create( years, months, days );
  }

  // 基本判斷是否可賣
  public virtual bool IsSellable
  {
   get
   {
    return DateTime.Today < EndDate;
   }
  }
 }

 // Drink
 public class Drink : Product
 {
  public override string Unit
  {
   get { return "L"; }
  }
 }

 // Food
 public class Food : Product
 {
  public override string Unit
  {
   get { return "KG"; }
  }
 }

 // Factory Interface
 public interface IFactory<T> where T : IProduct
 {
  T CreateProduct();
 }

 // Base Factroy
 public abstract class BaseFactroy<TProduct> : IFactory<TProduct> where TProduct : IProduct, new()
 {
  // 產生空的Instance
  public abstract TProduct CreateProduct();
  // 產生Instance執行Constructor
  public TProduct CreateProduct( Action<TProduct> constructor )
  {
   var instance = new TProduct();
   constructor( instance );
   return instance;
  }
 }

 // Drink Factory
 public class DrinkFactory : BaseFactroy<Drink>
 {
  private static readonly Lazy<DrinkFactory> _instance
   = new Lazy<DrinkFactory>(
    () =>
     new DrinkFactory() );
  public static DrinkFactory Instance
  {
   get
   {
    return _instance.Value;
   }
  }

  public override Drink CreateProduct()
  {
   return new Drink();
  }
 }

 // Food Factory
 public class FoodFactory : BaseFactroy<Food>
 {
  private static readonly Lazy<FoodFactory> _instance
   = new Lazy<FoodFactory>(
    () =>
     new FoodFactory() );
  public static FoodFactory Instance
  {
   get
   {
    return _instance.Value;
   }
  }

  public override Food CreateProduct()
  {
   return new Food();
  }
 }
}



在第一次drink呼叫DrinkFactory.Instance時,會先到155行,因為限量在這裡使用Lazy型態,所以在此時才會到150行去執行new DrinkFactroy();,產生就會回到155行。然而到drink2呼叫DrinkFactroy.Instance時,一樣到155行,但因為_Instance已經有產生了,所以就直接回傳跳出。

優點

  1. 可確保控制的Instance是唯一的。
  2. 因為可繼承與覆寫,所以有OO的特性且可便利的擴充。
  3. 相較於Static Class,Singleton較有彈性(因為可繼承,覆寫...)。

Singleton v.s. Static Class

Singleton Class與Static Class常常被拿來比較,Static Class不需要產生Instance就可以直接操作,這和Singleton使用自身Instance操作的概念很像。但大家比較疑惑的是何時要使用Static Class何時要使用Singleton Class,其實從Singleton的優點中就可以知道使用的時機。當你要設計的一個Class將來可能會有擴充或者是需要實作介面就可以使用Singleton;而Static Class不具有OO概念所以無法繼承與實作任何介面。因為Singleton為一個物件實體,所以Singleton物件可以拿來當作參數來傳遞或者是進行複製。下列表格簡單列出幾點Singleton Class與Static Class的差異:



Static Class
Singleton Class
是否有Instance
N
Y
OO特性
N
Y
記憶體存放位置
Heap
Stack
是否可傳遞
N
Y
記憶體回收
N
Y

相關樣式(Related Pattern)

  1. Abstract Factory
  2. Builder
  3. Prototype

使用頻率










參考來源:
Design Patterns - Gang of Four
C#: System.Lazy<T> and the Singleton Design Pattern




留言