Singleton Pattern 單例模式

什麼是 Singleton Pattern 單例模式

Singleton 確保一個類別只有一個實例,並提供對該實例的全域存取。

該模式並沒有規定如何處理,這是我們可以發揮創意的地方。

在實作上,你必須 自己控制實例化(instantiation)方式

由於是全域存取唯一的實例,相對應的處理必須 序列化(Serialization),保證不同客戶端來的需求,都能依序處理。

  1. Ensure that the class has only a single instance.

  2. Provide easy global access to this instance.

  3. Control how it is instantiated.

  4. Any critical region must be entered serially.

GoF 四人幫對 Singleton Pattern 單例模式的描述為:

It’s important for some classes to have exactly one instance. Although there can be many printers in a system, there should be only one printer spooler. There should be only one file system and one window manager…(即「列印多工緩衝處理器」服務)


何處使用

雖然很多人說不要使用 Singleton Pattern 單例模式,事實上還是有適用之處。例如(應該一看秒懂):

  • Loggers

  • Caching

  • Thread Pools

  • Database Connections

  • Configuration Access

我們的共學課程,提到的是 object factory,我反而不太了解和 Singleton 的關係。

單例經常與其他模式一起使用(應該看實例):

  1. Abstract Factory

  2. Builder

  3. Prototype

  4. Facade

  5. State


使用時機

何時使用

當你想要控制對 共享資源 的存取。

何時不應該使用

  1. 謹慎使用,不要讓它退化到 只剩全域存取的功能,這樣會使程式難以理解和維護。

  2. 是否違反了 SRP(單一責任原則)

    例如:使用一個單例,同時處理 logger 和 cache,這就是違反了 SRP。


設計考量

1. 載入時機

有兩種設計 Singleton Pattern 單例模式的思維:

Lazy Construction(延遲建構)

實例只在第一次要使用前創建。(也可以稱為 Lazy Loading,比較對仗)

如果不確定何時使用,也不想浪費資源,可以考慮這種方式。

Eager Loading(急切載入)

無論是否馬上用到,一開始就先準備好。

範例:射擊遊戲中的音效。你在遊戲開始時,就使用單例載入,但等到遊戲中真正發射時才存取播放。

主要的考量在記憶體。

2. Thread Safety(線程安全)

因為 Python 支援多執行緒,我們在設計 多執行緒的單例 時,會用鎖住關鍵區塊(lock the Critical Section)來妥善安排其 序列化(Serialization)

Singleton Pattern Architecure 單例模式架構

單例模式可以透過以下三種方式達成:

  • Base class

  • Decorator

  • Meta class(最合適)

不過在開始前,我們先看單例模式的基礎架構(與 Python 無關)。

通用型 Singleton Pattern

通常透過以下方式完成:

  • 將類別的所有建構函式宣告為 private,防止它被其他物件實例化

  • 提供傳回實例引用的靜態方法

實例通常儲存為私人靜態變數;該實例是在變數初始化時創建的,在首次呼叫靜態方法之前的某個時刻創建。

以下表格模仿 UML 說明

Singleton 說明
- instance: Singleton private static instance
在類別層級而不是物件層級定義,所以只能有一個實例。
- Singleton() private constructor 防止外部呼叫實例化
+ getInstance(): Singleton 透過呼叫 static getInstance 方法來取得實例
這種方式是 Lazy Construction(延遲建構)
if (instance == null):
    instance = new Singleton()
return instance
隱藏細節

示意圖中,有一個名為 Singleton 的類別,它具有以下元素:

  1. instance: 這是一個靜態(或類別級別)成員變數,它保存了 Singleton 類的唯一實例。

  2. constructor(): 這是一個私有的建構函數,它用來防止從外部創建額外的實例。只有 Singleton 類自己的內部方法可以調用它。

  3. getInstance(): 這是一個靜態方法,它提供了對 Singleton 實例的全局訪問點。如果 Singleton 實例已經存在,則返回該實例;否則,它會創建一個新的實例並返回。

Singleton 設計模式確保了一個類別只能有一個實例存在,並提供了一個方便的方式來訪問該實例。這對於需要共享單一資源或控制全局設置的情況非常有用。通常,Singleton 模式還可以實現一些額外的功能,如延遲實例化(只有在需要時才創建實例)或執行緒安全(確保多執行緒環境下也能正確運作)。不過,上面的示意圖只顯示了 Singleton 模式的基本結構。

方法一:改寫 __new__

