TypeScript | 関数・クラス・ジェネリクス:クラス設計 – Singletonパターン

TypeScript TypeScript
スポンサーリンク

ゴール:「アプリ全体で“そのクラスは1個だけ”をコードで保証する感覚をつかむ」

Singleton(シングルトン)パターンは一言でいうと、

「このクラスのインスタンスは、アプリ全体で“必ず1つだけ”にする設計パターン」

です。

「設定」「ログ」「接続管理」みたいな、“共有されるべきもの”に向いています。

ここでは TypeScript での書き方と、
なぜそう書くのか、どこが重要なのかを、初心者向けにかみ砕いていきます。


まずは「普通のクラス」との違いを押さえる

普通のクラスは、いくらでも new できる

まず、普通のクラスから。

class Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

const logger1 = new Logger();
const logger2 = new Logger();

logger1.log("hello");
logger2.log("world");

console.log(logger1 === logger2); // false(別インスタンス)
TypeScript

この状態だと、Logger はいくらでも new できます。

「それの何が悪いの?」と思うかもしれませんが、
例えばこんなケースを想像してみてください。

  • ログの出力先を変えたい
  • 設定を変えたい
  • 接続情報を変えたい

それを「アプリ全体で一貫させたい」のに、
あちこちでバラバラに new Logger() されていたら、
どのインスタンスがどの設定を持っているのか分からなくなります。

Singleton は「インスタンスを1つに固定する」ための仕組み

そこで出てくるのが Singleton です。

「このクラスは、アプリ全体で1個だけ。
みんな、その1個を共有して使ってね」

というルールを、コードで表現します。


TypeScript での基本的な Singleton 実装

private constructor と static プロパティを組み合わせる

TypeScript で典型的な Singleton は、こう書きます。

class Logger {
  private static instance: Logger | null = null;

  private constructor() {
    // 外から new させない
  }

  static getInstance(): Logger {
    if (this.instance === null) {
      this.instance = new Logger();
    }
    return this.instance;
  }

  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}
TypeScript

使う側はこうなります。

const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

logger1.log("hello");
logger2.log("world");

console.log(logger1 === logger2); // true(同じインスタンス)
TypeScript

ここでやっていることを、丁寧に分解してみます。

重要ポイント1:コンストラクタを private にする

private constructor() {}
TypeScript

これがめちゃくちゃ重要です。

コンストラクタを private にすると、
クラスの外から new Logger() ができなくなります。

// const l = new Logger(); // エラー:コンストラクタが private
TypeScript

つまり、

「Logger のインスタンスを作れるのは、このクラス自身だけ」

という状態になります。

Singleton では、

「勝手に new されると困る」
「インスタンスの数をクラス側で管理したい」

ので、この private constructor が鍵になります。

重要ポイント2:static な「唯一のインスタンス」を持つ

private static instance: Logger | null = null;
TypeScript

ここで、クラス自身が「唯一のインスタンス」を保持するための
static プロパティを持っています。

static なので、インスタンスではなく「クラスに紐づく変数」です。

  • Logger.instance はクラス全体で1つだけ
  • ここに「唯一の Logger インスタンス」を入れておく

という役割です。

重要ポイント3:getInstance で「1回だけ new して、あとは使い回す」

static getInstance(): Logger {
  if (this.instance === null) {
    this.instance = new Logger();
  }
  return this.instance;
}
TypeScript

このメソッドが、Singleton の心臓部です。

やっていることはシンプルで、

1回目の呼び出し
instancenull なので new Logger() して保存し、それを返す

2回目以降の呼び出し
→ すでに instance に入っているインスタンスをそのまま返す

という動きです。

これにより、

const a = Logger.getInstance();
const b = Logger.getInstance();

console.log(a === b); // 常に true
TypeScript

が保証されます。

「インスタンスを作るのは最初の1回だけ。
あとはずっと同じものを返す」

これが Singleton のコアアイデアです。


どんなときに Singleton を使うのか

例1:アプリ全体で共有したい設定(Config)

