Singleton - Design pattern on Laravel

Laravel におけるデザインパターン入門 #1。シングルトンパターンについて解説していきます。

Laravel におけるデザインパターン - Singleton

Laravel におけるデザインパターン入門 #1は Singleton パターンについて解説していきます。

シングルトンパターン

シングルトンパターンは、クラスの破棄・生成に関するデザインパターンです。

通常 クラスは New 演算子を用いることで自由に作成することができます。 例えば 送料の計算を行う PostageService クラスの生成は以下のようになります。

<?php
// ...
$service = new PostageService($options);

PostageService クラスは引数で送料計算に関する設定を受け取る事ができます。 この送料サービスを利用するメソドは以下のような形になるでしょう。

必要に応じて、 new 演算子をコールしてクラスを利用するといった方法は、 一般的ではあるものの、一方で以下のような問題が発生します。

  • アプリケーション内の複数箇所でのクラス利用が同じ初期オプションで利用されているかの保証が困難
  • クラス内のコンストラクタで重たい処理が行われている場合,同一の内容が new する度に実行される。

「重たい処理」 というのは、例えば 送料テーブルなどを先に取得するためのDBへのアクセスや、 外部システムに配送状況を先に問い合わせておくようなAPI アクセスなど、 クラス利用に必要な初期化処理を意味します。

シングルトンパターンは、 「クラスに対してインスタンスが一つしか生成されないことを保証」することで、 これらの問題を解決しようとするものです。

以下は簡単な シングルトン実装の例です。

<?php 
class PostageService{
  
  protected $instance;
  
  static public function getInstance(){
    if(!static::$instance){
      $options = [
        // ...
      ];
      static::$instance = new static($options);
    }
    return static::$instance;
  }
  
  public function __construct($options) {
    // ...
  }
}

クラスを利用する際には、newするのではなく、 PostageService::getInstance()をコールしてオブジェクトの生成・取得を行います。

クラスインスタンスは、PostageService::getInstance()が初めてコールされたタイミングで生成され、 移行は、内部で生成されたインスタンスのキャッシュを常に返すようになります。

厳密には 直接の new 呼び出しを禁止するように __construct を private にしたり オブジェクトの複製を行う __clone メソドを無効化したりする必要があります。

シングルトンを実装することで、クラスインスタンスの多様性は完全に失われますが オブジェクトの生成に関する管理はアプリケーション全体で唯一つ、を保証することが出来るようになります。

Laravel におけるシングルトン

Laravel においては シングルトンは DI コンテナベースで実現することができます。

Laravel の app() 関数でアクセス可能なDI コンテナには シングルトンの仕組みが用意されており、 アプリケーション全体で共有したいクラスは、ここに singleton 関数をつかって格納することができます。

app()->singleton("hoge",function(){
  $options = [
    ....
  ];
  return new PostageService($options);
});

singleton関数の第一引数は コンテナ登録時の名前です。 この名前をつかって、クラスを利用する側では以下のようにしてインスタンスを取り出します。

$postageService = app("hoge")

PostageService の生成は、 登録された名前でのコンテナ呼び出し app("hoge") が初めて行われた際に行われ、 以後の コンテナ呼び出しでは、生成済みのクラスインスタンスが返却されます。

以前 クラスの new 自体は 任意に実行可能となっていますが、 アプリケーション内での管理を行いたい程度のニーズであれば、これでも十分運用でカバー可能です。

クラスのコード内に シングルトンのための実装を入れずにすむため、 テスト内では 非シングルトンとして、アプリケーションではシングルトンとして、といった 利用の切り分けも可能になります。

通常はコンテナ登録時に、

app()->singleton(PostageService::class,function(){
  $options = [
    ....
  ];
  return new PostageService($options);
});

のようにしてクラス名で名前付けを行います。 これにより利用時にも、

$postageService = app(PostageService::class)

という形でクラス名を利用したコールが可能になるほか、DI ベースでの呼び出しも可能になります。

一般的に app()->singleton( ... ) を用いたコンテナへの登録は ServiceProvider を用いて行うのが良いでしょう。

シングルトンの運用と管理

シングルトンは便利な半面、単純なグローバル変数です。

コストのかかるインスタンスの処理を共通化することはパフォーマンスにも大きなメリットを与えますが、 シングルトン部分が肥大化することでコードの管理は煩雑になります。

原則 コンストラクタの実行コストは少なくするのが原則で、 例えば前述のように、 PostageService の生成に、 送料テーブルなどを先に取得するためのDBへのアクセスや、 外部システムに配送状況を先に問い合わせておくようなAPI アクセスが必要な場合、 それ専用のクラスをシングルトンとして用意して、PostageService と分離するのが良いでしょう。

シングルトンのメリット・デメリット

シングルトンでメリット・デメリットが発生するケースは 多くが、クラスの中に状態を持っているようなコードです。

前述のようにクラスが生成される際に、

  • 送料テーブルなどを先に取得するためのDBアクセスを実行
  • 外部システムに配送状況を先に問い合わせておくようなAPI アクセスを実行

といった処理は、クラス内のプロパティとして結果が保持され、 クラス内の状態として保持されます。

こういった「状態」を共有出来るのはシングルトンのメリットで有る一方で、 クラス内の挙動が「状態」に依存するのはコード保守の観点で大きなデメリットとなります。

シングルトンとは


投稿一覧へ