𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
OpenAI Codex CLI × デザインパターン実装|GoF・クリーンアーキテクチャの自動生成ガイド

OpenAI Codex CLI × デザインパターン実装|GoF・クリーンアーキテクチャの自動生成ガイド

OpenAI Codex CLIデザインパターンクリーンアーキテクチャTypeScript設計
目次

「デザインパターンは知っているけど、実務でどう使えばいいかわからない」——そんな悩みをOpenAI Codex CLIが解決します。

Codex CLIにデザインパターンの適用を指示するだけで、プロジェクトのコンテキストに合わせたパターン実装を自動生成できます。 本記事では、SES現場で実際に使える主要デザインパターンのCodex CLIによる実装方法を解説します。

この記事を3秒でまとめると

  • Codex CLIでGoFデザインパターンをプロジェクトに合わせて自動実装
  • Factory、Strategy、Observer、Decoratorなど実務で頻出する8パターンをカバー
  • クリーンアーキテクチャの層構造もCodex CLIで一括スキャフォールド可能

デザインパターンとCodex CLIの組み合わせが強い理由

デザインパターンの最大の課題は「適用判断」と「具体的な実装」のギャップです。パターンの概念を知っていても、目の前のコードにどう適用すればいいかわからない——SESエンジニアの多くが経験する壁です。

Codex CLIが解決する3つの課題

  1. パターン選択: コードの構造を分析して、最適なパターンを提案
  2. 具体的な実装: プロジェクトの言語・フレームワークに合わせた実装を生成
  3. 既存コードへの適用: リファクタリング対象のコードにパターンを段階的に適用

デザインパターン適用フロー

生成パターン(Creational Patterns)

Factory Method パターン

SES案件でよくある「複数のデータソースに対応するシステム」にFactory Methodを適用します。

Codex CLIへのプロンプト:

以下の要件でFactory Methodパターンを実装してください:
- データソース: PostgreSQL、MongoDB、Redis
- 各データソースにはconnect()、query()、disconnect()メソッドが必要
- 環境変数でデータソースを切り替え可能にする
- TypeScriptで型安全に実装する

生成されるコード:

// データソースのインターフェース
interface DataSource {
  connect(): Promise<void>;
  query<T>(operation: string, params?: Record<string, unknown>): Promise<T>;
  disconnect(): Promise<void>;
  isConnected(): boolean;
}

// 抽象Factoryクラス
abstract class DataSourceFactory {
  abstract createDataSource(config: DataSourceConfig): DataSource;

  // テンプレートメソッド:接続付きのデータソース生成
  async createConnected(config: DataSourceConfig): Promise<DataSource> {
    const source = this.createDataSource(config);
    await source.connect();
    return source;
  }
}

// PostgreSQL実装
class PostgresDataSource implements DataSource {
  private pool: Pool | null = null;

  constructor(private config: PostgresConfig) {}

  async connect(): Promise<void> {
    this.pool = new Pool({
      host: this.config.host,
      port: this.config.port,
      database: this.config.database,
      user: this.config.user,
      password: this.config.password,
      max: this.config.maxConnections || 10,
    });
    await this.pool.query('SELECT 1');
  }

  async query<T>(sql: string, params?: Record<string, unknown>): Promise<T> {
    if (!this.pool) throw new Error('Not connected');
    const result = await this.pool.query(sql, Object.values(params || {}));
    return result.rows as T;
  }

  async disconnect(): Promise<void> {
    await this.pool?.end();
    this.pool = null;
  }

  isConnected(): boolean {
    return this.pool !== null;
  }
}

// MongoDB実装
class MongoDataSource implements DataSource {
  private client: MongoClient | null = null;
  private db: Db | null = null;

  constructor(private config: MongoConfig) {}

  async connect(): Promise<void> {
    this.client = new MongoClient(this.config.uri);
    await this.client.connect();
    this.db = this.client.db(this.config.database);
  }

  async query<T>(collection: string, params?: Record<string, unknown>): Promise<T> {
    if (!this.db) throw new Error('Not connected');
    const result = await this.db.collection(collection).find(params || {}).toArray();
    return result as T;
  }

  async disconnect(): Promise<void> {
    await this.client?.close();
    this.client = null;
    this.db = null;
  }

  isConnected(): boolean {
    return this.client !== null;
  }
}

