LESSON 30分

ストーリー

高橋アーキテクト
コンストラクタの引数が10個もあるクラスを見たことはあるかい?
あなた
あります…。どの引数が何なのか、毎回ドキュメントを見ないとわかりません
高橋アーキテクト
Builderパターンを使えば、そのストレスから解放される。それから、設定管理やログ管理で”このクラスのインスタンスは1つだけ”を保証したい場面もあるだろう。それがSingletonだ

Builder パターン

目的

複雑なオブジェクトの構築を段階的に行い、同じ構築過程で異なる表現を生成できるようにする。

パターンなしの問題

// 問題:引数が多すぎるコンストラクタ
const query = new DatabaseQuery(
  'users',        // テーブル名
  ['name', 'email'], // カラム
  'age > 18',     // WHERE条件
  'name',         // ORDER BY
  'ASC',          // 方向
  10,             // LIMIT
  0,              // OFFSET
  true,           // DISTINCT?
  'department',   // GROUP BY
  'COUNT(*) > 5'  // HAVING
);
// どの引数が何を意味するのか分からない

Builder の実装

class DatabaseQuery {
  readonly table: string;
  readonly columns: string[];
  readonly where?: string;
  readonly orderBy?: string;
  readonly orderDirection: 'ASC' | 'DESC';
  readonly limit?: number;
  readonly offset?: number;
  readonly distinct: boolean;
  readonly groupBy?: string;
  readonly having?: string;

  private constructor(builder: QueryBuilder) {
    this.table = builder.table;
    this.columns = builder.columns;
    this.where = builder.whereClause;
    this.orderBy = builder.orderByColumn;
    this.orderDirection = builder.orderDir;
    this.limit = builder.limitCount;
    this.offset = builder.offsetCount;
    this.distinct = builder.isDistinct;
    this.groupBy = builder.groupByColumn;
    this.having = builder.havingClause;
  }

  static builder(table: string): QueryBuilder {
    return new QueryBuilder(table);
  }
}

class QueryBuilder {
  columns: string[] = ['*'];
  whereClause?: string;
  orderByColumn?: string;
  orderDir: 'ASC' | 'DESC' = 'ASC';
  limitCount?: number;
  offsetCount?: number;
  isDistinct = false;
  groupByColumn?: string;
  havingClause?: string;

  constructor(readonly table: string) {}

  select(...cols: string[]): QueryBuilder {
    this.columns = cols;
    return this;
  }

  where(condition: string): QueryBuilder {
    this.whereClause = condition;
    return this;
  }

  orderBy(column: string, direction: 'ASC' | 'DESC' = 'ASC'): QueryBuilder {
    this.orderByColumn = column;
    this.orderDir = direction;
    return this;
  }

  limit(count: number): QueryBuilder {
    this.limitCount = count;
    return this;
  }

  offset(count: number): QueryBuilder {
    this.offsetCount = count;
    return this;
  }

  distinct(): QueryBuilder {
    this.isDistinct = true;
    return this;
  }

  groupBy(column: string): QueryBuilder {
    this.groupByColumn = column;
    return this;
  }

  having(condition: string): QueryBuilder {
    this.havingClause = condition;
    return this;
  }

  build(): DatabaseQuery {
    return new (DatabaseQuery as any)(this);
  }
}

// 使い方:読みやすく、必要なものだけ指定
const query = DatabaseQuery
  .builder('users')
  .select('name', 'email')
  .where('age > 18')
  .orderBy('name', 'ASC')
  .limit(10)
  .build();

Builder の利点

利点説明
可読性メソッド名が引数の意味を説明する
柔軟性必要なプロパティだけ設定できる
不変性build() 後のオブジェクトは不変にできる
バリデーションbuild() 時にまとめて検証できる

Singleton パターン

目的

クラスのインスタンスが1つだけであることを保証し、そのインスタンスへのグローバルなアクセスポイントを提供する。