アプリの設定を表すクラスは、
典型的な Singleton 候補です。

class AppConfig {
  private static instance: AppConfig | null = null;

  private constructor(
    public readonly apiEndpoint: string,
    public readonly timeoutMs: number
  ) {}

  static getInstance(): AppConfig {
    if (this.instance === null) {
      this.instance = new AppConfig("https://api.example.com", 5000);
    }
    return this.instance;
  }
}
TypeScript

使う側は、どこからでもこう書けます。

const config = AppConfig.getInstance();
console.log(config.apiEndpoint);
TypeScript

どのファイルから呼んでも、
同じ AppConfig インスタンスが返ってきます。

「設定はアプリ全体で1つだけ」
という前提を、コードで保証できています。

例2:ログ出力(Logger)

さっきの Logger も、典型的な例です。

class Logger {
  private static instance: Logger | null = null;

  private constructor() {}

  static getInstance(): Logger {
    if (this.instance === null) {
      this.instance = new Logger();
    }
    return this.instance;
  }

  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}
TypeScript

アプリのあちこちで、

Logger.getInstance().log("何か起きた");
TypeScript

と書いても、
使っているのは常に同じ Logger です。

将来、「ログの出力先をファイルに変えたい」などの変更があっても、
Logger クラスの中だけを直せば済みます。


Singleton の「良いところ」と「注意点」

良いところ:共有状態を一元管理できる

Singleton の一番のメリットは、

「共有されるべき状態を、1つの場所で管理できる」

ことです。

  • 設定
  • ログ
  • 接続プール
  • キャッシュ

など、「アプリ全体で1つでいいもの」は、
Singleton にすると扱いやすくなります。

どこからでも同じインスタンスにアクセスできるので、
「どのインスタンスが本物だっけ?」と迷わなくて済みます。

注意点1:「どこからでも触れる」は諸刃の剣

ただし、
「どこからでも同じインスタンスにアクセスできる」というのは、
裏を返すと、

「どこからでも状態をいじれてしまう」

ということでもあります。

  • いろんな場所から設定を書き換える
  • いろんな場所から同じオブジェクトを mutate する

といったことをやり始めると、
バグの原因になりやすいです。

なので、Singleton にするクラスは、

  • できるだけ「読み取り専用」にする(readonly を活用する)
  • 状態をむやみに書き換えさせない API にする

といった工夫が大事になります。

注意点2:テストがしづらくなることもある

Singleton は「グローバルな共有状態」に近いので、
テストのときに扱いづらくなることがあります。

例えば、

  • あるテストで Singleton の状態を変える
  • 別のテストにもその影響が残ってしまう

といったことが起きがちです。

これを避けるには、

  • Singleton を直接使うのではなく、interface 経由で注入する
  • テスト時には差し替え可能な設計にする

など、もう一段階進んだ設計が必要になります。

初心者のうちは、

「Singleton は便利だけど、乱用すると“なんでもグローバル変数”みたいになる」

という感覚だけ持っておけば十分です。


Singleton を自分の言葉で説明できるようにする

最後に、あなた自身の言葉でこう整理してみてください。

Singleton パターンとは、

  • 「このクラスのインスタンスはアプリ全体で1つだけ」にする設計パターン
  • TypeScript では、private constructorstatic instancestatic getInstance() で実現する
  • 1回だけ new して、あとは同じインスタンスを返し続ける
  • 設定・ログ・接続管理など、「共有されるべきもの」に向いている
  • ただし、共有状態が増えすぎるとバグの温床にもなるので、使いどころは選ぶ

今のあなたのコードの中で、

「これ、本当はアプリ全体で1個だけでいいよな」と感じるものがあれば、
それを Singleton にしてみると、設計の感覚が一気にクリアになります。

逆に、
「別に何個あっても困らないもの」まで Singleton にし始めたら、
それはやりすぎのサインです。

そのバランスを考えながら、
1つだけ、自分のプロジェクトの中で Singleton 候補を探してみてください。

タイトルとURLをコピーしました