// Factory実装
class ConcreteDataSourceFactory extends DataSourceFactory {
  createDataSource(config: DataSourceConfig): DataSource {
    switch (config.type) {
      case 'postgres':
        return new PostgresDataSource(config as PostgresConfig);
      case 'mongodb':
        return new MongoDataSource(config as MongoConfig);
      case 'redis':
        return new RedisDataSource(config as RedisConfig);
      default:
        throw new Error(`Unsupported data source type: ${config.type}`);
    }
  }
}

// 使用例
const factory = new ConcreteDataSourceFactory();
const dataSource = await factory.createConnected({
  type: process.env.DB_TYPE as 'postgres' | 'mongodb' | 'redis',
  host: process.env.DB_HOST!,
  // ...
});

Abstract Factory パターン

UIコンポーネントのテーマ切り替えなど、関連するオブジェクト群を一括生成する場合に使います。

// テーマファクトリー
interface UIFactory {
  createButton(label: string): Button;
  createInput(placeholder: string): Input;
  createCard(title: string, content: string): Card;
  createModal(title: string): Modal;
}

class LightThemeFactory implements UIFactory {
  createButton(label: string): Button {
    return new LightButton(label, {
      bg: '#ffffff', color: '#1a1a2e', border: '#e5e7eb',
      hoverBg: '#f3f4f6',
    });
  }
  createInput(placeholder: string): Input {
    return new LightInput(placeholder, {
      bg: '#ffffff', border: '#d1d5db', focusBorder: '#4285f4',
    });
  }
  // ...
}

class DarkThemeFactory implements UIFactory {
  createButton(label: string): Button {
    return new DarkButton(label, {
      bg: '#1e293b', color: '#f8fafc', border: '#334155',
      hoverBg: '#334155',
    });
  }
  // ...
}

// ファクトリーの切り替え
function getThemeFactory(theme: 'light' | 'dark'): UIFactory {
  return theme === 'dark' ? new DarkThemeFactory() : new LightThemeFactory();
}

Builder パターン

複雑なオブジェクト(クエリ、設定、リクエスト)の構築にはBuilderパターンが最適です。

class QueryBuilder<T> {
  private conditions: string[] = [];
  private orderByClause: string[] = [];
  private limitValue?: number;
  private offsetValue?: number;
  private selectFields: string[] = ['*'];
  private joinClauses: string[] = [];

  constructor(private tableName: string) {}

  select(...fields: (keyof T)[]): this {
    this.selectFields = fields as string[];
    return this;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  whereEquals(field: keyof T, value: string | number): this {
    this.conditions.push(`${String(field)} = '${value}'`);
    return this;
  }

  join(table: string, on: string, type: 'INNER' | 'LEFT' | 'RIGHT' = 'INNER'): this {
    this.joinClauses.push(`${type} JOIN ${table} ON ${on}`);
    return this;
  }

  orderBy(field: keyof T, direction: 'ASC' | 'DESC' = 'ASC'): this {
    this.orderByClause.push(`${String(field)} ${direction}`);
    return this;
  }

  limit(value: number): this {
    this.limitValue = value;
    return this;
  }

  offset(value: number): this {
    this.offsetValue = value;
    return this;
  }

  build(): string {
    let query = `SELECT ${this.selectFields.join(', ')} FROM ${this.tableName}`;

    if (this.joinClauses.length > 0) {
      query += ` ${this.joinClauses.join(' ')}`;
    }

    if (this.conditions.length > 0) {
      query += ` WHERE ${this.conditions.join(' AND ')}`;
    }

    if (this.orderByClause.length > 0) {
      query += ` ORDER BY ${this.orderByClause.join(', ')}`;
    }

    if (this.limitValue !== undefined) {
      query += ` LIMIT ${this.limitValue}`;
    }

    if (this.offsetValue !== undefined) {
      query += ` OFFSET ${this.offsetValue}`;
    }

    return query;
  }
}

// 使用例
const query = new QueryBuilder<User>('users')
  .select('id', 'name', 'email')
  .whereEquals('status', 'active')
  .where('created_at > NOW() - INTERVAL 30 DAY')
  .join('departments', 'users.dept_id = departments.id', 'LEFT')
  .orderBy('name', 'ASC')
  .limit(20)
  .offset(0)
  .build();

構造パターン(Structural Patterns)

Decorator パターン

SES案件のAPI開発で頻出する、ロギング・キャッシュ・認証を横断的に追加するパターンです。

// 基本インターフェース
interface ApiClient {
  get<T>(path: string): Promise<T>;
  post<T>(path: string, body: unknown): Promise<T>;
}

// 基本実装
class HttpApiClient implements ApiClient {
  constructor(private baseUrl: string) {}