TypeScript での実装

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

  private constructor(
    readonly databaseUrl: string,
    readonly apiKey: string,
    readonly logLevel: string
  ) {}

  static initialize(dbUrl: string, apiKey: string, logLevel: string): AppConfig {
    if (AppConfig.instance) {
      throw new Error('AppConfig is already initialized');
    }
    AppConfig.instance = new AppConfig(dbUrl, apiKey, logLevel);
    return AppConfig.instance;
  }

  static getInstance(): AppConfig {
    if (!AppConfig.instance) {
      throw new Error('AppConfig has not been initialized');
    }
    return AppConfig.instance;
  }

  // テスト用リセット
  static resetForTesting(): void {
    AppConfig.instance = null;
  }
}

// 使い方
// アプリケーション起動時に1回だけ初期化
AppConfig.initialize('postgres://localhost:5432/mydb', 'key-123', 'info');

// どこからでもアクセス
const config = AppConfig.getInstance();
console.log(config.databaseUrl);

Singleton の注意点

高橋アーキテクトのアドバイス:

「Singletonは最も有名なパターンだが、最も乱用されるパターンでもある。グローバル状態を隠蔽するため、テストが困難になり、依存関係が見えにくくなる。使う前に本当に必要か考えてほしい」

使っていい場面避けるべき場面
アプリケーション設定ビジネスロジック
ログマネージャーデータアクセス
コネクションプールテストしたいクラス

Singleton の代替:DI(依存性注入)

// Singleton の代わりに DI コンテナでスコープ管理
class AppConfig {
  constructor(
    readonly databaseUrl: string,
    readonly apiKey: string,
    readonly logLevel: string
  ) {}
}

// DI コンテナが「1つだけ」を保証する
// container.registerSingleton(AppConfig, new AppConfig(...));

class UserService {
  constructor(private config: AppConfig) {} // 注入される
}

DIを使えば、Singletonパターンを使わずにインスタンスの一意性を保証でき、テスト時にモックへの差し替えも容易です。


Builder + Singleton の実践例

// 設定オブジェクトを Builder で構築し、Singleton として管理
class AppConfig {
  private static instance: AppConfig | null = null;

  private constructor(
    readonly port: number,
    readonly dbUrl: string,
    readonly logLevel: string,
    readonly corsOrigins: string[]
  ) {}

  static builder(): AppConfigBuilder {
    return new AppConfigBuilder();
  }

  static getInstance(): AppConfig {
    if (!this.instance) throw new Error('Not initialized');
    return this.instance;
  }

  static setInstance(config: AppConfig): void {
    this.instance = config;
  }
}

class AppConfigBuilder {
  private port = 3000;
  private dbUrl = 'localhost';
  private logLevel = 'info';
  private corsOrigins: string[] = [];

  withPort(port: number): this { this.port = port; return this; }
  withDbUrl(url: string): this { this.dbUrl = url; return this; }
  withLogLevel(level: string): this { this.logLevel = level; return this; }
  withCorsOrigins(origins: string[]): this { this.corsOrigins = origins; return this; }

  build(): AppConfig {
    const config = new (AppConfig as any)(this.port, this.dbUrl, this.logLevel, this.corsOrigins);
    AppConfig.setInstance(config);
    return config;
  }
}

// 起動時
AppConfig.builder()
  .withPort(8080)
  .withDbUrl('postgres://prod:5432/app')
  .withLogLevel('warn')
  .withCorsOrigins(['https://example.com'])
  .build();

まとめ

ポイント内容
Builder複雑なオブジェクトを段階的に構築する
Singletonインスタンスが1つだけであることを保証する
Builder の利点可読性、柔軟性、不変性の確保
Singleton の注意テスト困難になるため、DI での代替を検討

チェックリスト

  • Builder パターンを使って可読性の高いオブジェクト生成ができる
  • Singleton パターンの利点と欠点を説明できる
  • Singleton の代替としてDIを検討できる

次のステップへ

次は生成パターンの最後、Prototype パターンを学びます。既存のオブジェクトをコピーして効率的に新しいオブジェクトを生成する方法です。


推定読了時間: 30分