本節介紹的方式,是改寫 allocator(即 __new__),但如果只實作 __new__,對於有 __init__ 的 class 會有問題,所以要一併改寫 __init__

簡單的說,就是建立一個 _instance 參數。如果沒創建過,就創建並記錄進該參數;如果創建過,就回傳該參數。

import random

class Database:
    ## initialized = False

    def __init__(self):
        self.id = random.randint(1,101)
        print('Generated an id of ', self.id)
        print('Loading database from file')
        pass

    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Database, cls).__new__(cls, *args, **kwargs)

        return cls._instance
d1 = Database()
d2 = Database()

print("===============================")
print(d1.id, d2.id)
print(d1 == d2)

輸出:

Generated an id of  82
Loading database from file
Generated an id of  10
Loading database from file
===============================
10 10
True

方法二:decorator 裝飾器

裝飾器的方法,很接近方法一。

在 class singleton,建立一個 _instances 參數(dict)。然後在 get_instance 檢查,如果沒創建過,就創建並記錄進該參數;如果創建過,就回傳該參數。

然後使用 decorator 裝飾器把 class Database 包起來。

def singleton(class_):
    _instances = {}

    def get_instance(*args, **kwargs):
        if class_ not in _instances:
            _instances[class_] = class_(*args, **kwargs)
        return _instances[class_]

    return get_instance


@singleton
class Database:
    def __init__(self):
        print('Loading database')


d1 = Database()
d2 = Database()
print("===============================")
print(d1 == d2)

輸出:

Loading database  ## 只列印一次
===============================
True

方法三:metaclass 元類別

metaclass 是最適合的做法,透過 __call__ 這個 method 來控制是否創建。

在 class singleton,建立一個 _instances 參數(dict)。然後在 __call__ 檢查,如果沒創建過,就創建並記錄進該參數;如果創建過,就回傳該參數。

class Singleton(type):
    """ Metaclass that creates a Singleton base type when called. """
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


class Database(metaclass=Singleton):
    def __init__(self):
        print('Loading database')


d1 = Database()
d2 = Database()
print("===============================")
print(d1 == d2)

輸出:

Loading database  ## 只列印一次
===============================
True

Monostate(Borg)

MonostateSingleton 在確保應用程式中,可以全域存取同樣的資料,但實現的方式不同。

Monostate 可以有許多實例,這些實例共享相同的屬性(狀態)、

Singleton 單例可以具有屬性,但主要特點是只有一個實例(同一個實例的屬性,當然相同)。

設計目的

  • Monostate:Monostate 確保在應用程式中的 所有實例共享相同的狀態。每個實例都可以設置狀態,但所有實例都將讀取和修改相同的狀態變數。Monostate 主要用於共享狀態而不是單例實例。

  • Singleton:Singleton 確保在整個應用程式中只有一個實例可用。這種模式的主要目的是 限制類別的實例數量,以確保全局資源或服務的獨一性。

狀態共享

  • Monostate:所有 Monostate 的實例都共享相同的狀態變數。當一個實例修改狀態時,所有其他實例都會受到影響,因為它們都操作同一個狀態變數。

  • Singleton:Singleton 的主要重點是確保只有一個實例存在,而不關心狀態的共享。單例可以具有狀態,但它的主要特點是只有一個實例。

實作方式

  • Monostate:實作 Monostate 通常在類中 使用 靜態成員變數 來存儲狀態。這使得所有實例都共享相同的狀態。

  • Singleton:實作 Singleton 通常使用私有的建構函數、靜態成員變數和一個靜態方法(通常稱為 getInstance)來確保只有一個實例。Singleton 也可以擁有私有的狀態變數,但主要目的是確保實例數量。

# all members are static

class CEO:  ##  Monostate 模式
    __shared_state = {  ##  CEO 的共享狀態,即 'name' 和 'age'。
        'name': 'Steve',
        'age': 55
    }

    def __init__(self):
        ## 將實例的 __dict__ 設置為 __shared_state,這樣所有 CEO 的實例都共享相同的狀態。
        self.__dict__ = self.__shared_state

    def __str__(self):
        return f'{self.name} is {self.age} years old'

class Monostate:  ## 實現 Monostate 模式的基類
    _shared_state = {}  ## 類別層級變數,用於存儲共享的狀態

    def __new__(cls, *args, **kwargs):  ## 改寫 `__new__`,以確保只有一個實例存在。
        obj = super(Monostate, cls).__new__(cls, *args, **kwargs)
        obj.__dict__ = cls._shared_state
        return obj