  async get<T>(path: string): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`);
    return res.json() as Promise<T>;
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    const res = await fetch(`${this.baseUrl}${path}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    return res.json() as Promise<T>;
  }
}

// ログデコレータ
class LoggingDecorator implements ApiClient {
  constructor(private client: ApiClient, private logger: Logger) {}

  async get<T>(path: string): Promise<T> {
    const start = Date.now();
    this.logger.info(`GET ${path}`);
    try {
      const result = await this.client.get<T>(path);
      this.logger.info(`GET ${path} completed`, { duration: Date.now() - start });
      return result;
    } catch (error) {
      this.logger.error(`GET ${path} failed`, { duration: Date.now() - start, error });
      throw error;
    }
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    const start = Date.now();
    this.logger.info(`POST ${path}`, { bodySize: JSON.stringify(body).length });
    try {
      const result = await this.client.post<T>(path, body);
      this.logger.info(`POST ${path} completed`, { duration: Date.now() - start });
      return result;
    } catch (error) {
      this.logger.error(`POST ${path} failed`, { duration: Date.now() - start, error });
      throw error;
    }
  }
}

// キャッシュデコレータ
class CachingDecorator implements ApiClient {
  private cache = new Map<string, { data: unknown; expiry: number }>();

  constructor(private client: ApiClient, private ttlMs: number = 60000) {}

  async get<T>(path: string): Promise<T> {
    const cached = this.cache.get(path);
    if (cached && cached.expiry > Date.now()) {
      return cached.data as T;
    }

    const result = await this.client.get<T>(path);
    this.cache.set(path, { data: result, expiry: Date.now() + this.ttlMs });
    return result;
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    // POST はキャッシュしない
    return this.client.post<T>(path, body);
  }
}

// 認証デコレータ
class AuthDecorator implements ApiClient {
  constructor(private client: ApiClient, private getToken: () => Promise<string>) {}

  async get<T>(path: string): Promise<T> {
    const token = await this.getToken();
    // 認証ヘッダーを内部的に付与(実装では BaseClient を拡張)
    return this.client.get<T>(path);
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    const token = await this.getToken();
    return this.client.post<T>(path, body);
  }
}

// デコレータを組み合わせて使用
const client = new AuthDecorator(
  new CachingDecorator(
    new LoggingDecorator(
      new HttpApiClient('https://api.example.com'),
      logger
    ),
    300000 // 5分キャッシュ
  ),
  () => authService.getToken()
);

Adapter パターン

外部APIのレスポンス形式を内部モデルに変換するアダプターパターンです。

// 内部モデル
interface User {
  id: string;
  fullName: string;
  email: string;
  role: 'admin' | 'user' | 'viewer';
  createdAt: Date;
}

// 外部API A のレスポンス形式
interface ExternalUserA {
  user_id: number;
  first_name: string;
  last_name: string;
  mail_address: string;
  permission_level: number;
  registered_date: string;
}

// 外部API B のレスポンス形式
interface ExternalUserB {
  uid: string;
  display_name: string;
  contact: { email: string; phone?: string };
  access: 'FULL' | 'READ_WRITE' | 'READ_ONLY';
  metadata: { created: number };
}

// アダプター A
class UserAdapterA {
  static adapt(external: ExternalUserA): User {
    const roleMap: Record<number, User['role']> = {
      0: 'viewer',
      1: 'user',
      2: 'admin',
    };

    return {
      id: String(external.user_id),
      fullName: `${external.first_name} ${external.last_name}`,
      email: external.mail_address,
      role: roleMap[external.permission_level] || 'viewer',
      createdAt: new Date(external.registered_date),
    };
  }
}

// アダプター B
class UserAdapterB {
  static adapt(external: ExternalUserB): User {
    const roleMap: Record<string, User['role']> = {
      'FULL': 'admin',
      'READ_WRITE': 'user',
      'READ_ONLY': 'viewer',
    };

    return {
      id: external.uid,
      fullName: external.display_name,
      email: external.contact.email,
      role: roleMap[external.access] || 'viewer',
      createdAt: new Date(external.metadata.created * 1000),
    };
  }
}

