--- tags: 設計模式 --- # 獨體模式(Singleton Pattern) ## 前言 - 你希望某個類別最多僅能存在一個物件實體 --- 通常是此類別控制了某個共享資源(E.g. File, 連線, DB 等等), 此時可以考慮使用 Singleton Pattern. - 你希望對於全域變數的存取是經過限制或是控制的, 此時可以考慮使用 Singleton Pattern. #### 問題需求 你的程式需要存取 DB, 你寫了一個類別專門用來快取 DB 的資料 (e.g. Entity Framework 的 DbContext. 從 DB 預先讀資料到 Memory 中, 使用者操作此物件的資料, 等同於操作 DB 的資料). 此時 1. 你希望這個類別只能有一個物件實體 ---> 因為多個實體代表會讀多次 DB 的資料到 Memory. 增加 Memory 使用量. 而且多個物件實體, 也讓你也不好管理 DB 資料的狀態. (E.g. 哪個物件的資料應該寫入 DB) 2. 你不希望任何人都可以存取這個物件實體 ---> 因為若是任何人都可以指派值給此全域變數, 你正在使用的物件實體可能會被其他物件覆蓋, 或是清空為 null. ## Singleton 介紹 ### 定義 > Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance. > Ensure a class only has one instance, and provide a global point of access to it. > * Applicability > * Need exactly one instance of a class and a well-known access point > * Need to have the sole instance extensible by subclassing > * Consequences > * Need controlled access > * Permits refinement of operations and representation > * More flexible than static class operators - 確保某一類別, 其只能有一個物件實體存在, 並且其會提供唯一的存取點, 讓使用者取得物件實體. - 類別會自己保存自己這個物件實體. - Singleton 在乎物件的生命週期. - 通常此唯一的物件被 new 起來後 , 就不會 disposse 掉. - 不希望任何人都可以隨意 set 這個物件. ### UML ![structure-en-Singleton.png](https://github.com/s0920832252/C_Sharp/blob/master/Files/DesignPattern/structure-en-Singleton.png?raw=true) ### 名詞定義 #### Singleton 定義 - 以 sealed class 的形式存在, 且僅能有一個存取層級為 private 的建構子. - private 建構子令外界無權限可以使用 new 的方式建立 Singleton 物件. - 若無 sealed 關鍵字, Singleton 的巢狀類別能透過繼承 Singleton 的方式, 存取 Singleton 的私有建構子 ---> 違反只能有一個 Singleton 物件實體存在的定義. - ```C# public class Singleton { public class SubSingleton : Singleton { public SubSingleton() { // 可以有不只一個 Singleton 物件實體. 違反定義 var singleton = new Singleton(); var singleton2 = new Singleton(); var singleton3 = new Singleton(); } } // 外界無法使用 new 的方式建立 Singleton 物件. private Singleton() { } } ``` - 提供單一存取點供外界取得物件實體 (在 C# 中, 通常以 Property 實作), 讓外界只能透過此存取點存取此唯一的物件實體. ```C# // No Thread Safe Example public sealed class Singleton { private static Singleton _singletonObj = null; // 外界只能使用 Property SingletonObj(存取點) 去得到唯一的 Singleton 物件. public static Singleton SingletonObj => _singletonObj ??= new Singleton(); private Singleton() {} } ``` #### Client 定義 - 欲使用 Singleton 物件實體者. - Client 端會透過 Singleton 類別提供的存取點來取得唯一的 Singleton 物件. ## Singleton 基本實作 Example ### Double-checked Locking For Thread Safe #### Singleton ```C# public sealed class Singleton { private Singleton(){} // 私有建構子, 防止外部使用 new , 產生物件 private static readonly object _lockObj = new object(); private static Singleton _singleton; public static Singleton SingletonObj { get { if (_singleton == null) { lock (_lockObj) { _singleton ??= new Singleton(); } } return _singleton; } } } ``` #### Client ```C# internal static class Program { private static void Main(string[] args) { for (var i = 0; i < 20; i++) { Task.Run(() => { Console.WriteLine(Singleton.SingletonObj.GetHashCode()); }); } Console.ReadLine(); } } ``` #### 輸出 ``` 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 ``` ### Static Initialization For Thread Safe ( No Lazy ) #### Singleton ```C# public sealed class Singleton { private Singleton(){} // 私有建構子, 防止外部使用 new , 產生物件 public static Singleton SingletonObj { get; } = new Singleton(); } ``` #### Client ```C# internal static class Program { private static void Main(string[] args) { for (var i = 0; i < 20; i++) { Task.Run(() => { Console.WriteLine(Singleton.SingletonObj.GetHashCode()); }); } Console.ReadLine(); } } ``` #### 輸出 ``` 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 ``` - C# static field initialization 的時機是 **當類別內任何成員被使用時** , 意即即使 Singleton 沒有被使用到, Singleton 也會被初始化. ```C# // Singleton public sealed class EagerSingleton { private EagerSingleton() => Console.WriteLine($"{nameof(EagerSingleton)} created"); public static int Count => 100; // Property 是 C# 的語法糖, 此處實際上會替你建造一個 field public static EagerSingleton EagerSingletonObj { get; } = new EagerSingleton(); } // Client internal static class Program { private static void Main(string[] args) { Console.WriteLine(EagerSingleton.Count); Console.ReadLine(); } } /* 輸出結果 EagerSingleton created 100 */ ``` - [Properties Init](https://docs.microsoft.com/en-us/dotnet/csharp/properties#property-syntax) - > **The compiler generates the storage location for the field that backs up the property.** ### Lazy Instance For Thread Safe #### Singleton ```C# public sealed class Singleton { private Singleton() => Console.WriteLine("Singleton created"); public static int Count => 100; public static Singleton SingletonObj => InnerSingleton.InnerObj; private class InnerSingleton { internal static Singleton InnerObj { get; } = new Singleton(); } } ``` #### Client ```C# internal static class Program { private static void Main(string[] args) { Console.WriteLine(Singleton.Count); // 呼叫 Count 不會令 Singleton 被初始化 for (var i = 0; i < 20; i++) { Task.Run(() => { Console.WriteLine(Singleton.SingletonObj.GetHashCode()); }); } Console.ReadLine(); } } ``` #### 輸出 ``` 100 Singleton created 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 ``` ### .NET 4 的 Lazy<T> For Thread Safe #### Singleton ```C# public sealed class Singleton { private Singleton(){} private static readonly Lazy<Singleton> _lazy = new Lazy<Singleton>(() => new Singleton()); public static Singleton SingletonObj => _lazy.Value; } ``` #### Client ```C# internal static class Program { private static void Main(string[] args) { for (var i = 0; i < 20; i++) { Task.Run(() => { Console.WriteLine(Singleton.SingletonObj.GetHashCode()); }); } Console.ReadLine(); } } ``` #### 輸出 ``` 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 63835064 ``` ## 使用 IOC 容器實作 Singleton - 上述基本實作 Singleton 的方式都會碰到以下問題 - 使用 Singleton 物件的邏輯四散 - 其實作的本質**仍然是全域變數**. 你可以在程式中的任何一個地方取得 Singleton 物件, 並且進行操作(壞味道) - 單元測試撰寫不易 - 因為 Singleton 的實作需要使用到 static , 而大多數 mock 框架通常都需要使用 inheritance & override 的特性. 因此你幾乎沒辦法 mock Singleton object. - 違反單一職責原則(Single Responsibility Principle) - 原本在建立類別時, 我們本來就會賦予其某項職責. 但 Singleton 需要多負責物件的建立以及管理生命週期. 物件的建立應該使用工廠模式, 封裝建立物件的邏輯, 另外 Singleton Pattern 的另外一個問題是不容易進行擴充. - 違反開放封閉原則 Open-Closed Principle (OCP) - SingletonObj 並非抽象型別, 因此若需求變更或有新需求時, 我們沒辦法將 SingletonObj 替換為新的類別. 只能修改原有實作類別的程式碼. - IOC 容器框架, 通常其會支援使用 Singleton 模式, 僅需在物件註冊時, 對其宣告為 Singleton 狀態 (每個 IOC 框架的語法有若干差異). 因此就不需要自己實作 Singleton Pattern(**IOC 框架幫你實作了**) ```C# // Register var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<IMyDependency, MyDependency>(); var app = builder.Build(); // Resolve using (var serviceScope = app.Services.CreateScope()) { var services = serviceScope.ServiceProvider; var myDependency = services.GetRequiredService<IMyDependency>(); myDependency.WriteMessage("Call services from main"); } ``` - [IOC 容器註冊](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0#service-registration-methods) ## 總結 - **Singleton 確保某個 class 型態只會有至多一個物件實體存在. 此物件實體僅可透過類別提供的存取點取得.** - Singleton 的實作通常會選 Lazy 的方式, 意即 Singleton 物件實體只有第一次被 Client 使用時, 才會初始化. - 雖然定義上 Singleton Pattern 只能允許至多一個物件實體存在, 但若實際情況需要, 或許可以加以變化, 改成允許至多 N 個. 只需要修改 Singleton class 對外的存取點即可 E.g. SingletonObj. - Singleton 在自行實作時, 對於 multi-thread 的情境時需要一些特別處理. - 程式碼耦合增加 - E.g. 在 A class 物件修改了 Singleton 物件的某個屬性, 可能會影響到 B class 物件操作 Singleton 物件後的結果. 像是 B class 需要依據 Singleton 物件的屬性判斷是否做某些事情. ## 參考 [Singleton](https://refactoring.guru/design-patterns/singleton) [隨手 Design Pattern (6) - 單例模式 (Singleton Pattern)](https://raychiutw.github.io/2019/%E9%9A%A8%E6%89%8B-Design-Pattern-6-%E5%96%AE%E4%BE%8B%E6%A8%A1%E5%BC%8F-Singleton-Pattern/) [[Design Pattern] Singleton 單例模式](https://ithelp.ithome.com.tw/articles/10221791) [什麼是 Singleton?(以 C# 為例)](https://blog.davy.tw/posts/singleton-basic-in-c-sharp/) [Lazy Initialization](https://docs.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization) [Design Pattern: Singleton - Class design vs Lazy<​T> vs Container](https://www.linkedin.com/pulse/design-pattern-singleton-class-vs-lazyt-container-mohamed-ebrahim/?trk=articles_directory) [Design patterns that I often avoid: Singleton](https://www.infoworld.com/article/3112025/design-patterns-that-i-often-avoid-singleton.html) [Programming Patterns Overview](https://kremer.cpsc.ucalgary.ca/patterns/) --- ###### Thank you! You can find me on - [GitHub](https://github.com/s0920832252) - [Facebook](https://www.facebook.com/fourtune.chen) 若有謬誤 , 煩請告知 , 新手發帖請多包涵 # :100: :muscle: :tada: :sheep: <iframe src="https://skilltree.my/c67b0d8a-9b69-47ce-a50d-c3fc60090493/promotion?w=250" width="250" style="border:none"></iframe>