iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 12
2
Software Development

什麼?又是/不只是 Design Patterns!?系列 第 12

[Design Pattern] Singleton 單例模式

很多時候會希望一個類別只會有唯一一個實體,像是 DB 的接口、應用程式的偏好設定、 一個中國

這時就會需要 Singleton Pattern 了。它的實作很簡單,也很方便。但是要小心不要過度使用了,Singleton 也可以是一個 anti pattern,這部分會在下半段說明。

Singleton Pattern 介紹

想要確保一個類別只有一個實體的話,要怎麼做到呢?首先標準的物件導向語言都有提供 private, protected 等關鍵字來限制存取範圍(通常稱為 scope),只要使用以上關鍵字來限制建構子的存取範圍,再搭配公開的靜態方法 (public static method)、靜態成員變數(static field),就能讓外部存取指定的實例,而且確保該實例是全域唯一的實體。

上面這段話很抽象嗎?直接來看程式碼吧!

以 Java 為例:

class DBHelper {

   //通常我們會命名該實體為 'instance' 
   private static DBHelper instance;
   
   //只能透過這個 method 來拿到唯一實體
   public static DBHelper getInstance() {
      if (instance == null) {
          instance = new DBHelper();
      }
      
      return instance;
   }
   
   //建構子不能公開
   private DBHelper() {}
   
   //DBHelper 提供的 public method, 
   public boolean insert(SomeBean bean) {
      ...
   }
}

這邊還包含了一個 null 檢查,為什麼呢?因為有時候不希望一開始就佔了系統資源,像是 Android App 的開啟時間是很珍貴的,如果開啟時間太久使用者可是會刪掉 App 的!而這種需要才建立實例的方式就叫做 lazy initialization。

我知道這時候有人就會開始說,你這樣做不對喔!沒有考慮到 thread safe,應該用 double checked-locking 才行。的確很多網路上的資源都有提到這個,多執行緒會是一個問題。但是想想喔,在怎樣的情況下,會有兩個不一樣的 thread 會呼叫 getInstance(),而且還是在還沒初始化的情況下。

我相信這種情況應該很少遇到才對(尤其是 App 開發),以我本身的經驗,大部分的人在應用程式初始化的時候就建立好實例了。在這種情況下,說不定連 lazy initialization 都不用。所以在這裡想要提供大家另一個觀點,在使用 double checked-locking 之前,要多想想為什麼需要他,以現有的架構下有可能從不同的 thread 去呼叫 getInstance()嗎?只在指定的 thread 上呼叫是可行的嗎?還是其實跟本不需要 Singleton?

不知道有沒有讀者跟我一樣不喜歡 double checked-locking ,看看 wiki 連結的內容,要考慮的問題遠比你想像中的多!

Singleton Pattern 優點

  • 簡單、方便。提供一個唯一的的入口來操作物件。不管何時何地都能透過 XXX.getInstance() 來進行操作,不用經過一系列的物件初始化動作。
  • 確保資料的正確性,假設有一個物件存放著使用者的登入狀態,如果再建立另一個一樣的物件將無法確保他們都會有一樣的登入狀態。因此產生不同步的行為。

Singleton Pattern 的不良示範

前面有提到 Singleton 有可能過度使用,以下提供幾點:

  • 不要在 Singleton 裡寫複雜邏輯,Singleton 應該隨時保持單純,可以把他當成簡單的 wrapper ,通常可以搭配 Facade pattern 一起使用。另一個不這麼做的原因是 Singleton 不好寫測試。
  • 不要到處使用 getInstance(),相對的應該考慮使用 dependency injection 將需要的類別注入到將要使用的類別,使用 getInstnace() 將會使得單元測試極為困難。以下是一個不好的範例
class AccountService {

    private AccountRepo accountRepo = 
        //不要這樣做,方便不代表隨便
        DBHelper.getInstance().getAccountRepo()

    public AccountService() {}
    
    public boolean hasLogin() {
       //還有可能忘記上面就拿過 AccountRepo 了
       Account account = DBHelper.getInstance()
                                 .getAccountRepo()
                                 .getAccount()
                                 
       ...
    }
    
    ...
}

class AccountService {

    private AccountRepo accountRepo;

    //真正需要的只有 AccountRepo,不用認識 DBHelper,在閱讀程式碼的時候也會比較清楚他們的相依性
    public AccountService(AccountRepo accountRepo) {
        this.accountRepo = accountRepo;
    }
    
    ...
}

class AccountServiceTest {

   @Test
   public void verifyAccount() {
       //測試好寫多了,要怎樣的測試資料都很好做
       AccountRepo repo = new FakeAccountRepo("John", true);
       AccountService service = new AccountService(repo);
       ...
       assertTrue(service.hasLogin())
   }
}
  • 要的變數拿不到?全部放到 Singleton 不就好了!這個應該是最常見的錯誤用法了,一但這成為了習慣,程式架構將會變得雜亂不堪,各式各樣的變數放在同一個地方,可能還有好幾個重複的、幾乎一樣的檢查一再出現。那要怎麼解決呢?通常遇到這種情況應該要離開你的鍵盤,拿出紙跟筆,好好思考問題以及動手設計吧。

總結

Singleton 要解決的問題是唯一實例。同一個系統裡面只會有同一種狀態,但是由於他方便的特性(可以隨時呼叫靜態方法),會讓大家開始濫用。因此在使用時要隨時注意有沒有犯了文章上面的錯誤。

另一方面,很多語言都有提供 DI 以及 IOC 框架,這些框架可以幫你建立實例,只要預先寫好物件的相依關係即可。同時還可以幫你解決 Singleton 要做的事情,可以不用自己實作 。但是要注意一但用了這些框架,Singleton 所提供的保護將不會存在,如果有一個新人不知道框架的運作方式的話,還是有可能會有問題的。

沒有最好的解法,只有最適合的解法,要使用 Singleton 還是 DI 框架,還是要各個團隊自己來決定,今天的文章就到這邊,感謝大家的閱讀。

Reference

作者:Yanbin


上一篇
[Design Pattern] Prototype 原型模式
下一篇
[Design Pattern] Proxy 代理模式
系列文
什麼?又是/不只是 Design Patterns!?32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言