振る舞いパターン(Behavioral Patterns)

Strategy パターン

SES案件での料金計算・通知送信・認証方式の切り替えに最適です。

// 通知戦略のインターフェース
interface NotificationStrategy {
  send(recipient: string, message: string, options?: NotificationOptions): Promise<void>;
  validate(recipient: string): boolean;
}

// メール通知
class EmailStrategy implements NotificationStrategy {
  constructor(private transporter: Transporter) {}

  async send(recipient: string, message: string, options?: NotificationOptions): Promise<void> {
    await this.transporter.sendMail({
      to: recipient,
      subject: options?.subject || 'Notification',
      html: message,
    });
  }

  validate(recipient: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient);
  }
}

// Slack通知
class SlackStrategy implements NotificationStrategy {
  constructor(private webhookUrl: string) {}

  async send(recipient: string, message: string): Promise<void> {
    await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ channel: recipient, text: message }),
    });
  }

  validate(recipient: string): boolean {
    return recipient.startsWith('#') || recipient.startsWith('@');
  }
}

// SMS通知
class SmsStrategy implements NotificationStrategy {
  constructor(private twilioClient: TwilioClient) {}

  async send(recipient: string, message: string): Promise<void> {
    await this.twilioClient.messages.create({
      to: recipient,
      from: process.env.TWILIO_PHONE_NUMBER,
      body: message,
    });
  }

  validate(recipient: string): boolean {
    return /^\+[1-9]\d{1,14}$/.test(recipient);
  }
}

// コンテキスト:通知サービス
class NotificationService {
  private strategies = new Map<string, NotificationStrategy>();

  register(type: string, strategy: NotificationStrategy): void {
    this.strategies.set(type, strategy);
  }

  async notify(
    type: string,
    recipient: string,
    message: string,
    options?: NotificationOptions
  ): Promise<void> {
    const strategy = this.strategies.get(type);
    if (!strategy) throw new Error(`Unknown notification type: ${type}`);

    if (!strategy.validate(recipient)) {
      throw new Error(`Invalid recipient for ${type}: ${recipient}`);
    }

    await strategy.send(recipient, message, options);
  }
}

// セットアップ
const notifier = new NotificationService();
notifier.register('email', new EmailStrategy(mailer));
notifier.register('slack', new SlackStrategy(process.env.SLACK_WEBHOOK!));
notifier.register('sms', new SmsStrategy(twilioClient));

// 使用
await notifier.notify('email', '[email protected]', 'Your report is ready');
await notifier.notify('slack', '#alerts', ':warning: Server CPU > 90%');

Observer パターン

イベント駆動アーキテクチャの基盤となるパターンです。

// 型安全なイベントエミッター
type EventMap = {
  'user:created': { userId: string; email: string };
  'user:updated': { userId: string; changes: Record<string, unknown> };
  'order:placed': { orderId: string; userId: string; total: number };
  'order:completed': { orderId: string; completedAt: Date };
  'payment:received': { orderId: string; amount: number; method: string };
};

class TypedEventEmitter<T extends Record<string, unknown>> {
  private listeners = new Map<keyof T, Set<Function>>();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void | Promise<void>): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);

    // unsubscribe関数を返す
    return () => {
      this.listeners.get(event)?.delete(listener);
    };
  }

  async emit<K extends keyof T>(event: K, data: T[K]): Promise<void> {
    const handlers = this.listeners.get(event);
    if (!handlers) return;

    const promises = Array.from(handlers).map(async (handler) => {
      try {
        await handler(data);
      } catch (error) {
        console.error(`Error in event handler for ${String(event)}:`, error);
      }
    });

    await Promise.allSettled(promises);
  }
}

// 使用例
const events = new TypedEventEmitter<EventMap>();

// ユーザー作成時のリスナー登録
events.on('user:created', async (data) => {
  await sendWelcomeEmail(data.email);
});

events.on('user:created', async (data) => {
  await createDefaultSettings(data.userId);
});

events.on('order:placed', async (data) => {
  await notifySlack(`New order: ${data.orderId} ($${data.total})`);
});

// イベント発行
await events.emit('user:created', { userId: '123', email: '[email protected]' });

Command パターン

Undo/Redo機能や操作履歴の記録にCommandパターンを使います。

