Javaを鍛える|デザインパターン全23回 #2

Singleton:システム全体で共有する設定・ロガーを1つに保つ

  • Java
  • デザインパターン
  • Singleton
  • Javaを鍛える

📋 この記事の目次
  1. Singletonは何のためにあるか
  2. Before:newするたびに別インスタンスができてしまう
  3. After:コンストラクタを非公開にし、共有インスタンスを1つだけ持つ
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftには、既定の担当者名など、システム全体で使う設定を保持するSystemConfigクラスがあります。ところが、このクラスは使う場所ごとにnew SystemConfig()されており、設定ファイルの読み込み処理が何度も実行されてしまっています。

public class SystemConfig {
    private final String defaultAssignee;

    public SystemConfig() {
        System.out.println("[SystemConfig] 設定ファイルを読み込みました");
        this.defaultAssignee = "unassigned";
    }

    public String getDefaultAssignee() {
        return defaultAssignee;
    }
}

Singletonは何のためにあるか

Singletonは、「あるクラスのインスタンスが、システム全体でただ1つしか存在しないことを保証する」 パターンです。会社全体で共有すべき掲示板が部署ごとにバラバラに用意されてしまったら、部署によって見ている情報が食い違ってしまいます。Singletonは、この「共有すべきものは1つに保つ」という考え方をコードで保証する仕組みです。

Before:newするたびに別インスタンスができてしまう

public class Main {
    public static void main(String[] args) {
        SystemConfig config1 = new SystemConfig();
        SystemConfig config2 = new SystemConfig();
        System.out.println("config1 == config2: " + (config1 == config2));
        System.out.println("既定の担当者: " + config1.getDefaultAssignee());
    }
}

実行結果:

[SystemConfig] 設定ファイルを読み込みました
[SystemConfig] 設定ファイルを読み込みました
config1 == config2: false
既定の担当者: unassigned

「設定ファイルを読み込みました」が2回出力され、config1config2は別のインスタンスになっています(==の結果がfalse)。設定を実行時に変更する機能を将来追加した場合、config1側だけ変更されてconfig2側には反映されない、という食い違いが起きる土台がすでに出来上がっています。

After:コンストラクタを非公開にし、共有インスタンスを1つだけ持つ

public class SystemConfig {
    private static final SystemConfig INSTANCE = new SystemConfig();

    private final String defaultAssignee;

    private SystemConfig() {
        System.out.println("[SystemConfig] 設定ファイルを読み込みました");
        this.defaultAssignee = "unassigned";
    }

    public static SystemConfig getInstance() {
        return INSTANCE;
    }

    public String getDefaultAssignee() {
        return defaultAssignee;
    }
}
public class Main {
    public static void main(String[] args) {
        SystemConfig config1 = SystemConfig.getInstance();
        SystemConfig config2 = SystemConfig.getInstance();
        System.out.println("config1 == config2: " + (config1 == config2));
        System.out.println("既定の担当者: " + config1.getDefaultAssignee());
    }
}

実行結果:

[SystemConfig] 設定ファイルを読み込みました
config1 == config2: true
既定の担当者: unassigned

コンストラクタをprivateにして外部からnewできないようにし、唯一のインスタンスをstatic finalフィールドとしてクラス自身が保持しています。getInstance()を通してしか取得できないため、「設定ファイルを読み込みました」は1回しか出力されず、config1config2は完全に同じインスタンスになります(==の結果がtrue)。

🧓 ベテランの現場コラム Singletonは書き方自体は簡単ですが、実務では「本当にシステム全体で1つであるべきものか」を見極めることの方が重要です。テスト用に差し替えたいオブジェクトや、将来的に複数の設定セット(環境ごとの設定など)を扱う可能性があるものにまでSingletonを使ってしまうと、後から柔軟性を失って苦労することがあります。設計編で学んだDI(依存性注入)と組み合わせ、「Springのコンテナが1つだけBeanを管理する」という形にする方が、実務では見通しが良くなる場合も多いです。

こんな場面で使う/使わない

  • 使う: システム全体の設定、ログ出力先、共有キャッシュなど、「複数存在すると矛盾が起きる」ものを扱う場合
  • 使わない: テストのたびに差し替えたいオブジェクトや、将来複数のインスタンスを使い分ける可能性があるもの。DIコンテナに管理を任せる設計の方が適していることが多い

まとめ

  • Singletonは「システム全体でインスタンスを1つに保証する」パターン
  • コンストラクタをprivateにし、staticな唯一のインスタンスをgetInstance()で提供する
  • 何でもSingletonにせず、「複数存在すると矛盾が起きるものか」を見極めて適用する

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。