Interactive

クラス

クラスは関数の糖衣構文であり必須ではありませんが、学習データの蓄積により、ClaudeCodeはクラスを指定した方が高品質なコードを生成する場合があります。特にデザインパターンの実装、メソッドチェーンの構築、ビジネスロジックの組織化において、クラスを使用することでより適切な実装が自動生成されます。弊社では、データと振る舞いをまとめて表現する場合にのみイミュータブルなクラス設計を採用しています。

弊社の推奨ルール

  1. イミュータブル設計必須 - Object.freeze()とreadonly使用
  2. withメソッドで更新 - 新しいインスタンスを返す
  3. 単一責任の原則 - 1クラス1責務を徹底
  4. アクセサーを活用 - getterで計算済み値を提供
  5. コンストラクタでバリデーション - 不正な状態を防止

ClaudeCodeでの利用

イミュータブルクラスの依頼

「ショッピングカートをイミュータブルなクラスで実装して」
→ 状態管理を含むクラスとして生成
「このクラスをイミュータブル設計に変更して、Object.freeze()も追加して」
→ Object.freeze()と新インスタンス生成に変換

ビジネスロジックの実装

「ドメインモデルとしてUserクラスを設計して」
→ ビジネスロジックを含むクラスとして生成
「注文の状態管理をクラスで実装して、状態遷移のバリデーションも含めて」
→ 状態遷移を含む複雑なロジックを生成

イミュータブル設計の依頼

「このクラスをイミュータブル設計に変更して、Object.freeze()も追加して」
→ Object.freeze()と新インスタンス生成に変換
「この状態更新をイミュータブルパターンで書き直して」
→ 直接変更を避けて新インスタンス生成に変換

イミュータブルなクラス設計

データと操作をまとめて表現したい場合は、イミュータブルなクラスを使用します。状態の変更は新しいインスタンスを生成することで安全に行います。

// ✅ 推奨:イミュータブルなクラス
export class Order {
  constructor(
    private readonly id: string,
    private readonly items: ReadonlyArray<OrderItem> = []
  ) {
    Object.freeze(this);
  }
  
  addItem(item: OrderItem): Order {
    return new Order(this.id, [...this.items, item]);
  }
  
  get total() {
    return this.items.reduce((sum, item) => 
      sum + item.price * item.quantity, 0
    );
  }
}

基本的な構造はシンプルに保ちつつ、必要なメソッドを追加できます。

// 状態管理メソッドの追加
removeItem(itemId: string): Order {
  const filteredItems = this.items.filter(item => item.id !== itemId);
  return new Order(this.id, filteredItems);
}

confirm(): Order {
  if (this.items.length === 0) {
    throw new Error('Cannot confirm empty order');
  }
  return new Order(this.id, this.items);
}

このパターンにより、ビジネスルールがクラス内に集約され、不正な状態遷移を防げます。

ドメインモデルの実装

複雑なビジネスロジックを持つドメインオブジェクトもイミュータブルなクラスで表現します。

// ✅ 推奨:ドメインモデル
export class User {
  constructor(
    private readonly id: string,
    private readonly email: string
  ) {
    this.validateEmail(email);
    Object.freeze(this);
  }
  
  private validateEmail(email: string): void {
    if (!email.includes('@')) {
      throw new Error('Invalid email format');
    }
  }
  
  changeEmail(newEmail: string): User {
    return new User(this.id, newEmail);
  }
}

ビジネスロジックの追加も段階的に実装できます。

// 追加のドメインロジック
get isValid() {
  return this.email.includes('@');
}

deactivate(): User {
  return new User(this.id, this.email);
}

このパターンにより、ドメインの知識がクラスに集約され、保守しやすくなります。

状態機械としてのクラス

複雑な状態遷移を持つオブジェクトも、イミュータブルなクラスで安全に実装できます。

type TaskStatus = 'todo' | 'in_progress' | 'done';

export class Task {
  constructor(
    private readonly id: string,
    private readonly status: TaskStatus = 'todo'
  ) {
    Object.freeze(this);
  }

状態遷移メソッドはそれぞれ現在の状態をチェックし、有効な遷移のみを許可します。不正な状態遷移はエラーとして扱い、型安全な状態管理を実現します。

  start(): Task {
    if (this.status !== 'todo') {
      throw new Error('Task can only be started from todo status');
    }
    return new Task(this.id, 'in_progress');
  }
  
  complete(): Task {
    if (this.status !== 'in_progress') {
      throw new Error('Task can only be completed from in_progress status');
    }
    return new Task(this.id, 'done');
  }
}

状態チェックのヘルパーメソッドも追加できます。

// 状態チェック用のgetterメソッド
get canStart() {
  return this.status === 'todo';
}

get canComplete() {
  return this.status === 'in_progress';
}

このパターンにより、不正な状態遷移を防ぎ、型安全な状態管理ができます。

配列の不変操作

配列を含むクラスでも、元の配列を変更せず新しい配列を生成します。

// ❌ 推奨しない:配列の直接変更
class UserList {
  constructor(private users: User[]) {}
  
  addUser(user: User) {
    this.users.push(user); // 元配列を変更
  }
}

配列を含むクラスでも、元の配列を変更せず新しい配列を生成するイミュータブルパターンを使用します。ReadonlyArrayを使って型レベルで不変性を保証し、スプレッド演算子で新しい配列を作成します。

// ✅ 推奨:配列の不変操作
class UserList {
  constructor(private readonly users: ReadonlyArray<User> = []) {
    Object.freeze(this);
  }
  
  addUser(user: User): UserList {
    return new UserList([...this.users, user]);
  }
  
  removeUser(userId: string): UserList {
    return new UserList(this.users.filter(u => u.id !== userId));
  }
}

このパターンにより、元のデータが保護され、予期しない変更を防げます。

よくある問題

withメソッドでthisを返してしまう

Fluent APIのwithメソッドで、新しいインスタンスを作らずにthisを返してしまい、不変性が破られる問題です。必ず新しいインスタンスを返します。

可変なプロパティを含む

readonlyを付け忘れたり、配列やオブジェクトを直接変更可能にして、不変性が破られる問題です。全てのプロパティをreadonlyにし、配列はReadonlyArrayを使用します。

Object.freeze()の位置が間違っている

プロパティ設定後にfreeze()を呼ばず、効果が発揮されない問題です。コンストラクタの最後でObject.freeze(this)を必ず呼び出します。

ビジネスロジックの散在

関連するロジックがクラス外に分散して、保守性が悪化する問題です。ドメインの知識はクラス内に集約し、外部からは最小限のAPIのみ公開します。

深いオブジェクトの更新が複雑

ネストしたオブジェクトの一部だけ更新したい場合に、コードが複雑になる問題です。適切に分割するか、Immerなどのライブラリを検討します。

パフォーマンスへの懸念

インスタンス生成のコストを心配してイミュータブル設計を避ける問題です。モダンJSエンジンでは最適化されており、保守性のメリットが大きく上回ります。