---
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

### 名詞定義
#### 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>