唯一のオブジェクト / One of a king object

Singleton Pattern

インスタンスを 1 つしか必要としないオブジェクトが存在する.その場合,複数のインスタンスを生成するとかえって問題になることがある.

  • Thread pool
  • Cache
  • Dialogbox
  • Logger
  • etc.

Singleton Pattern はインスタンスが 1 度だけ生成されることを保証する仕組みである.

典型的にはコンストラクタを非公開,つまり private にすることで (外部から) 生成を禁ずる.

head_first_design_patterns/one_of_a_king_object/singleton.cpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#include <memory>

class Singleton {
 private:
  // 外部からのインスタンス化を禁ずる.
  Singleton() {}
  static Singleton* singleton_;

 public:
  static Singleton* GetInstance() {
    // 内部からであればコンストラクタが呼べる.
    if (!singleton_) singleton_ = new Singleton();
    return singleton_;
  }

  // コピーコンストラクタなどは delete で禁止しなければならない.
  Singleton(const Singleton& other) = delete;
  void operator=(const Singleton&) = delete;
};

Singleton* Singleton::singleton_ = nullptr;

int main() {
  // コンストラクタが private なので外部からインスタンス化できない.
  // auto singleton = new Singleton();

  // static なメンバ関数からのみインスタンス化できる.
  auto singleton = Singleton::GetInstance();
}

Singleton Pattern の定義

本書において Singleton Pattern を以下のように定義されている (P.177).

Singleton パターンは,クラスがインスタンスを 1 つしか持たないことを保証し,そのインスタンスにアクセスするグローバルポイントを提供する.

本文中では「グローバル」という言葉に対する定義はなされていないが,ここでいうグローバルは「あるプロセスの中」と解釈して差し支えないと思われる.しかし必ずしもプロセスである必要はないだろう.
// 極端なことをいえば Singleton Pattern の概念自体は現実世界に拡張できるだろう.

現実的には,そのオブジェクトの定義およびインスタンスにアクセスしうる範囲をグローバル呼ぶものと想像する.前者は生成の可否,後者は複製の可否に影響する.例えば C# において internal class Singleton {} のような形で定義されるとき,ここでいうグローバルはプロセスとは一致しない Singleton Pattern のクラスが定義されるだろう.

Thread-safe な実装

本文中でも指摘されているが GetInstance() が Thread-unsafe なため Multi-thread Programming の文脈ではインスタンスが単一であることを保証できない.

