【TS】今さら聞けないシングルトンパターン

3 min read読了の目安(約3000字 2

はじめに

今回はシングルトンパターン(Singleton Pattern)について解説します。
インスタンスの唯一性を保証する、GoFの23種のデザインパターンの中でも有名なデザインです。

シングルトンパターンとは?

有名どころでTECHSCOREさんの解説があります。

以下、引用です。

あるクラスのインスタンスが一つしかないことを保証したい場合があります。注意深く設計すれば、唯一のインスタンスを使いまわすことは可能でしょうが、このインスタンスが唯一であることを保障するものとはなりません。このような場合に、威力を発揮するのが Singleton パターンです。

Singleton パターンは、コンストラクタを private とすることで、他クラスから新たにインスタンスが生成されないような構造とすることで、インスタンスの生成を制御します。まずは、サンプルケースを考えて見ましょう。

続いてサンプルケース。

サンプルケースでは、図書館の貸出帳を考えて見ます。図書の貸出に当たって、その貸出帳がいくつあるのかわからないような状態では、、管理がとても難しくなります。例えば、「ゼロからはじめるJava」は貸し出し中ですか?と質問されたとき、貸出帳が何冊あるのか分からなければ、「この貸出帳によると、現在図書館にあるはずですが、貸出帳は他にもあるかもしれないし・・・」なんてことになりかねません。こんなとき「貸出帳はこの1冊のみ」と保証されていれば、本が貸し出し中であるのか、図書館にあるのかすぐに判断できます。

ポイントは、あくまで「保証」ということです。
この例だと、シングルトンパターンを用いずとも貸出帳の機能で排他制御をしっかりと行なっていれば、貸出内容の齟齬は防げるかもしれません。
ただ、そういったバグを起こさないように「設計レベルでインスタンスの唯一性を保証」するのがシングルトンパターンです。

例題

以下のような内容のクラスを想定します

  • NormalUserAdminUserという2種のクラスを定義する
  • それぞれIUserというインターフェースを実装する
  • IUserdispInfo()という関数を定義する
  • NormalUserはコンストラクタの引数でユーザ名を受け取る
  • NormalUserdispInfo()で自身のユーザ名を表示する
  • AdminUserdispInfo()で管理ユーザである旨を表示する
  • AdminUserインスタンスは唯一とする

ポイントは「AdminUserのインスタンスが唯一」かつ「IUserを実装している」という点です。
インスタンスを作りたくないだけなら、private constructorとしてインスタンスを生成できないようにした上で、staticdispInfoを宣言すれば実装できます。

class AdminUser {
  // コンストラクタをprivateで宣言することで
  // インスタンスを生成できなくする
  private constructor() {}

  static dispInfo = () => {
    console.log('Admin User');
  }
}

AdminUser.dispInfo(); // --> Admin User

俗に言う「ユーティリティクラス」です。
しかし、例のようにインターフェースを実装したい場合はdispInfostaticにすることができません。

シングルトンパターンで実装

シングルトンパターンで実装した例が下記になります。
AdminUser_instanceを持っており、instanceのゲッターで返却しています。
外部からはinstanceを経由してアクセスすることしかできず、結果としてインスタンスの唯一性が保証されています。

// ユーザIF
interface IUser {
  dispInfo: () => void;
}

// 通常ユーザ
class NormalUser implements IUser {
  private _name: string;
  constructor(name: string) {
    this._name = name;
  }

  dispInfo = () => {
    console.log("UserName is ", this._name);
  }
}

// 管理者ユーザ
class AdminUser implements IUser {
  private static _instance: AdminUser;
  private constructor() {}

  public static get instance(): AdminUser {
    // instanceがなければ生成
    if(!this._instance) {
      console.log("Generate AdminUser...");
      this._instance = new AdminUser();
    }

    // 自身が持つインスタンスを返す
    return this._instance;
  }

  dispInfo = () => {
    console.log('Admin User');
  }
}

const user = new NormalUser("taro");
const admin = AdminUser.instance; // --> Generate AdminUser...

user.dispInfo(); // --> UserName is taro
admin.dispInfo(); // --> Admin User

// 同じインスタンスを呼び出しているため「Generate AdminUser...」が表示されない
const admin2 = AdminUser.instance;

まとめ

今回はTypescriptでデザインパターンの一つである Singleton Pattern について紹介しました。
途中にもあるようにシングルトンパターンは「インターフェースの実装」を考えなければ「ユーティリティクラス」として賄えてしまいます。
個人的には「インスタンスの唯一性を保ったまま、インターフェースを継承する」場合において有効なデザインパターンかなと思います。

ただ、このあたりは自分自身の理解が曖昧で、他にも「こういう理由で使った方がいい」とか「こういう場合もシングルトンが適している」等あるかと思います。
そういった場合はコメントで指摘いただけると幸いです。