interface Command {
  execute(): Promise<void>;
  undo(): Promise<void>;
  describe(): string;
}

class CommandHistory {
  private history: Command[] = [];
  private undoneCommands: Command[] = [];

  async execute(command: Command): Promise<void> {
    await command.execute();
    this.history.push(command);
    this.undoneCommands = []; // 新しいコマンド実行でredo履歴をクリア
  }

  async undo(): Promise<void> {
    const command = this.history.pop();
    if (!command) throw new Error('Nothing to undo');
    await command.undo();
    this.undoneCommands.push(command);
  }

  async redo(): Promise<void> {
    const command = this.undoneCommands.pop();
    if (!command) throw new Error('Nothing to redo');
    await command.execute();
    this.history.push(command);
  }

  getHistory(): string[] {
    return this.history.map((cmd) => cmd.describe());
  }
}

// 具体コマンド例:ドキュメント編集
class InsertTextCommand implements Command {
  constructor(
    private document: Document,
    private position: number,
    private text: string
  ) {}

  async execute(): Promise<void> {
    this.document.insertAt(this.position, this.text);
  }

  async undo(): Promise<void> {
    this.document.deleteRange(this.position, this.position + this.text.length);
  }

  describe(): string {
    return `Insert "${this.text.substring(0, 20)}..." at position ${this.position}`;
  }
}

クリーンアーキテクチャのスキャフォールド

Codex CLIで一括生成

Codex CLIに以下のプロンプトを投げると、クリーンアーキテクチャの層構造を一括スキャフォールドできます。

以下の仕様でクリーンアーキテクチャのプロジェクト構造を生成してください:
- ドメイン: ユーザー管理(User, Role, Permission)
- ユースケース: 登録、認証、プロフィール更新、権限管理
- インフラ: PostgreSQL + Redis
- プレゼンテーション: Express.js REST API
- 各層の依存関係は内側のみ(依存性逆転の原則を遵守)

生成されるディレクトリ構造:

src/
├── domain/                    # ドメイン層
│   ├── entities/
│   │   ├── User.ts
│   │   ├── Role.ts
│   │   └── Permission.ts
│   ├── repositories/          # リポジトリインターフェース
│   │   ├── IUserRepository.ts
│   │   └── IRoleRepository.ts
│   ├── services/              # ドメインサービス
│   │   └── AuthDomainService.ts
│   └── value-objects/
│       ├── Email.ts
│       └── Password.ts
├── application/               # アプリケーション層
│   ├── use-cases/
│   │   ├── RegisterUser.ts
│   │   ├── AuthenticateUser.ts
│   │   ├── UpdateProfile.ts
│   │   └── ManagePermissions.ts
│   ├── dto/
│   │   ├── CreateUserDto.ts
│   │   └── UpdateProfileDto.ts
│   └── interfaces/
│       ├── IAuthService.ts
│       └── ICacheService.ts
├── infrastructure/            # インフラ層
│   ├── database/
│   │   ├── PostgresUserRepository.ts
│   │   └── PostgresRoleRepository.ts
│   ├── cache/
│   │   └── RedisCacheService.ts
│   ├── auth/
│   │   └── JwtAuthService.ts
│   └── config/
│       └── database.ts
└── presentation/              # プレゼンテーション層
    ├── controllers/
    │   ├── UserController.ts
    │   └── AuthController.ts
    ├── middleware/
    │   ├── auth.ts
    │   └── validation.ts
    └── routes/
        ├── user.routes.ts
        └── auth.routes.ts

ドメインエンティティの例

// domain/entities/User.ts
export class User {
  private constructor(
    public readonly id: string,
    private _name: string,
    private _email: Email,
    private _password: Password,
    private _role: Role,
    public readonly createdAt: Date
  ) {}

  static create(props: {
    name: string;
    email: string;
    password: string;
    role?: Role;
  }): User {
    const email = Email.create(props.email);
    const password = Password.create(props.password);

    return new User(
      crypto.randomUUID(),
      props.name,
      email,
      password,
      props.role || Role.defaultRole(),
      new Date()
    );
  }

  get name(): string { return this._name; }
  get email(): Email { return this._email; }
  get role(): Role { return this._role; }

  updateName(name: string): void {
    if (!name || name.length < 1) throw new Error('Name cannot be empty');
    this._name = name;
  }

