ストーリー
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分