この問題に対する解決策を述べる.

  1. GetInsntance() への同時アクセスを許容しない
    • 言語によって実装は異なるが本書では Java を扱っているので synchronized 修飾子を例に挙げている
    • C++ では std::mutex で排他処理を行うことになるだろう
    • 同期処理はパフォーマンスへの影響が大きくなるため,頻繁に呼ばれるような処理ではなるべく避けたい
  2. 遅延インスタンス生成から先行インスタンス生成に変える
    • 書籍中では GetInstance() が呼ばれたタイミングでインスタンス化される (遅延インスタンス生成) が Singleton Pattern のメリットであるかのように述べられている (詳細については感想で述べる) が,実際は生成するタイミング自体は Singleton Pattern の関知する所ではない ([Singleton Pattern の定義](#Singleton Pattern の定義) を参照),
    • 端的にいえば GetInstance() を呼ばれたタイミングではなく (static 初期化子などで) クラスがロードされたタイミングで初期化してしまえばよい.
  3. ダブルチェックロッキングを利用し同期処理を減らす
    • GetInsntance() の中でもコンストラクタの呼び出し部分にだけ同期処理を行い,大抵のケースでロックを取らずに済ませる実装である
    • ここまでくると Singleton がどう,というよりは Multi-thread Programming におけるテクニックの話になってくるのでここでは割愛する

以下は C++ で先行インスタンス生成のスタイルに変更したものである.

head_first_design_patterns/one_of_a_king_object/singleton_static_initializer.cpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#include <iostream>
#include <memory>

class Singleton {
 private:
  // 外部からのインスタンス化を禁ずる.
  Singleton() {}
  static Singleton* singleton_;

 public:
  static Singleton* GetInstance() {
    // 起動時に初期化されているため null check が不要である.
    return singleton_;
  }

  // コピーコンストラクタなどは delete で禁止しなければならない.
  Singleton(const Singleton& other) = delete;
  void operator=(const Singleton&) = delete;
};

// null でなくインスタンスで初期化する.
Singleton* Singleton::singleton_ = new Singleton();

int main() {
  // コンストラクタが private なので外部からインスタンス化できない.
  // auto singleton = new Singleton();

  // static なメンバ関数からのみインスタンス化できる.
  auto singleton = Singleton::GetInstance();
}

Singleton Pattern の課題

Singleton Pattern と疎結合設計について以下のように記述されている (P.184).

疎結合原則は,「相互にやり取りするオブジェクト間は,疎結合設計を使用する」というものです.Singleton を変更する場合には,その Singleton に結びついたすべてのオブジェクトを変更しなければいけないので,Singleton ではこの原則に違反しやすいのです.

あるクラス A と クラス B が存在し,クラス B がクラス A を利用する場合,クラス B はクラス A に依存することになる.グローバルにアクセスされる Singleton クラスは様々なクラスから利用される.つまり依存されることになり,結合が密になる (疎結合原則に反する).

感想

正確さと分かりやすいさのトレードオフ

一般に Singleton の是非はだいぶ意見が割れているように思う.はたから見ている限り,仕組みが単純で導入しやすいため安易に使われてしまうが,他のパターンに比べてデメリットが明確であることが多いのがその所以のように思う.

この節 (おそらくこの書籍全体) を通して,正確さよりも分かりやすさを意識しているようで,しばしば (言いたいことはわかるが) 矛盾した記述が散見された.

例えば P.170 に以下の記載がある.

Singleton パターンでは,オブジェクトは必要になったときに初めて作成できるのだ.

一方で P.174 には以下の記載がある.

どの時点でもインスタンスが 1 つしか存在しないことを保証する,Singleton パターンに基づいています.

前者の「必要になるまでオブジェクトが作成 (インスタンス化の意と理解している) されない」に対して後者は「いついかなる時も (つまり必要か否かによらず) 1 つのインスタンスが存在する」と述べている.

この 2 文は (使用する言語にもよってくるのだが) 一般に矛盾していると解釈して差し支えないだろう.(私の理解では) Singleton の本旨は後者が正しい.しかし Singleton Pattern を実装した結果,前者の性質を持つことは往々にしてあり,それ自体は知っていたほうがよいだろう.

しかしこのくだりを細かく説明することが目的としているわけではない.このように情報を (言葉を選ばずに言えば) 騙し騙し混ぜ込んでいるように感じた.

遅延インスタンス化と先行インスタンス化の謎

「遅延インスタンス化と先行インスタンス化の問題がある」と述べられているが,結局これがどのような状況なのか説明が少ない.特に P.184 でグローバル変数と Singleton の違いについて以下のように説明している.

Javaでは,グローバル変数は基本的にはオブジェクトに対するスタティックな参照です.この方法でグローバル変数を使う場合,欠点がいくつかあります.1 つは既に説明したように,遅延インスタンス化と先行インスタンス化の問題があることです.

ここで説明されているのは,おそらく単にグローバル変数として定義するとそのグローバル変数を初期化するタイミングがコントロールできない1という話だと想像しはするが,想像の域をでない.

列挙体が Singleton の孕む諸問題を解決する謎

P.185 に以下の記述があるが,正直まるでわからない.Java の仕様だろうか.

これまで説明してきた問題の多く (同期に関する心配,クラスローディング問題,リフレクション,シリアライズ/デシリアライズ問題) は,列挙型を使って Singleton を作成すればすべて解決できます.


  1. 単に static な参照が遅延インスタンス化だとするなら P.181 の例にある static 初期化子による先行インスタンス生成というのと矛盾する.static 参照かどうかは遅延インスタンス化/先行インスタンス化と関係ない話だと考える. ↩︎