  updateEmail(email: string): void {
    this._email = Email.create(email);
  }

  changeRole(newRole: Role): void {
    this._role = newRole;
  }

  verifyPassword(plainPassword: string): boolean {
    return this._password.verify(plainPassword);
  }
}

Codex CLIでの効果的なパターン適用プロンプト

パターン検出プロンプト

プロジェクトのコードベースを分析して、以下のデザインパターン適用機会を特定してください:

1. switch文が3つ以上のケースを持つ箇所 → Strategy パターン候補
2. 同じインターフェースで異なる実装が必要な箇所 → Factory パターン候補
3. 横断的関心事(ログ、キャッシュ、認証)→ Decorator パターン候補
4. オブジェクト構築が5つ以上のパラメータを持つ → Builder パターン候補
5. コールバックチェーンや状態変化の通知 → Observer パターン候補

各候補について、現在のコード、推奨パターン、改善後のコードを提示してください。

リファクタリングプロンプト

このファイルをStrategy パターンを使ってリファクタリングしてください。
条件:
- 既存のテストが壊れないようにする
- インターフェースを先に定義してから実装クラスを作る
- 移行は段階的に行い、旧コードとの互換性を維持する

Codex CLIのプロンプトエンジニアリングで、効果的なプロンプトの書き方を詳しく解説しています。

SES現場でのデザインパターン活用事例

事例1:決済システムのリファクタリング

あるSES案件では、5つの決済手段(クレジットカード、コンビニ、銀行振込、電子マネー、QRコード決済)への対応が必要でした。

Before: 巨大なif-else文で決済処理を分岐

After: Codex CLIでStrategy + Factory パターンを適用

  • 新しい決済手段の追加が「クラス1つ追加 + ファクトリー登録」だけで完了
  • テストが各決済戦略ごとに独立して書けるようになった
  • コードのカバレッジが45%→87%に向上

事例2:マルチテナントSaaS

Decoratorパターンでテナントごとの機能差分を表現し、Observerパターンでテナント間イベント連携を実装した事例です。

// テナント固有機能のデコレータ
const service = new TenantAuditDecorator(
  new TenantCacheDecorator(
    new BaseUserService(repository),
    tenantCache
  ),
  auditLogger
);

Codex CLIでのレガシーコードリファクタリングも参考にしてください。

テスト戦略:パターン適用後のテスト

デザインパターンを適用したコードは、テストが書きやすくなるのがメリットです。

describe('NotificationService (Strategy)', () => {
  it('should send email notification', async () => {
    const mockEmailStrategy = {
      send: vi.fn().mockResolvedValue(undefined),
      validate: vi.fn().mockReturnValue(true),
    };

    const service = new NotificationService();
    service.register('email', mockEmailStrategy);

    await service.notify('email', '[email protected]', 'Hello');

    expect(mockEmailStrategy.send).toHaveBeenCalledWith(
      '[email protected]', 'Hello', undefined
    );
  });

  it('should throw for invalid recipient', async () => {
    const mockStrategy = {
      send: vi.fn(),
      validate: vi.fn().mockReturnValue(false),
    };

    const service = new NotificationService();
    service.register('email', mockStrategy);

    await expect(
      service.notify('email', 'invalid', 'Hello')
    ).rejects.toThrow('Invalid recipient');
  });
});

Codex CLIのテスト自動化で、テスト生成の詳細を解説しています。

まとめ:Codex CLIでデザインパターンを実務に落とし込む

デザインパターンは「知識」だけでは価値がありません。実務のコードに適用してこそ意味があるのです。

  • Factory: 複数のデータソースやサービスの切り替えを柔軟に
  • Strategy: if-else地獄からの解放、新しい戦略の追加が容易に
  • Decorator: ログ・キャッシュ・認証を透過的に追加
  • Observer: イベント駆動アーキテクチャの基盤
  • Builder: 複雑なオブジェクト構築を読みやすく
  • Command: Undo/Redo、操作履歴の実装

Codex CLIを使えば、パターンの選択から実装、テストまでを一貫してAIに支援させることができます。SES現場で「設計力のあるエンジニア」として差をつけましょう。


OpenAI Codex CLIシリーズの他の記事も読む

👉 Codex CLI入門ガイド 👉 Codex CLI レガシーコードリファクタリング 👉 Codex CLI プロンプトエンジニアリング 👉 SES BASEで案件を探す

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修