Javaにおける設計パターン入門:シングルトンパターン

序文

プログラミングを始めたばかりの頃から「設計パターン」という言葉は耳にしていたが、当時はまったくの初心者だったため、深く関わることはなかった。仕事で基本的な業務コードに慣れてきた頃になってようやく「設計パターン」を学び始めた。最初に学んだのは「ファクトリーパターン」であったが、今回は「シングルトンパターン」について述べる。なぜこのパターンから始めるのか?それは「シングルトンパターン」が最も簡単なパターンだからである。順序があるものであり、難易度の低いものから始めることにした。以上、余談はこれくらいにして、本題に入ろう。

設計パターンの概要

ここでの概要は本当に「概要」である。

何が設計パターンか

設計パターンとは、繰り返し使用され、多くの人が知り、分類されたコード設計の経験則である。

なぜ設計パターンを使うのか

設計パターンを使用することで、コードの再利用性が向上し、他の人の理解を助け、コードの信頼性を保証できる。

設計パターンの種類

設計パターンには合計23のパターンがあり、主な分類としては以下の3つに分けられる:

1. 作成に関するパターン

これらのパターンは、オブジェクトの作成時にロジックを隠す方法を提供する。new演算子による直接的なインスタンス化ではなく、より柔軟なオブジェクト生成を行う。

  • シングルトンパターン
  • ファクトリーパターン
  • アブストラクトファクトリーパターン
  • ビルダーパターン
  • プロトタイプパターン

2. 構造に関するパターン

これらのパターンは、クラスとオブジェクトの組み合わせに焦点を当てている。継承を用いてインターフェースを組み合わせたり、新しい機能を持つコンポジットオブジェクトを定義したりする。

  • アダプターパターン
  • ブリッジパターン
  • フィルターパターン
  • コンポジットパターン
  • デコレーターパターン
  • ファサードパターン
  • ファイアーウォールパターン
  • プロキシパターン

3. 行動に関するパターン

これらのパターンは、オブジェクト間の通信に注目している。

  • チェーン・オブ・レスポンシビリティパターン
  • コマンドパターン
  • インタープリターパターン
  • イテレーターパターン
  • メディエーターパターン
  • メモントパターン
  • オブザーバーパターン
  • ステートパターン
  • オブジェクトなしパターン
  • ストラテジーパターン
  • テンプレートパターン
  • アクセッサー・パターン

設計パターンの原則

設計パターンの6つの原則

  1. 開閉原則:拡張には開き、修正には閉じる。
  2. リスコフ置換原則:開閉原則の補足。ベースクラスが使用できる場所では、サブクラスも常に使用可能である。LSPは継承再利用の基盤であり、派生クラスがベースクラスに置き換え可能で、ソフトウェア単位の機能に影響を与えない場合にのみ、ベースクラスを再利用できる。
  3. 依存逆転原則:インターフェースに基づいてプログラミングし、具象ではなく抽象に依存する。
  4. インターフェース分離原則:可能な限り複数の分離されたインターフェースを使用し、クラス間の結合度を下げる。
  5. ディミトリの法則:エンティティは他のエンティティとの相互作用をできるだけ少なくし、システムの機能モジュールを独立させることを目的とする。
  6. コンポジット・リユース原則:継承よりも合成/集約を使用することを推奨する。

シングルトンパターン

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

特定のクラスがシステム内で一つしかインスタンスを持たないことを保証し、外部からのアクセスを容易にするパターンである。例えばWindowsのタスクマネージャーはシングルトンの一例である。

シングルトンパターンの使用場面

よく使われる例として「データベース接続プール」、「スレッドプール」、「ログオブジェクト」などが挙げられる。

シングルトンパターンの実装方法

初めてシングルトンパターンを学ぶ際、一般的に学ぶのは「饿汉式」と「饱汉式(遅延初期化)」である。それぞれの実装を見てみよう。

饿汉式
プライベートなコンストラクタを定義し、そのクラスのインスタンスをprivate static final変数として保持し、公開メソッドから取得する。

class SingletonExample1 {
    private SingletonExample1() {}

    private static final SingletonExample1 INSTANCE = new SingletonExample1();

    public static SingletonExample1 getInstance() {
        return INSTANCE;
    }
}

饱汉式
プライベートなコンストラクタを定義し、静的プライベート変数を宣言し、公開メソッドでnullチェックを行い、必要に応じてインスタンスを作成する。

class SingletonExample2 {
    private SingletonExample2() {}

    private static SingletonExample2 instance;

    public static SingletonExample2 getInstance() {
        if (instance == null) {
            instance = new SingletonExample2();
        }
        return instance;
    }
}

この2つの方式の利点と欠点について見ていこう。

饿汉式

  • 利点:実装がシンプルで、synchronizedキーワードを使わなくてもスレッドセーフである。
  • 欠点:クラスロード時にインスタンスが作成され、メモリを常時占有する。

饱汉式

  • 利点:メモリ節約ができる。初回アクセス時にのみ初期化される。
  • 欠点:スレッドセーフではない。複数スレッドによる呼び出しで複数のインスタンスが作られる可能性がある。
  • 総合評価:実装が簡単だがスレッドセーフではない。効率はそこそこ。

飽漢式でもsynchronizedキーワードを追加すればスレッドセーフになるが、パフォーマンスの観点からは最適ではない。

JDK1.5以前に最も良いとされる2つの実装方法としては、「静的内部クラス」および「二重ロックチェック」がある。

静的内部クラス
プライベートコンストラクタを定義し、静的内部クラスでインスタンスを保持し、外部からアクセスするためのpublic staticメソッドを用意する。

class SingletonExample4 {
    private SingletonExample4() {}

    private static class SingletonHolder {
        private static final SingletonExample4 INSTANCE = new SingletonExample4();
    }

    public static final SingletonExample4 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

内部クラスはprivateであり、外部からはgetInstance()メソッド以外にアクセスできない。遅延初期化かつスレッドセーフであり、JDKバージョンに依存しない。

二重ロックチェック
volatile修飾子付きのstatic private変数を定義し、初期化チェックを行う。最初のnullチェックで効率を上げ、synchronizedブロックでスレッドセーフを確保。二度目のnullチェックで再初期化を防ぐ。

class SingletonExample6 {
    private SingletonExample6() {}

    private static volatile SingletonExample6 instance;

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

この方法は長期間、最も効率的かつスレッドセーフな実装として知られていたが、実装が煩雑で初心者にはやや難しい。

JDK1.5以降では列挙型が導入され、シングルトンパターンの実装が非常に簡単になった。スレッドセーフで、パフォーマンスも高い。最も重要なのは、実装が非常にシンプルである点である。

列挙型シングルトン

enum SingletonExample7 {
    INSTANCE;
}

はい、これだけのコードで十分である。他に何も必要ない。
列挙型はJDK1.5以降でのみ使用可能で、自動的にシリアライズ機構が提供され、複数回のインスタンス化を防ぐ。複雑なシリアライズやリフレクション攻撃にも耐える。これはEffective Javaの著者Josh Blochが推奨する方法である。

まとめ

シングルトンパターンの実装方法を紹介したが、以下のような注意点がある(列挙型を除く)。

  1. コンストラクタをprivateにすること;
  2. private staticなインスタンス変数を定義すること;
  3. public staticなメソッドでインスタンスを返却すること;

タグ: Java シングルトンパターン 設計パターン スレッドセーフ 列挙型

7月19日 19:07 投稿