class CFO(Monostate):  ## 繼承 Monostate 並定義了自己的狀態
    def __init__(self):  ## 定義自己的狀態變數,包括 'name' 和 'money_managed'
        self.name = ''
        self.money_managed = 0

    def __str__(self):
        return f'{self.name} manages ${self.money_managed}bn'
ceo1 = CEO()
print(ceo1)  ## Steve is 55 years old

ceo1.age = 66

ceo2 = CEO()
ceo2.age = 77
print(ceo1)  ## Steve is 77 years old
print(ceo2)  ## Steve is 77 years old

ceo2.name = 'Tim'

ceo3 = CEO()
print(ceo1, ceo2, ceo3)  ## Tim is 77 years old Tim is 77 years old Tim is 77 years old

cfo1 = CFO()
cfo1.name = 'Sheryl'
cfo1.money_managed = 1

print(cfo1)  ## Sheryl manages $1bn

cfo2 = CFO()
cfo2.name = 'Ruth'
cfo2.money_managed = 10
print(cfo1, cfo2, sep='\n')  ## Ruth manages $10bn \n Ruth manages $10bn

比較簡潔的例子

class Monostate:
    _shared_state = {}  # 這是共享的狀態變數

    def __init__(self):
        self.__dict__ = self._shared_state  # 將 __dict__ 設置為共享狀態

    def set_data(self, key, value):
        self._shared_state[key] = value

    def get_data(self, key):
        return self._shared_state.get(key)


# 創建兩個 Monostate 實例
instance1 = Monostate()
instance2 = Monostate()

# 設置狀態
instance1.set_data("name", "Alice")

# 檢查狀態
print("Instance 1 Name:", instance1.get_data("name"))  # 這會輸出 "Instance 1 Name: Alice"
print("Instance 2 Name:", instance2.get_data("name"))  # 這也會輸出 "Instance 2 Name: Alice"
print(instance1 == instance2)

輸出:

Instance 1 Name: Alice
Instance 2 Name: Alice
False

Singleton Testability

因為 Singleton 只有一個實例,所以測試時就是那個實例。對於線上資料庫,這樣做有風險。

解決方式很單純,就是多建一個 Dummy Database 來解決定這個問題。

class SingletonRecordFinder:
    def total_population(self, cities):
        result = 0
        for c in cities:
            result += Database().population[c]  ## 這裡會直接存取到資料庫
        return result
class SingletonTests(unittest.TestCase):
    def test_is_singleton(self):
        db = Database()
        db2 = Database()
        self.assertEqual(db, db2)  ## 輸出:OK

    def test_singleton_total_population(self):
        """ This tests on a live database :( """
        rf = SingletonRecordFinder()
        names = ['Seoul', 'Mexico City']
        tp = rf.total_population(names)
        self.assertEqual(tp, 17500000 + 17400000)  # what if these change?  ## 輸出:OK

    ddb = DummyDatabase()

    def test_dependent_total_population(self):
        crf = ConfigurableRecordFinder(self.ddb)  ## Dummy Database
        self.assertEqual(   ## 輸出:OK
            crf.total_population(['alpha', 'beta']),
            3
        )
Tokyo
33200000
New York
17800000
Sao Paulo
17700000
Seoul
17500000
Mexico City
17400000
Osaka
16425000
Manila
14750000

以下未整理

Python

在 Python 中,並沒有一個單獨的建構函數,但我們可以透過 __new____init__ 來實作。

  • __new__: This is a static method that is responsible for creating and returning a new instance of a class. It is the first step in the object instantiation process.

  • __init__: This is an instance method that is responsible for initializing an object’s attributes after it has been created by __new__. The __init__ method does not return a value and is called automatically after the object is created.

  • __call__: This is an instance method that allows a class’s instances to be called as if they were functions.

    In a metaclass, this method allows instances of the metaclass (which are class objects themselves) to be called as if they were functions.

    This is executed when creating instances of the classes created by the
    metaclass.

    By customizing this method, you can control how instances of
    those classes are created or returned.

接下來我們會實作三種 Singleton Pattern 單例模式:

  • GoF 傳統方式的 Singleton Pattern 單例模式

    1. This will give us the most simple and generic version.

    2. We will control constructor access.

    3. Instantiation will be through static method.

  • Simple (or Naive) Python Singleton

    1. No constructor control, so instantiation will be through constructor.

    2. 改寫 __new__ method

  • metaclass 方式的 Singleton Pattern 單例模式

    1. We will override the __call__ method

    2. We could also use the __new__ and __init__ methods as we will see in later examples.