在設計一套系統時,有時會遇到某些資源需要嚴格控管使用,這時候就要限制只有一個實體可存取此資源,例如我想限制DB的Connection數量,可以寫一個Connection Pool來控制DB Connection的數量,當沒有空閒的DB Connection可以用時就要等待。像上面的那種情況,Connection Pool的實體就不能多個,不然會變成多個實體在控制。這時候Connection Pool就能套用Singleton Pattern。
Singleton必須實作一個Instance()方法,在內部判斷private static欄位instance是否已經有產生,如果沒有就產生,接著回傳自身Instance。
限量寫了一個範例來解釋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。以下為範例程式碼:
在第一次drink呼叫DrinkFactory.Instance時,會先到155行,因為限量在這裡使用Lazy型態,所以在此時才會到150行去執行new DrinkFactroy();,產生就會回到155行。然而到drink2呼叫DrinkFactroy.Instance時,一樣到155行,但因為_Instance已經有產生了,所以就直接回傳跳出。
參考來源:
Design Patterns - Gang of Four
C#: System.Lazy<T> and the Singleton Design Pattern
目的(Intent)
確保此Singleton物件在應用程式的生命週期中只有一個Instance,此Singleton Class須提供一個介面讓外部取得此Singleton Instance。動機(Motivation)
如前面所說,當需要嚴格控管資源時,或該Class在應用程式中只需要一個Instance時可使用。為了確保自身只有一個Instance,Singleton Class必須自己負責檢查確保沒有多個Instance產生,如果沒有就自己產生一個,故Singleton Class要負責以下三個工作:- 檢查自己手中是否有Instance
- 產生自身Instance
- 提供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已經有產生了,所以就直接回傳跳出。
優點
- 可確保控制的Instance是唯一的。
- 因為可繼承與覆寫,所以有OO的特性且可便利的擴充。
- 相較於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)
- Abstract Factory
- Builder
- Prototype
使用頻率
參考來源:
Design Patterns - Gang of Four
C#: System.Lazy<T> and the Singleton Design Pattern
留言
張貼留言