iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 24
1
Software Development

Learning Design Pattern in 30 real-case practices系列 第 24

別再因為發號碼牌重複被客訴! (Singleton 單例模式)

Singleton 單例模式

僅將此篇文章獻給我的摯友、導師,Charles,此篇文章參考了大部分他的知識和文章,原文:程湘之間

需求描述

Amy(PO):

As a 業務副總
I want 在明天的新產品線上預購活動,讓預購客戶取得一個唯一的預購號碼,而且要在產品發表會螢幕上打上線上預購的數目。
So that 由這個成功的產品發表會來增加產品曝光率

思考設計

Hachi:
「在產品發表會螢幕上打上線上預購的數目」? 讓我想到購物節電商即時顯示的即時交易數據!

JB:
你說的沒錯! 這次就是這樣搞!
不過我比較擔心如果預購的人很零星,那個螢幕上的預購數字動很慢...或跟本不動...不就太瞎了...

Lily:
別替他們擔心! 業務部的人已經評估一個預購量,讓我們在預購開始時,直接逐筆累加在螢幕上的預購數! 讓現場看起來幾秒之內我們就收到了幾萬筆預購!! 然後螢幕放個綵帶的動畫...副總上台致詞轉移焦點...結束完美的發表會!

Hachi:
所以我們寫個迴圈,每秒累加一個亂數就算完成這個需求了嗎!

Lily:
哈哈! 你學的很快! 不過我們重點是在於真正線上客戶的預購! 畢竟他們收到的預購號碼不能重複!
所以我們加上單例模式(Singleton)來滿足這個需求!

定義

確保類別只有一個實例(instance),並提供所有對象訪問這個實例的方法。(WIKI)

Singleton in C#

在C#中,我們可以將 Singleton分成以下幾種:

  1. Non thread-safe
  2. Thread-safe using double-check locking
  3. Thread-safe, eager singleton
  4. Thread-safe, lazy singleton

在實作Singleton類別前,我們先建立一個提供預購號碼的父類別:

public class NumberProvider
{
    protected int Counter = 0;
    public int GetNumber()
    {
        Counter++;
        return Counter;
    }
}

接著我們直接讓Singleton類別繼承NumberProvider以在主程式可以使用GetNumber()
讓我們可以專注在如何確保Singleton類別永遠只有產生一個實例。

Non thread-safe

public sealed class NonThreadSafeSingleton: NumberProvider
{
    private static NonThreadSafeSingleton INSTANCE = null;
    public static NonThreadSafeSingleton Instance 
    {
        get
        {
            if (INSTANCE == null)
                INSTANCE = new NonThreadSafeSingleton();

            return INSTANCE;
        }
    }
}

在多執行緒環境,有可能在取得實體時:NonThreadSafeSingleton.Instance造成在各執行緒同時判斷INSTANCE == null成立,而重複建立INSTANCE的情況。

例如在單元測試程式模擬同時有十位客戶(10 Threads)作線上預購,結果使用Non thread-safe Singleton造成其中五位客戶產生了重複的預購號碼: 1 和 2 。

(19)陳 先生預購2組,預購號碼為: 1,2
(21)施 先生預購2組,預購號碼為: 1,2
(20)謝 先生預購2組,預購號碼為: 1,2
(22)林 先生預購2組,預購號碼為: 1,2
(8)林 先生預購2組,預購號碼為: 1,2
(8)李 先生預購2組,預購號碼為: 11,12
....

PS. 以上輸出格式為 (執行緒ID) XXX預購N組: 預購號碼

Thread-safe using double-check locking

我們改採用lock(object)來確保同時只有一條thread可以建立instance。

public sealed class DoubleCheckSingleton: NumberProvider
{
    private static DoubleCheckSingleton INSTANCE = null;
    static readonly object padlock = new object(); //用來LOCK建立instance的程序。
    public static DoubleCheckSingleton Instance
    {
        get
        {
            if (INSTANCE == null)
            {
                lock (padlock) //lock此區段程式碼,讓其它thread無法進入。
                {
                    if (INSTANCE == null)
                    {
                        INSTANCE = new DoubleCheckSingleton();
                    }
                }
            }
            return INSTANCE;
        }
    }
}

Thread-safe, eager singleton

比較簡潔的Singleton寫法,在宣告INSTANCE時即建立實體,所以加載此類別時實體會立即被建立。

public sealed class EagerSingleton: NumberProvider
{
    private static EagerSingleton INSTANCE = new EagerSingleton();
    public static EagerSingleton Instance 
    {
        get
        {
            return INSTANCE;
        }
    }
}

Thread-safe, lazy singleton

不同於Eager Singleton,Lazy Singleton只會在真正使用到實體時才建立。

public sealed class LazySingleton : NumberProvider
{
    public static LazySingleton Instance
    {
        get
        {
            return InnerClass.instance;
        }
    }
    class InnerClass
    {
        static InnerClass()
        {
        }
        internal static readonly LazySingleton instance = new LazySingleton();
    }
}

以上除了Non thread-safe Singleton,其他皆可確保在多執行緒環境下建立單例實體。
但是要注意在操作這些單例類別裡面的變數時,也需要確保它的存取是Thread safe的。
所以我們更新NumberProvider裡面的GetNumber()方法如下:

public class NumberProvider
{
    private static readonly object numberBlock = new object();
    protected int Counter = 0;
    public int GetNumber()
    {

        lock (numberBlock)
        {
            Counter++;
            return Counter;

        }
    }
}

這樣就可以確保在多個客戶同時線上預購時,不會取到相同的號碼。
單元測試程式碼請參考這裡

Singleton in Python

在Python因為沒有private修飾詞,無法在類別裡建立私有的變數來作為單例實體。
比較直覺的方式是採用module的方式來作為Singleton模式。
或是利用以下程式碼建立一個單例類別NumberProvider

def singleton(cls):
    obj = cls()
    # Always return the same object
    cls.__new__ = staticmethod(lambda cls: obj)
    # Disable __init__
    try:
        del cls.__init__
    except AttributeError:
        pass
    return cls


@singleton
class NumberProvider():
    counter =0
    def getNumber(self):
        self.counter+=1
        return self.counter

在多執行緒的測試程式碼:

class GetNumberThread(threading.Thread):
    def __init__(self):
        super().__init__()

    def run(self):
        for i in range(0, 30):
            singleton = NumberProvider()
            number = singleton.getNumber()
            print(number)


if(NumberProvider()==NumberProvider()):
    print('Correct!')
    
threads = []
for i in range(0,10):
    thread = GetNumberThread()
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

Sample Codes

  1. C#
  1. Python

Reference


上一篇
察言觀色! 敵不動,我不動! (Observer 觀察者模式)
下一篇
[Design Pattern實例] 在策略模式使用委派解耦合
系列文
Learning Design Pattern in 30 real-case practices30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言