在設計一套系統時,有時會遇到某些資源需要嚴格控管使用,這時候就要限制只有一個實體可存取此資源,例如我想限制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



留言
張貼留言