iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Software Development

也該是時候學學 Design Pattern 了系列 第 10

Day 10: Creational patterns - Singleton

目的

建立一個「唯一」物件,專責於服務只能單一連線的情境,例如跟資料庫的溝通,同時確保全域內都可以呼叫該物件。

說明

Singleton 是相當好懂的模式,用在只能服務單一連線的情境下,避免多重連線產生時間差,導致執行完畢的結果不是我們要的,例如:

  1. 與資料庫溝通的寫入,時間差可能會讓寫入的順序錯亂。
  2. 權限的許可,時間差可能讓權限判定失效。

相關作法是:

  1. 避免物件擁有建立的可能性,因此要封鎖建構式。
  2. 物件一定有一個方法:getInstance(),負責回傳 instance,讓全域都可以呼叫並使用。
  3. 物件可能有其他方法。

Java 因為可以跑多的執行緒(Thread),所以在 getInstance() 除了原有的鎖之外還要多加兩道鎖:

  1. 確認 instance 是否存在?
  2. 確認是不是有其他執行緒正在建立中?
  3. 要建立時,確認 instance 是否存在?

至於 JavaScript,不能使用常見的 class 寫法,而是使用 IIFEs(Immediately Invoked Functions Expressions),讓物件只會建立一次,沒有其他建立的方法。

UML 圖

Singleton Pattern UML Diagram

使用 Java 實作

Singleton: EmergencyTelephone

public class EmergencyTelephone {
    private static EmergencyTelephone instance;

    private EmergencyTelephone() {
    }

    private void useTelephone(String phoneNumber) {
        System.out.println("(號碼:" + phoneNumber + ")拿起話筒");
        System.out.println("(號碼:" + phoneNumber + ")輸入號碼");
        System.out.println("(號碼:" + phoneNumber + ")等待接通");
        System.out.println("(號碼:" + phoneNumber + ")確認電話已經打通");
    }

    private void tellDetails(String phoneNumber) {
        System.out.println("(號碼:" + phoneNumber + ")詳述情況");
        System.out.println("(號碼:" + phoneNumber + ")告知地點");
        System.out.println("(號碼:" + phoneNumber + ")記錄指示");
    }

    private void finishTelephoneTalk(String phoneNumber) {
        System.out.println("(號碼:" + phoneNumber + ")掛上電話");
        System.out.println("(號碼:" + phoneNumber + ")思考指示");
        System.out.println("(號碼:" + phoneNumber + ")行動");
    }

    public synchronized void execute(String phoneNumber) {
        useTelephone(phoneNumber);
        tellDetails(phoneNumber);
        finishTelephoneTalk(phoneNumber);
    }

    public static EmergencyTelephone getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new EmergencyTelephone();
                }
            }
        }

        return instance;

    }
}

進行測試

public class EmergencyTelephoneSample extends Thread {
    String phoneNumber;

    public EmergencyTelephoneSample(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public void run() {
        EmergencyTelephone emergencyTelephone = EmergencyTelephone.getInstance();
        if (emergencyTelephone != null) {
            System.out.println("這通電話號碼是:" + phoneNumber + ",這台電話的生產序號是:" + emergencyTelephone.hashCode());
            emergencyTelephone.execute(phoneNumber);
        }
    }

    public static void main(String[] args) {
        EmergencyTelephone emergencyTelephone1 = EmergencyTelephone.getInstance();
        EmergencyTelephone emergencyTelephone2 = EmergencyTelephone.getInstance();

        if (emergencyTelephone1.hashCode() == emergencyTelephone2.hashCode()) {
            System.out.println("兩個是同一個物件");
        }

        Thread t1 = new EmergencyTelephoneSample("119");
        Thread t2 = new EmergencyTelephoneSample("110");
        t1.start();
        t2.start();
    }
}

使用 JavaScript 實作

Singleton: EmergencyTelephone

const EmergencyTelephone = (() => {
  let instance = null;

  function initialize() {
    let hashCode = Math.floor(Math.random() * 10000000);

    function useTelephone(phoneNumber) {
      console.log("(號碼:" + phoneNumber + ")拿起話筒");
      console.log("(號碼:" + phoneNumber + ")輸入號碼");
      console.log("(號碼:" + phoneNumber + ")等待接通");
      console.log("(號碼:" + phoneNumber + ")確認電話已經打通");
    }

    function tellDetails(phoneNumber) {
      console.log("(號碼:" + phoneNumber + ")詳述情況");
      console.log("(號碼:" + phoneNumber + ")告知地點");
      console.log("(號碼:" + phoneNumber + ")記錄指示");
    }

    function finishTelephoneTalk(phoneNumber) {
      console.log("(號碼:" + phoneNumber + ")掛上電話");
      console.log("(號碼:" + phoneNumber + ")思考指示");
      console.log("(號碼:" + phoneNumber + ")行動");
    }

    return {
      execute: function (phoneNumber) {
        useTelephone(phoneNumber);
        tellDetails(phoneNumber);
        finishTelephoneTalk(phoneNumber);
      },
      getHashCode: function () {
        return hashCode;
      }
    };
  }

  return {
    getInstance: function () {
      if (instance === null) {
        instance = initialize();
      }

      return instance;
    }
  }
})();

進行測試

const emergencyTelephoneSample = () => {
  const emergencyTelephone1 = EmergencyTelephone.getInstance();
  const emergencyTelephone2 = EmergencyTelephone.getInstance();

  if (emergencyTelephone1.getHashCode() === emergencyTelephone2.getHashCode()) {
    console.log("兩個是同一個物件");
  } else {
    console.log("出問題,兩個不是同一個物件");
  }

  console.log("進行第一通電話撥打");
  const phoneNumber1 = "119";
  console.log("這通電話號碼是:" + phoneNumber1 + ",這台電話的生產序號是:" + emergencyTelephone1.getHashCode());
  emergencyTelephone1.execute(phoneNumber1);

  console.log("進行第二通電話撥打");
  const phoneNumber2 = "110";
  console.log("這通電話號碼是:" + phoneNumber2 + ",這台電話的生產序號是:" + emergencyTelephone2.getHashCode());
  emergencyTelephone2.execute(phoneNumber2);
}

emergencyTelephoneSample();

總結

Singleton 是非常容易理解的模式,也是十分常見的模式。

要注意的反倒是語言特性,不同語言執行上有細節,例如執行緒的多寡,將影響 Singleton 的效果,這點唯有加強自身對該語言的熟練才能避免。

這是最後一篇 Creational patterns,明天將進入下個類別:Structural patterns 的第一個模式:Adapter 模式。


上一篇
Day 09: Creational patterns - Prototype
下一篇
Day 11: Structural patterns - Adapter
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言