Design Pattern / Creational - Singleton Pattern 單例模式
Singleton 保證一個 Class 只會有一個 Instance,並提供一個存取該 Instance 的全域節點。
Singleton 解決了兩個問題,但也因此違反了 Single Responsibility Principle。
Solution
所有的實現都包含以下兩個步驟:
- 將預設 constructor 設為 private,防止其他 object 使用 Singleton class 的
new
operator - 建立一個 static 建構方法作為 constructor。該函數會調用 private constructor 來建立物件,並將其儲存在一個靜態 field。之後所有對於該函數的調用都會回傳這一個 cache 物件。
如果程式碼能夠存取 Singleton class,那就用呼叫 Singleton 的靜態方法。無論何時調用該方法,都會回傳相同的物件。
Real-World Analogy
政府是 Singleton pattern 的一個很好的比喻。一個國家只有一個政府機關,不管組成政府的每個人的身份是什麼,該政府 X
是識別這些掌管者的全域存取節點。
Structure
Pseudocode
Applicability
適用於建立一個物件需要消耗的資源過多的時候,例如要存取 IO 和資料庫等資源
- 如果程式中的某個 class 對於所有 Client 只有一個可用的 Instance,可以使用 Singleton
- Singleton pattern 禁止透過除了特殊 constructor 方法以外的任何方式來建立自身 class 的物件。該方法可以建立一個新物件,但如果該物件已經被建立,則回傳已有的物件
- 如果需要更加嚴格的控制全域變數,可以使用 Singleton pattern
- Singleton pattern 跟全域變數不同,它只保證 class 存在一個 Instance。除了 Singleton class自己以外,無法透過其他方式替換 cache 的 Instance。
需要注意的是,隨時可以調整限制並設定生成 Singleton instance 的數量,只需要修改獲得 instance
方法,亦即getInstance
中的程式即可。
- Singleton pattern 跟全域變數不同,它只保證 class 存在一個 Instance。除了 Singleton class自己以外,無法透過其他方式替換 cache 的 Instance。
How to Implement
- 在 Class 中增加一個 private 靜態欄位來儲存 instance
- 宣告一個 public 靜態建立方法來獲得 singleton instance
- 在靜態方法中實現 "延遲初始化"。該方法會在首次被調用的時候建立一個新物件,並將其儲存在靜態欄位。之後該方法每次被調用都會回傳該 instance
- 將 class 的 constructor 設定為 private。Class 的靜態方法依舊能調用 constructor,但是其他物件不能
- 檢查 Client 程式碼,將對 singleton 的 constructor 的調用方式替換成其靜態建立方法
Pros and Cons
Pros
- 可以保證一個 class 只有一個 instance
- 得到一個指向該 Instance 的全域存取節點
- 只在首次請求 Singleton 物件的時候進行初始化
Cons
- 違反了 Single Responsibility Principle
- Singleton 可能會掩蓋不良的設計,比如程式元件彼此的關聯過多
- 該 pattern 在多執行緒情況下需要進行特別處理,避免多個執行緒多次建立 instance
- Singleton 的 Client 程式碼單元測試可能會比較困難。因為許多測試框架會用基於繼承的方式來建立模擬物件。由於 Singleton 的 constructor 是 private 的,而且絕大部分的語言無法重寫靜態方法,所以需要想出仔細考慮模擬 Singleton 的方法。或是就不要撰寫測試或不要使用 Singleton pattern
Relations with Other Patterns
- Facade class 通常可以轉換成 Singleton,因為大多數情況下一個 Facade instance 就足夠了
如果能將物件的所有共享狀態簡化成一個 Flyweight 物件,那麼 Flyweight 就和 Singleton 類似了。但這兩者的根本性質是不同的。
- 只有一個 Singleton instance,但是 Flyweight 可以有多個 Instance,各個 instance 的內在狀態也可以不同。
- Singleton 物件是可變的,Flyweight 物件是不可變的
- Abstract Factory、Builder & Prototype 都可以用 Singleton 來實現
Code Examples
Python
Naïve
class SingletonMeta(type):
"""
The Singleton class can be implemented in different ways in Python. Some
possible methods include: base class, decorator, metaclass. We will use the
metaclass because it is best suited for this purpose.
"""
_instances = {}
def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Singleton(metaclass=SingletonMeta):
def some_business_logic(self):
"""
Finally, any singleton should define some business logic, which can be
executed on its instance.
"""
# ...
if __name__ == "__main__":
# The client code.
s1 = Singleton()
s2 = Singleton()
if id(s1) == id(s2):
print("Singleton works, both variables contain the same instance.")
else:
print("Singleton failed, variables contain different instances.")
Thread-safe
from threading import Lock, Thread
class SingletonMeta(type):
"""
This is a thread-safe implementation of Singleton.
"""
_instances = {}
_lock: Lock = Lock()
"""
We now have a lock object that will be used to synchronize threads during
first access to the Singleton.
"""
def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
# Now, imagine that the program has just been launched. Since there's no
# Singleton instance yet, multiple threads can simultaneously pass the
# previous conditional and reach this point almost at the same time. The
# first of them will acquire lock and will proceed further, while the
# rest will wait here.
with cls._lock:
# The first thread to acquire the lock, reaches this conditional,
# goes inside and creates the Singleton instance. Once it leaves the
# lock block, a thread that might have been waiting for the lock
# release may then enter this section. But since the Singleton field
# is already initialized, the thread won't create a new object.
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class Singleton(metaclass=SingletonMeta):
value: str = None
"""
We'll use this property to prove that our Singleton really works.
"""
def __init__(self, value: str) -> None:
self.value = value
def some_business_logic(self):
"""
Finally, any singleton should define some business logic, which can be
executed on its instance.
"""
def test_singleton(value: str) -> None:
singleton = Singleton(value)
print(singleton.value)
if __name__ == "__main__":
# The client code.
print("If you see the same value, then singleton was reused (yay!)\n"
"If you see different values, "
"then 2 singletons were created (booo!!)\n\n"
"RESULT:\n")
process1 = Thread(target=test_singleton, args=("FOO",))
process2 = Thread(target=test_singleton, args=("BAR",))
process1.start()
process2.start()
JavaScript
/**
* The Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance.
*/
class Singleton {
private static instance: Singleton;
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
private constructor() { }
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
/**
* Finally, any singleton should define some business logic, which can be
* executed on its instance.
*/
public someBusinessLogic() {
// ...
}
}
/**
* The client code.
*/
function clientCode() {
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
if (s1 === s2) {
console.log('Singleton works, both variables contain the same instance.');
} else {
console.log('Singleton failed, variables contain different instances.');
}
}
clientCode();
Go
package main
import (
"fmt"
"sync"
)
var lock = &sync.Mutex{}
type single struct {
}
var singleInstance *single
func getInstance() *single {
if singleInstance == nil {
lock.Lock()
defer lock.Unlock()
if singleInstance == nil {
fmt.Println("Creating single instance now.")
singleInstance = &single{}
} else {
fmt.Println("Single instance already created.")
}
} else {
fmt.Println("Single instance already created.")
}
return singleInstance
}
Swift
Conceptual
import XCTest
/// The Singleton class defines the `shared` field that lets clients access the
/// unique singleton instance.
class Singleton {
/// The static field that controls the access to the singleton instance.
///
/// This implementation let you extend the Singleton class while keeping
/// just one instance of each subclass around.
static var shared: Singleton = {
let instance = Singleton()
// ... configure the instance
// ...
return instance
}()
/// The Singleton's initializer should always be private to prevent direct
/// construction calls with the `new` operator.
private init() {}
/// Finally, any singleton should define some business logic, which can be
/// executed on its instance.
func someBusinessLogic() -> String {
// ...
return "Result of the 'someBusinessLogic' call"
}
}
/// Singletons should not be cloneable.
extension Singleton: NSCopying {
func copy(with zone: NSZone? = nil) -> Any {
return self
}
}
/// The client code.
class Client {
// ...
static func someClientCode() {
let instance1 = Singleton.shared
let instance2 = Singleton.shared
if (instance1 === instance2) {
print("Singleton works, both variables contain the same instance.")
} else {
print("Singleton failed, variables contain different instances.")
}
}
// ...
}
/// Let's see how it all works together.
class SingletonConceptual: XCTestCase {
func testSingletonConceptual() {
Client.someClientCode()
}
}
Real World
import XCTest
/// Singleton Design Pattern
///
/// Intent: Ensure that class has a single instance, and provide a global point
/// of access to it.
class SingletonRealWorld: XCTestCase {
func testSingletonRealWorld() {
/// There are two view controllers.
///
/// MessagesListVC displays a list of last messages from a user's chats.
/// ChatVC displays a chat with a friend.
///
/// FriendsChatService fetches messages from a server and provides all
/// subscribers (view controllers in our example) with new and removed
/// messages.
///
/// FriendsChatService is used by both view controllers. It can be
/// implemented as an instance of a class as well as a global variable.
///
/// In this example, it is important to have only one instance that
/// performs resource-intensive work.
let listVC = MessagesListVC()
let chatVC = ChatVC()
listVC.startReceiveMessages()
chatVC.startReceiveMessages()
/// ... add view controllers to the navigation stack ...
}
}
class BaseVC: UIViewController, MessageSubscriber {
func accept(new messages: [Message]) {
/// handle new messages in the base class
}
func accept(removed messages: [Message]) {
/// handle removed messages in the base class
}
func startReceiveMessages() {
/// The singleton can be injected as a dependency. However, from an
/// informational perspective, this example calls FriendsChatService
/// directly to illustrate the intent of the pattern, which is: "...to
/// provide the global point of access to the instance..."
FriendsChatService.shared.add(subscriber: self)
}
}
class MessagesListVC: BaseVC {
override func accept(new messages: [Message]) {
print("MessagesListVC accepted 'new messages'")
/// handle new messages in the child class
}
override func accept(removed messages: [Message]) {
print("MessagesListVC accepted 'removed messages'")
/// handle removed messages in the child class
}
override func startReceiveMessages() {
print("MessagesListVC starts receive messages")
super.startReceiveMessages()
}
}
class ChatVC: BaseVC {
override func accept(new messages: [Message]) {
print("ChatVC accepted 'new messages'")
/// handle new messages in the child class
}
override func accept(removed messages: [Message]) {
print("ChatVC accepted 'removed messages'")
/// handle removed messages in the child class
}
override func startReceiveMessages() {
print("ChatVC starts receive messages")
super.startReceiveMessages()
}
}
/// Protocol for call-back events
protocol MessageSubscriber {
func accept(new messages: [Message])
func accept(removed messages: [Message])
}
/// Protocol for communication with a message service
protocol MessageService {
func add(subscriber: MessageSubscriber)
}
/// Message domain model
struct Message {
let id: Int
let text: String
}
class FriendsChatService: MessageService {
static let shared = FriendsChatService()
private var subscribers = [MessageSubscriber]()
func add(subscriber: MessageSubscriber) {
/// In this example, fetching starts again by adding a new subscriber
subscribers.append(subscriber)
/// Please note, the first subscriber will receive messages again when
/// the second subscriber is added
startFetching()
}
func startFetching() {
/// Set up the network stack, establish a connection...
/// ...and retrieve data from a server
let newMessages = [Message(id: 0, text: "Text0"),
Message(id: 5, text: "Text5"),
Message(id: 10, text: "Text10")]
let removedMessages = [Message(id: 1, text: "Text0")]
/// Send updated data to subscribers
receivedNew(messages: newMessages)
receivedRemoved(messages: removedMessages)
}
}
private extension FriendsChatService {
func receivedNew(messages: [Message]) {
subscribers.forEach { item in
item.accept(new: messages)
}
}
func receivedRemoved(messages: [Message]) {
subscribers.forEach { item in
item.accept(removed: messages)
}
}
}