T-CREATOR

TypeScript クラス設計:継承・抽象クラス・ミックスインの使い分け

TypeScript クラス設計:継承・抽象クラス・ミックスインの使い分け

TypeScript でクラス設計に取り組む際、継承・抽象クラス・ミックスインという 3 つの強力なパターンが目の前に広がります。しかし、どのパターンをいつ使うべきかという判断は、多くの開発者を悩ませる永遠のテーマではないでしょうか。

「この機能は継承で実装すべき?それとも抽象クラス?」「ミックスインパターンって本当に必要なの?」そんな疑問を抱きながら、結局は慣れ親しんだパターンに頼ってしまう...そのような経験をお持ちの方も多いはずです。

本記事では、TypeScript におけるクラス設計の核心に迫り、継承・抽象クラス・ミックスインの本質的な違いと最適な使い分けについて詳しく解説いたします。単なる文法の説明ではなく、設計思想の理解から実践的な選択基準まで、あなたのクラス設計スキルを確実に向上させる知識をお届けします。

オブジェクト指向設計の核心:継承の本質と限界

継承は、オブジェクト指向プログラミングの根幹をなす概念として長い間愛用されてきました。TypeScript における継承も、この伝統的な考え方を型安全性と共に実現する強力な仕組みです。しかし、継承の真の価値を理解するには、その本質と限界を正しく把握することが重要になります。

継承の本質:IS-A 関係の表現力

継承の最も重要な役割は、「IS-A 関係」を明確に表現することです。これは「A は B の一種である」という関係性を、コードレベルで直接的に表現する仕組みです。

typescript// 基底クラス:動物の共通特性
class Animal {
  protected name: string;
  protected species: string;

  constructor(name: string, species: string) {
    this.name = name;
    this.species = species;
  }

  public makeSound(): void {
    console.log(`${this.name}が音を立てています`);
  }

  public getInfo(): string {
    return `名前: ${this.name}, 種類: ${this.species}`;
  }
}

// 派生クラス:犬は動物の一種
class Dog extends Animal {
  private breed: string;

  constructor(name: string, breed: string) {
    super(name, '犬'); // 基底クラスのコンストラクタ呼び出し
    this.breed = breed;
  }

  // メソッドのオーバーライド
  public makeSound(): void {
    console.log(`${this.name}がワンワンと吠えています`);
  }

  // 独自メソッドの追加
  public fetch(): void {
    console.log(`${this.name}がボールを取ってきました`);
  }

  public getInfo(): string {
    return `${super.getInfo()}, 品種: ${this.breed}`;
  }
}

// 使用例
const myDog = new Dog('ポチ', '柴犬');
myDog.makeSound(); // ポチがワンワンと吠えています
myDog.fetch(); // ポチがボールを取ってきました
console.log(myDog.getInfo()); // 名前: ポチ, 種類: 犬, 品種: 柴犬

このコードから分かるように、継承は以下の価値を提供します:

#特徴説明
1コードの再利用基底クラスの機能を派生クラスで自動的に利用可能
2ポリモーフィズム同じインターフェースで異なる実装を透明に利用
3段階的特化基底クラスの一般的な機能を派生クラスで特化
4型の互換性派生クラスのインスタンスを基底クラス型として扱える

継承の限界:設計の制約と問題点

一方で、継承には避けられない制約と問題があります。これらを理解せずに継承を多用すると、かえって保守性の低いコードを生み出してしまいます。

単一継承の制約

TypeScript(JavaScript)は単一継承のみをサポートしており、複数のクラスから同時に継承することはできません。

typescript// これは不可能!コンパイルエラーになります
// class FlyingFish extends Fish, Bird { } // エラー!

// 解決策の一例:Compositionパターン
class Fish {
  swim(): void {
    console.log('泳いでいます');
  }
}

class Bird {
  fly(): void {
    console.log('飛んでいます');
  }
}

// Compositionを使用した解決法
class FlyingFish {
  private fishBehavior: Fish;
  private birdBehavior: Bird;

  constructor() {
    this.fishBehavior = new Fish();
    this.birdBehavior = new Bird();
  }

  swim(): void {
    this.fishBehavior.swim();
  }

  fly(): void {
    this.birdBehavior.fly();
  }
}

強い結合の問題

継承は基底クラスと派生クラス間に強い結合を生み出します。基底クラスの変更が派生クラスに予期しない影響を与える可能性があります。

typescript// 問題のある設計例
class BaseRepository {
  protected connection: any;

  constructor() {
    this.connection = this.createConnection();
  }

  // この実装変更が全ての派生クラスに影響
  protected createConnection(): any {
    // データベース接続ロジック
    return {
      /* connection object */
    };
  }

  public save(data: any): void {
    // 保存処理
  }
}

class UserRepository extends BaseRepository {
  // BaseRepositoryの内部実装に依存している
  public findUser(id: string): any {
    // this.connectionの具体的な型や実装に依存
    return this.connection.query(
      `SELECT * FROM users WHERE id = ${id}`
    );
  }
}

フラジャイルベースクラス問題

基底クラスの変更が派生クラスの動作を破壊する「フラジャイルベースクラス問題」は、継承設計における深刻な課題です。

typescript// フラジャイルベースクラス問題の例
class Counter {
  private count = 0;

  public increment(): void {
    this.count++;
    this.onIncrement(); // 派生クラスでオーバーライド可能なフック
  }

  public addMultiple(times: number): void {
    for (let i = 0; i < times; i++) {
      this.increment(); // increment()を使用
    }
  }

  protected onIncrement(): void {
    // 派生クラスでオーバーライド可能
  }

  public getCount(): number {
    return this.count;
  }
}

class LoggingCounter extends Counter {
  private logs: string[] = [];

  protected onIncrement(): void {
    this.logs.push(
      `カウントが${this.getCount()}になりました`
    );
  }

  public getLogs(): string[] {
    return [...this.logs];
  }
}

// 使用例
const counter = new LoggingCounter();
counter.addMultiple(3); // 3回incrementが呼ばれるため、3つのログが記録される

// しかし、基底クラスの実装が変更されると...

基底クラスのaddMultipleメソッドの実装を効率化のために変更した場合、派生クラスの動作が意図せず変わってしまう可能性があります。

継承を使うべき場面の判断基準

継承の本質と限界を踏まえ、継承を選択すべき場面を明確に定義しましょう。

#判断基準詳細
1明確な IS-A 関係派生クラスが基底クラスの「一種」として意味的に正しい
2安定した基底クラス基底クラスの設計が成熟しており、頻繁な変更がない
3段階的特化の必要性基底の機能を保持しつつ、追加・変更が必要
4ポリモーフィズムの活用同じインターフェースで異なる振る舞いを実現したい

継承は強力な道具ですが、その力は責任を伴います。次に、より柔軟な抽象化戦略について見ていきましょう。

TypeScript における抽象化戦略

TypeScript は、従来のクラス継承に加えて、インターフェースや抽象クラスといった多様な抽象化手段を提供しています。これらの選択肢を適切に使い分けることで、より柔軟で保守性の高い設計を実現できます。

インターフェースベース設計 vs 抽象クラス設計

インターフェースと抽象クラスは、どちらも「契約」を定義する仕組みですが、その特性と適用場面は大きく異なります。この違いを理解することが、効果的な抽象化戦略の第一歩となります。

インターフェースベース設計の特徴

インターフェースは、実装を一切含まない純粋な「契約」の定義です。

typescript// 支払い処理のインターフェース
interface PaymentProcessor {
  processPayment(
    amount: number,
    currency: string
  ): Promise<PaymentResult>;
  validatePaymentData(data: PaymentData): boolean;
  getTransactionFee(amount: number): number;
}

// 支払い結果の型定義
interface PaymentResult {
  transactionId: string;
  status: 'success' | 'failed' | 'pending';
  timestamp: Date;
  fee: number;
}

interface PaymentData {
  cardNumber: string;
  expiryDate: string;
  cvv: string;
  holderName: string;
}

// クレジットカード決済の実装
class CreditCardProcessor implements PaymentProcessor {
  async processPayment(
    amount: number,
    currency: string
  ): Promise<PaymentResult> {
    // クレジットカード固有の処理ロジック
    const transactionId = this.generateTransactionId();

    try {
      // 外部APIへの決済リクエスト
      const response = await this.callCreditCardAPI(
        amount,
        currency
      );

      return {
        transactionId,
        status: 'success',
        timestamp: new Date(),
        fee: this.getTransactionFee(amount),
      };
    } catch (error) {
      return {
        transactionId,
        status: 'failed',
        timestamp: new Date(),
        fee: 0,
      };
    }
  }

  validatePaymentData(data: PaymentData): boolean {
    // クレジットカード番号のバリデーション
    return (
      this.isValidCreditCardNumber(data.cardNumber) &&
      this.isValidExpiryDate(data.expiryDate) &&
      this.isValidCVV(data.cvv)
    );
  }

  getTransactionFee(amount: number): number {
    return amount * 0.029; // 2.9%の手数料
  }

  private generateTransactionId(): string {
    return `cc_${Date.now()}_${Math.random()
      .toString(36)
      .substring(2)}`;
  }

  private async callCreditCardAPI(
    amount: number,
    currency: string
  ): Promise<any> {
    // 実際のクレジットカードAPI呼び出し
    return { success: true };
  }

  private isValidCreditCardNumber(
    cardNumber: string
  ): boolean {
    // Luhnアルゴリズムなどでの検証
    return (
      cardNumber.length >= 13 && cardNumber.length <= 19
    );
  }

  private isValidExpiryDate(expiryDate: string): boolean {
    // 有効期限の検証
    const now = new Date();
    const expiry = new Date(expiryDate);
    return expiry > now;
  }

  private isValidCVV(cvv: string): boolean {
    // CVVの検証
    return cvv.length === 3 || cvv.length === 4;
  }
}

// PayPal決済の実装
class PayPalProcessor implements PaymentProcessor {
  async processPayment(
    amount: number,
    currency: string
  ): Promise<PaymentResult> {
    const transactionId = `paypal_${Date.now()}`;

    // PayPal固有の処理ロジック
    const paypalResponse = await this.callPayPalAPI(
      amount,
      currency
    );

    return {
      transactionId,
      status: paypalResponse.success ? 'success' : 'failed',
      timestamp: new Date(),
      fee: this.getTransactionFee(amount),
    };
  }

  validatePaymentData(data: PaymentData): boolean {
    // PayPalの場合、メールアドレスによる検証
    return this.isValidEmail(data.holderName); // holderNameにメールアドレスを使用
  }

  getTransactionFee(amount: number): number {
    return amount * 0.034 + 40; // 3.4% + 40円の固定費
  }

  private async callPayPalAPI(
    amount: number,
    currency: string
  ): Promise<any> {
    // PayPal API呼び出し
    return { success: true };
  }

  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

インターフェースベース設計の依存性注入パターン:

typescript// 支払いサービスクラス
class PaymentService {
  constructor(private processor: PaymentProcessor) {}

  async handlePayment(
    amount: number,
    currency: string,
    paymentData: PaymentData
  ): Promise<PaymentResult> {
    // データ検証
    if (!this.processor.validatePaymentData(paymentData)) {
      throw new Error('支払いデータが無効です');
    }

    // 最小金額チェック
    if (amount < 100) {
      throw new Error('最小支払い金額は100円です');
    }

    // 手数料の事前計算と表示
    const fee = this.processor.getTransactionFee(amount);
    console.log(`手数料: ${fee}円`);

    // 実際の決済処理
    return await this.processor.processPayment(
      amount,
      currency
    );
  }

  // プロセッサーの動的変更も可能
  switchProcessor(newProcessor: PaymentProcessor): void {
    this.processor = newProcessor;
  }
}

// 使用例
const creditCardProcessor = new CreditCardProcessor();
const paymentService = new PaymentService(
  creditCardProcessor
);

const paymentData: PaymentData = {
  cardNumber: '4111111111111111',
  expiryDate: '2025-12',
  cvv: '123',
  holderName: 'TARO YAMADA',
};

// クレジットカードで決済
const result1 = await paymentService.handlePayment(
  5000,
  'JPY',
  paymentData
);

// PayPalに切り替え
const paypalProcessor = new PayPalProcessor();
paymentService.switchProcessor(paypalProcessor);

// PayPalで決済
const paypalData: PaymentData = {
  cardNumber: '',
  expiryDate: '',
  cvv: '',
  holderName: 'user@example.com', // PayPalの場合はメールアドレス
};
const result2 = await paymentService.handlePayment(
  3000,
  'JPY',
  paypalData
);

抽象クラス設計の特徴

抽象クラスは、共通の実装と抽象メソッドを組み合わせたテンプレートを提供します。

typescript// データベース接続の抽象クラス
abstract class DatabaseConnection {
  protected connectionString: string;
  protected isConnected: boolean = false;

  constructor(connectionString: string) {
    this.connectionString = connectionString;
  }

  // 共通の実装メソッド
  public async connect(): Promise<void> {
    if (this.isConnected) {
      console.log('既に接続されています');
      return;
    }

    console.log('データベースに接続中...');
    await this.establishConnection();
    this.isConnected = true;
    console.log('接続が完了しました');
  }

  public async disconnect(): Promise<void> {
    if (!this.isConnected) {
      console.log('接続されていません');
      return;
    }

    console.log('データベース接続を切断中...');
    await this.closeConnection();
    this.isConnected = false;
    console.log('切断が完了しました');
  }

  // テンプレートメソッドパターン
  public async executeTransaction<T>(
    operation: (connection: any) => Promise<T>
  ): Promise<T> {
    await this.connect();

    try {
      await this.beginTransaction();
      const result = await operation(this.getConnection());
      await this.commitTransaction();
      return result;
    } catch (error) {
      await this.rollbackTransaction();
      throw error;
    }
  }

  // 抽象メソッド(サブクラスで必須実装)
  protected abstract establishConnection(): Promise<void>;
  protected abstract closeConnection(): Promise<void>;
  protected abstract getConnection(): any;
  protected abstract beginTransaction(): Promise<void>;
  protected abstract commitTransaction(): Promise<void>;
  protected abstract rollbackTransaction(): Promise<void>;

  // 抽象メソッド(クエリ実行)
  public abstract executeQuery(
    sql: string,
    params?: any[]
  ): Promise<any[]>;
  public abstract executeUpdate(
    sql: string,
    params?: any[]
  ): Promise<number>;
}

// MySQL接続の実装
class MySQLConnection extends DatabaseConnection {
  private connection: any;

  protected async establishConnection(): Promise<void> {
    // MySQL固有の接続処理
    this.connection = await this.createMySQLConnection();
  }

  protected async closeConnection(): Promise<void> {
    if (this.connection) {
      await this.connection.end();
      this.connection = null;
    }
  }

  protected getConnection(): any {
    return this.connection;
  }

  protected async beginTransaction(): Promise<void> {
    await this.connection.beginTransaction();
  }

  protected async commitTransaction(): Promise<void> {
    await this.connection.commit();
  }

  protected async rollbackTransaction(): Promise<void> {
    await this.connection.rollback();
  }

  public async executeQuery(
    sql: string,
    params: any[] = []
  ): Promise<any[]> {
    const [rows] = await this.connection.execute(
      sql,
      params
    );
    return rows;
  }

  public async executeUpdate(
    sql: string,
    params: any[] = []
  ): Promise<number> {
    const [result] = await this.connection.execute(
      sql,
      params
    );
    return result.affectedRows;
  }

  private async createMySQLConnection(): Promise<any> {
    // 実際のMySQL接続作成処理
    return {
      execute: async (sql: string, params: any[]) => [
        [],
        { affectedRows: 0 },
      ],
      beginTransaction: async () => {},
      commit: async () => {},
      rollback: async () => {},
      end: async () => {},
    };
  }
}

// PostgreSQL接続の実装
class PostgreSQLConnection extends DatabaseConnection {
  private client: any;

  protected async establishConnection(): Promise<void> {
    this.client = await this.createPostgreSQLClient();
    await this.client.connect();
  }

  protected async closeConnection(): Promise<void> {
    if (this.client) {
      await this.client.end();
      this.client = null;
    }
  }

  protected getConnection(): any {
    return this.client;
  }

  protected async beginTransaction(): Promise<void> {
    await this.client.query('BEGIN');
  }

  protected async commitTransaction(): Promise<void> {
    await this.client.query('COMMIT');
  }

  protected async rollbackTransaction(): Promise<void> {
    await this.client.query('ROLLBACK');
  }

  public async executeQuery(
    sql: string,
    params: any[] = []
  ): Promise<any[]> {
    const result = await this.client.query(sql, params);
    return result.rows;
  }

  public async executeUpdate(
    sql: string,
    params: any[] = []
  ): Promise<number> {
    const result = await this.client.query(sql, params);
    return result.rowCount;
  }

  private async createPostgreSQLClient(): Promise<any> {
    // 実際のPostgreSQL接続作成処理
    return {
      connect: async () => {},
      end: async () => {},
      query: async (sql: string, params?: any[]) => ({
        rows: [],
        rowCount: 0,
      }),
    };
  }
}

選択基準の比較表

#観点インターフェース抽象クラス
1実装の共有不可(メソッドシグネチャのみ)可能(部分実装を提供)
2多重実装可能(複数インターフェースを実装)不可(単一継承のみ)
3コンストラクタ定義不可定義可能
4アクセス修飾子public のみ全て使用可能
5適用場面契約の定義、プラガビリティテンプレート提供、部分実装の共有

型システムを活用した設計制約

TypeScript の型システムは、設計時の制約を型レベルで表現し、コンパイル時にその制約の違反を検出する強力な仕組みを提供します。

条件付き型による設計制約

typescript// 型レベルでの制約表現
type EventType = 'user' | 'order' | 'payment';

// イベントごとに異なるデータ構造を強制
type EventData<T extends EventType> = T extends 'user'
  ? { userId: string; action: string }
  : T extends 'order'
  ? { orderId: string; customerId: string; amount: number }
  : T extends 'payment'
  ? {
      transactionId: string;
      amount: number;
      status: string;
    }
  : never;

// イベントハンドラーの型安全な実装
abstract class EventHandler<T extends EventType> {
  protected eventType: T;

  constructor(eventType: T) {
    this.eventType = eventType;
  }

  // 型に応じたデータを受け取る抽象メソッド
  abstract handle(data: EventData<T>): Promise<void>;

  // 共通のログ処理
  protected log(message: string): void {
    console.log(`[${this.eventType}] ${message}`);
  }
}

// ユーザーイベントハンドラー
class UserEventHandler extends EventHandler<'user'> {
  constructor() {
    super('user');
  }

  // TypeScriptが自動的に正しい型を推論
  async handle(data: EventData<'user'>): Promise<void> {
    this.log(
      `ユーザー ${data.userId}${data.action} を実行しました`
    );
    // data.userId と data.action のみアクセス可能
    // data.orderId などは型エラーになる
  }
}

// 注文イベントハンドラー
class OrderEventHandler extends EventHandler<'order'> {
  constructor() {
    super('order');
  }

  async handle(data: EventData<'order'>): Promise<void> {
    this.log(
      `注文 ${data.orderId} (顧客: ${data.customerId}、金額: ${data.amount}円)`
    );
    // data.orderId, data.customerId, data.amount のみアクセス可能
  }
}

ブランド型による型安全性の向上

typescript// ブランド型による値の区別
type UserId = string & { readonly brand: unique symbol };
type ProductId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };

// 型ガード関数
function createUserId(id: string): UserId {
  if (!/^user_\d+$/.test(id)) {
    throw new Error('無効なユーザーIDフォーマットです');
  }
  return id as UserId;
}

function createProductId(id: string): ProductId {
  if (!/^prod_\d+$/.test(id)) {
    throw new Error('無効な商品IDフォーマットです');
  }
  return id as ProductId;
}

function createOrderId(id: string): OrderId {
  if (!/^order_\d+$/.test(id)) {
    throw new Error('無効な注文IDフォーマットです');
  }
  return id as OrderId;
}

// リポジトリクラスでの型安全な操作
class UserRepository {
  async findById(id: UserId): Promise<User | null> {
    // id は UserId 型なので、間違った型のIDを渡すことはできない
    return this.database.query(
      'SELECT * FROM users WHERE id = ?',
      [id]
    );
  }
}

class ProductRepository {
  async findById(id: ProductId): Promise<Product | null> {
    return this.database.query(
      'SELECT * FROM products WHERE id = ?',
      [id]
    );
  }
}

// 使用例:型安全性の恩恵
const userId = createUserId('user_123');
const productId = createProductId('prod_456');

const userRepo = new UserRepository();
const productRepo = new ProductRepository();

// これは正常動作
await userRepo.findById(userId);
await productRepo.findById(productId);

// これはコンパイルエラー!
// await userRepo.findById(productId); // Error: ProductId is not assignable to UserId
// await productRepo.findById(userId); // Error: UserId is not assignable to ProductId

次に、複数の機能を組み合わせる強力な手法、ミックスインパターンについて詳しく見ていきましょう。

ミックスインによる多重継承の実現

単一継承の制約を超えて、複数の機能を組み合わせる必要がある場面では、ミックスインパターンが強力な解決策となります。TypeScript では、関数を使ったミックスインにより、型安全性を保ちながら多重継承のような柔軟性を実現できます。

Composition パターンとの違いと使い分け

ミックスインと Composition パターンは、どちらも複数の機能を組み合わせる手法ですが、その目的と実装方法は大きく異なります。

Composition パターンの特徴

typescript// Compositionパターンの例:機能を外部から注入
interface Logger {
  log(message: string): void;
}

interface Cache {
  get(key: string): any;
  set(key: string, value: any): void;
}

interface Database {
  query(sql: string): Promise<any>;
}

// 依存性を外部から注入するサービスクラス
class UserService {
  constructor(
    private logger: Logger,
    private cache: Cache,
    private database: Database
  ) {}

  async getUser(id: string): Promise<any> {
    this.logger.log(`ユーザー取得開始: ${id}`);

    // キャッシュから確認
    let user = this.cache.get(`user:${id}`);
    if (user) {
      this.logger.log('キャッシュから取得');
      return user;
    }

    // データベースから取得
    user = await this.database.query(
      `SELECT * FROM users WHERE id = '${id}'`
    );

    // キャッシュに保存
    this.cache.set(`user:${id}`, user);
    this.logger.log(
      'データベースから取得してキャッシュに保存'
    );

    return user;
  }
}

// 具体的な実装を注入
const userService = new UserService(
  { log: (msg) => console.log(msg) },
  new Map(),
  {
    query: async (sql) => ({
      id: '1',
      name: 'テストユーザー',
    }),
  }
);

ミックスインパターンの特徴

typescript// ミックスイン用の型定義
type Constructor = new (...args: any[]) => {};

// ログ機能のミックスイン
function WithLogging<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private logs: string[] = [];

    log(message: string): void {
      const timestamp = new Date().toISOString();
      const logEntry = `[${timestamp}] ${message}`;
      this.logs.push(logEntry);
      console.log(logEntry);
    }

    getLogs(): string[] {
      return [...this.logs];
    }

    clearLogs(): void {
      this.logs = [];
    }
  };
}

// キャッシュ機能のミックスイン
function WithCaching<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private cache = new Map<string, any>();

    getFromCache(key: string): any {
      return this.cache.get(key);
    }

    setToCache(key: string, value: any): void {
      this.cache.set(key, value);
    }

    clearCache(): void {
      this.cache.clear();
    }

    getCacheSize(): number {
      return this.cache.size;
    }
  };
}

// バリデーション機能のミックスイン
function WithValidation<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private validators: Array<(data: any) => boolean> = [];

    addValidator(validator: (data: any) => boolean): void {
      this.validators.push(validator);
    }

    validate(data: any): boolean {
      return this.validators.every((validator) =>
        validator(data)
      );
    }

    getValidatorCount(): number {
      return this.validators.length;
    }
  };
}

// 基底クラス
class BaseService {
  protected serviceName: string;

  constructor(serviceName: string) {
    this.serviceName = serviceName;
  }

  getServiceName(): string {
    return this.serviceName;
  }
}

// 複数のミックスインを組み合わせ
class EnhancedUserService extends WithValidation(
  WithCaching(WithLogging(BaseService))
) {
  constructor() {
    super('UserService');

    // バリデーターを設定
    this.addValidator(
      (data: any) => data && typeof data.id === 'string'
    );
    this.addValidator(
      (data: any) =>
        data && data.name && data.name.length > 0
    );
  }

  async getUser(id: string): Promise<any> {
    this.log(`ユーザー取得開始: ${id}`);

    // キャッシュから確認
    const cacheKey = `user:${id}`;
    let user = this.getFromCache(cacheKey);

    if (user) {
      this.log('キャッシュから取得');
      return user;
    }

    // データベースから取得(模擬)
    user = {
      id,
      name: `ユーザー${id}`,
      email: `user${id}@example.com`,
    };

    // バリデーション
    if (!this.validate(user)) {
      this.log('バリデーションエラー');
      throw new Error('無効なユーザーデータです');
    }

    // キャッシュに保存
    this.setToCache(cacheKey, user);
    this.log('データベースから取得してキャッシュに保存');

    return user;
  }

  getStats(): any {
    return {
      serviceName: this.getServiceName(),
      cacheSize: this.getCacheSize(),
      logCount: this.getLogs().length,
      validatorCount: this.getValidatorCount(),
    };
  }
}

// 使用例
const userService = new EnhancedUserService();
userService.getUser('123').then((user) => {
  console.log('取得したユーザー:', user);
  console.log('サービス統計:', userService.getStats());
});

選択基準の比較

#観点Compositionミックスイン
1結合度疎結合(依存性注入)密結合(継承ベース)
2実行時変更可能(依存オブジェクトの差し替え)不可能(コンパイル時決定)
3型安全性インターフェースに依存継承による型安全性
4コード共有困難(各実装で重複)容易(ミックスイン内で共有)
5適用場面外部依存、柔軟性重視共通機能、拡張性重視

動的ミックスインと静的ミックスインの比較

ミックスインには、コンパイル時に決定される静的ミックスインと、実行時に組み合わせを変更できる動的ミックスインがあります。

静的ミックスイン(推奨)

typescript// 型安全な静的ミックスイン
type Timestamped = {
  timestamp: Date;
  getAge(): number;
};

type Serializable = {
  serialize(): string;
  deserialize(data: string): void;
};

// ミックスイン関数
function WithTimestamp<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base implements Timestamped {
    public timestamp: Date = new Date();

    getAge(): number {
      return Date.now() - this.timestamp.getTime();
    }

    touch(): void {
      this.timestamp = new Date();
    }
  };
}

function WithSerialization<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base implements Serializable {
    serialize(): string {
      const data: any = {};

      // 全てのenumerableプロパティをシリアライズ
      for (const key in this) {
        if (this.hasOwnProperty(key)) {
          data[key] = this[key];
        }
      }

      return JSON.stringify(data);
    }

    deserialize(jsonString: string): void {
      const data = JSON.parse(jsonString);

      for (const key in data) {
        if (data.hasOwnProperty(key)) {
          (this as any)[key] = data[key];
        }
      }
    }
  };
}

// 基底エンティティクラス
class Entity {
  constructor(public id: string) {}
}

// 複数ミックスインの組み合わせ
class User extends WithSerialization(
  WithTimestamp(Entity)
) {
  constructor(
    id: string,
    public name: string,
    public email: string
  ) {
    super(id);
  }

  getDisplayName(): string {
    return `${this.name} (${this.email})`;
  }
}

// 使用例
const user = new User(
  'user_001',
  '田中太郎',
  'tanaka@example.com'
);

// Timestamped機能
console.log('作成時刻:', user.timestamp);
setTimeout(() => {
  console.log('経過時間:', user.getAge(), 'ms');
}, 100);

// Serializable機能
const serialized = user.serialize();
console.log('シリアライズ結果:', serialized);

const newUser = new User('', '', '');
newUser.deserialize(serialized);
console.log(
  '復元されたユーザー:',
  newUser.getDisplayName()
);

動的ミックスイン(特別な場合のみ)

typescript// 動的ミックスインの実装(型安全性に注意が必要)
class DynamicMixin {
  private mixins: Array<(target: any) => void> = [];

  addMixin(mixin: (target: any) => void): this {
    this.mixins.push(mixin);
    return this;
  }

  applyTo(target: any): any {
    this.mixins.forEach((mixin) => mixin(target));
    return target;
  }
}

// ミックスイン機能の定義
const loggingMixin = (target: any) => {
  target.log = function (message: string) {
    console.log(`[${target.constructor.name}] ${message}`);
  };
};

const cachingMixin = (target: any) => {
  target._cache = new Map();
  target.setCache = function (key: string, value: any) {
    this._cache.set(key, value);
  };
  target.getCache = function (key: string) {
    return this._cache.get(key);
  };
};

// 実行時にミックスインを適用
class DynamicService {
  constructor(public name: string) {}
}

const service = new DynamicService('TestService');

// 条件に応じて動的にミックスインを適用
const mixer = new DynamicMixin();

if (process.env.NODE_ENV === 'development') {
  mixer.addMixin(loggingMixin);
}

if (process.env.ENABLE_CACHE === 'true') {
  mixer.addMixin(cachingMixin);
}

mixer.applyTo(service);

// 動的に追加されたメソッドの使用(型安全性が失われる)
(service as any).log?.('サービス開始');
(service as any).setCache?.('key1', 'value1');

動的ミックスインは柔軟性に優れますが、TypeScript の型安全性を損なう可能性があるため、特別な要件がない限り静的ミックスインの使用を推奨いたします。

次に、これらの設計パターンを適切に選択するための指針について詳しく見ていきましょう。

設計原則に基づく選択指針

優れたクラス設計を実現するためには、感覚や慣習だけでなく、確立された設計原則に基づいた判断が重要です。ここでは、SOLID 原則と DRY 原則を中心に、継承・抽象クラス・ミックスインの適切な選択指針を示します。

SOLID 原則とクラス設計パターン

SOLID 原則は、保守性とスケーラビリティの高いオブジェクト指向設計を実現するための 5 つの基本原則です。各原則と設計パターンの関係を詳しく見ていきましょう。

単一責任原則(Single Responsibility Principle)

原則: クラスは変更する理由を 1 つだけ持つべきです。

typescript// 悪い例:複数の責任を持つクラス
class BadUserManager {
  // ユーザーデータの管理
  private users: Map<string, User> = new Map();

  addUser(user: User): void {
    this.users.set(user.id, user);
  }

  // メール送信の責任も持っている(SRP違反)
  sendWelcomeEmail(userId: string): void {
    const user = this.users.get(userId);
    console.log(`Welcome email sent to ${user?.email}`);
  }

  // ログ出力の責任も持っている(SRP違反)
  logUserAction(userId: string, action: string): void {
    console.log(`User ${userId} performed: ${action}`);
  }
}

// 良い例:責任を分離した設計
class UserRepository {
  private users: Map<string, User> = new Map();

  addUser(user: User): void {
    this.users.set(user.id, user);
  }

  getUser(id: string): User | undefined {
    return this.users.get(id);
  }
}

class EmailService {
  sendWelcomeEmail(userEmail: string): void {
    console.log(`Welcome email sent to ${userEmail}`);
  }
}

class UserLogger {
  logUserAction(userId: string, action: string): void {
    console.log(`User ${userId} performed: ${action}`);
  }
}

// ミックスインでの適用例
function WithAuditLogging<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private auditLog: Array<{
      action: string;
      timestamp: Date;
    }> = [];

    protected logAction(action: string): void {
      this.auditLog.push({ action, timestamp: new Date() });
    }

    getAuditLog(): ReadonlyArray<{
      action: string;
      timestamp: Date;
    }> {
      return this.auditLog;
    }
  };
}

開放閉鎖原則(Open-Closed Principle)

原則: クラスは拡張に対して開いており、修正に対して閉じているべきです。

typescript// 抽象クラスによるOCP実装
abstract class PaymentProcessor {
  // 変更に対して閉じている部分(テンプレートメソッド)
  public async processPayment(
    amount: number
  ): Promise<PaymentResult> {
    await this.validateAmount(amount);
    const fee = this.calculateFee(amount);
    const result = await this.executePayment(amount - fee);
    await this.recordTransaction(result);
    return result;
  }

  // 拡張に対して開いている部分
  protected abstract executePayment(
    amount: number
  ): Promise<PaymentResult>;
  protected abstract calculateFee(amount: number): number;

  private async validateAmount(
    amount: number
  ): Promise<void> {
    if (amount <= 0) {
      throw new Error('金額は0より大きい必要があります');
    }
  }

  private async recordTransaction(
    result: PaymentResult
  ): Promise<void> {
    console.log(
      `Transaction recorded: ${result.transactionId}`
    );
  }
}

// 新しい支払い方法を追加(拡張)
class CryptocurrencyProcessor extends PaymentProcessor {
  protected async executePayment(
    amount: number
  ): Promise<PaymentResult> {
    // 暗号通貨固有の処理
    return {
      transactionId: `crypto_${Date.now()}`,
      status: 'success',
      timestamp: new Date(),
    };
  }

  protected calculateFee(amount: number): number {
    return amount * 0.01; // 1%の手数料
  }
}

リスコフ置換原則(Liskov Substitution Principle)

原則: 派生クラスは基底クラスと置換可能でなければなりません。

typescript// LSP を満たす継承設計
abstract class Shape {
  abstract calculateArea(): number;

  // 全ての図形に共通の契約
  public getAreaDescription(): string {
    const area = this.calculateArea();
    if (area < 0) {
      throw new Error('面積は負の値になりません');
    }
    return `面積: ${area}`;
  }
}

class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number
  ) {
    super();
  }

  calculateArea(): number {
    return this.width * this.height; // 常に正の値または0
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  calculateArea(): number {
    return Math.PI * this.radius ** 2; // 常に正の値または0
  }
}

// LSP違反の例(避けるべき)
class InvalidShape extends Shape {
  calculateArea(): number {
    return -1; // 基底クラスの契約に違反(負の面積)
  }
}

// 正しい使用例
function printShapeInfo(shape: Shape): void {
  // どの具体的な図形でも正しく動作する
  console.log(shape.getAreaDescription());
}

const shapes: Shape[] = [
  new Rectangle(5, 10),
  new Circle(7),
];

shapes.forEach(printShapeInfo); // LSPにより安全に実行可能

インターフェース分離原則(Interface Segregation Principle)

原則: クライアントは使用しないインターフェースに依存すべきではなく。

typescript// ISP違反の例
interface BadWorker {
  work(): void;
  eat(): void;
  sleep(): void;
}

// ISPに準拠した設計
interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

// 必要なインターフェースのみを実装
class Human implements Workable, Eatable, Sleepable {
  work(): void {
    console.log('人間が働いています');
  }

  eat(): void {
    console.log('人間が食事しています');
  }

  sleep(): void {
    console.log('人間が睡眠しています');
  }
}

class Robot implements Workable {
  work(): void {
    console.log('ロボットが働いています');
  }
  // eatやsleepは不要なので実装しない
}

// ミックスインでのISP適用
function WithWorking<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base implements Workable {
    work(): void {
      console.log(`${this.constructor.name} is working`);
    }
  };
}

function WithEating<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base implements Eatable {
    eat(): void {
      console.log(`${this.constructor.name} is eating`);
    }
  };
}

// 必要な機能のみを組み合わせ
class Employee extends WithEating(WithWorking(class {})) {}
class Machine extends WithWorking(class {}) {}

依存性逆転原則(Dependency Inversion Principle)

原則: 高水準モジュールは低水準モジュールに依存すべきではなく、両方とも抽象に依存すべきです。

typescript// 抽象(インターフェース)
interface DatabaseRepository {
  save(entity: any): Promise<void>;
  findById(id: string): Promise<any>;
}

interface NotificationService {
  sendNotification(
    message: string,
    recipient: string
  ): Promise<void>;
}

// 高水準モジュール(ビジネスロジック)
class UserService {
  constructor(
    private userRepository: DatabaseRepository,
    private notificationService: NotificationService
  ) {}

  async createUser(userData: any): Promise<void> {
    // ビジネスロジック
    const user = this.validateAndCreateUser(userData);

    // 抽象に依存(具体的な実装は知らない)
    await this.userRepository.save(user);
    await this.notificationService.sendNotification(
      'ユーザーが作成されました',
      user.email
    );
  }

  private validateAndCreateUser(userData: any): any {
    // バリデーションとユーザーオブジェクト作成
    return { ...userData, id: Date.now().toString() };
  }
}

// 低水準モジュール(具体的な実装)
class MySQLUserRepository implements DatabaseRepository {
  async save(entity: any): Promise<void> {
    console.log('MySQLにユーザーを保存しました');
  }

  async findById(id: string): Promise<any> {
    console.log(`MySQLからユーザー${id}を検索しました`);
    return { id, name: 'Test User' };
  }
}

class EmailNotificationService
  implements NotificationService
{
  async sendNotification(
    message: string,
    recipient: string
  ): Promise<void> {
    console.log(`Email sent to ${recipient}: ${message}`);
  }
}

DRY 原則とコード再利用の最適解

DRY(Don't Repeat Yourself)原則は、同じ知識やロジックを複数の場所で重複させることを避けるという原則です。

継承による DRY 実装

typescript// 共通のCRUD操作を基底クラスで定義
abstract class BaseRepository<T extends { id: string }> {
  protected items: Map<string, T> = new Map();

  async save(item: T): Promise<void> {
    this.items.set(item.id, item);
    await this.logOperation('save', item.id);
  }

  async findById(id: string): Promise<T | undefined> {
    const item = this.items.get(id);
    await this.logOperation('findById', id);
    return item;
  }

  async delete(id: string): Promise<boolean> {
    const deleted = this.items.delete(id);
    await this.logOperation('delete', id);
    return deleted;
  }

  // サブクラスで特化可能なフック
  protected async logOperation(
    operation: string,
    id: string
  ): Promise<void> {
    console.log(
      `${this.constructor.name}: ${operation} - ${id}`
    );
  }

  // サブクラスで必須実装
  abstract validate(item: T): boolean;
}

// 特定エンティティのリポジトリ
class UserRepository extends BaseRepository<User> {
  validate(user: User): boolean {
    return user.email.includes('@') && user.name.length > 0;
  }

  // User固有のメソッド
  async findByEmail(
    email: string
  ): Promise<User | undefined> {
    for (const user of this.items.values()) {
      if (user.email === email) {
        await this.logOperation('findByEmail', email);
        return user;
      }
    }
    return undefined;
  }
}

ミックスインによる DRY 実装

typescript// 機能別のミックスイン
function WithValidation<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private validationRules: Array<
      (data: any) => string | null
    > = [];

    addValidationRule(
      rule: (data: any) => string | null
    ): void {
      this.validationRules.push(rule);
    }

    validate(data: any): {
      isValid: boolean;
      errors: string[];
    } {
      const errors = this.validationRules
        .map((rule) => rule(data))
        .filter((error) => error !== null) as string[];

      return {
        isValid: errors.length === 0,
        errors,
      };
    }
  };
}

function WithCaching<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private cache = new Map<
      string,
      { data: any; expiry: number }
    >();
    private defaultTTL = 5 * 60 * 1000; // 5分

    getCached(key: string): any | null {
      const cached = this.cache.get(key);
      if (!cached) return null;

      if (Date.now() > cached.expiry) {
        this.cache.delete(key);
        return null;
      }

      return cached.data;
    }

    setCached(
      key: string,
      data: any,
      ttl: number = this.defaultTTL
    ): void {
      this.cache.set(key, {
        data,
        expiry: Date.now() + ttl,
      });
    }
  };
}

// 複数機能を組み合わせたサービス
class ProductService extends WithCaching(
  WithValidation(class {})
) {
  constructor() {
    super();

    // バリデーションルールの設定
    this.addValidationRule((product: any) =>
      !product.name ? 'Product name is required' : null
    );
    this.addValidationRule((product: any) =>
      product.price <= 0 ? 'Price must be positive' : null
    );
  }

  async getProduct(id: string): Promise<any> {
    // キャッシュから確認
    const cached = this.getCached(`product:${id}`);
    if (cached) return cached;

    // データベースから取得(模擬)
    const product = {
      id,
      name: `Product ${id}`,
      price: 1000,
    };

    // バリデーション
    const validation = this.validate(product);
    if (!validation.isValid) {
      throw new Error(
        `Validation failed: ${validation.errors.join(', ')}`
      );
    }

    // キャッシュに保存
    this.setCached(`product:${id}`, product);

    return product;
  }
}

機能組み合わせの最適解

#要件推奨パターン理由
1同じタイプの処理継承 + 抽象クラステンプレートメソッドパターンが有効
2独立した機能の組み合わせミックスイン柔軟な組み合わせが可能
3外部依存の切り替えComposition + インターフェーステスタビリティと柔軟性
4プラグイン的な機能インターフェース + ファクトリー動的な機能追加

実装パターンとアンチパターン

理論的な理解だけでなく、実際の開発で遭遇しやすい問題とその解決策を知ることが、優れたクラス設計スキルの習得につながります。

よくある設計ミスとその回避法

アンチパターン 1: 深すぎる継承階層

typescript// 悪い例:継承階層が深すぎる
class Animal {}
class Mammal extends Animal {}
class Primate extends Mammal {}
class Ape extends Primate {}
class Human extends Ape {}
class Developer extends Human {}
class TypeScriptDeveloper extends Developer {}
class SeniorTypeScriptDeveloper extends TypeScriptDeveloper {}

// 良い例:Compositionとインターフェースの活用
interface Skill {
  name: string;
  level: number;
}

interface Role {
  title: string;
  responsibilities: string[];
}

class Person {
  constructor(
    public name: string,
    public species: string = 'Human'
  ) {}
}

class Professional {
  private skills: Skill[] = [];
  private role: Role;

  constructor(private person: Person, role: Role) {
    this.role = role;
  }

  addSkill(skill: Skill): void {
    this.skills.push(skill);
  }

  getExpertise(): Skill[] {
    return this.skills.filter((skill) => skill.level >= 8);
  }
}

// 使用例
const developer = new Professional(new Person('田中太郎'), {
  title: 'Senior TypeScript Developer',
  responsibilities: ['設計', 'コードレビュー', '技術指導'],
});

developer.addSkill({ name: 'TypeScript', level: 9 });
developer.addSkill({ name: 'React', level: 8 });

アンチパターン 2: 不適切な抽象化

typescript// 悪い例:過度に抽象化された設計
abstract class AbstractFactoryProducerManager {
  abstract createAbstractFactory(): AbstractFactory;
}

abstract class AbstractFactory {
  abstract createAbstractProduct(): AbstractProduct;
}

abstract class AbstractProduct {
  abstract performAbstractOperation(): void;
}

// 良い例:必要最小限の抽象化
interface PaymentMethod {
  processPayment(amount: number): Promise<boolean>;
}

class CreditCard implements PaymentMethod {
  constructor(private cardNumber: string) {}

  async processPayment(amount: number): Promise<boolean> {
    // 実際の決済処理
    console.log(`Credit card payment: ${amount}`);
    return true;
  }
}

class BankTransfer implements PaymentMethod {
  constructor(private accountNumber: string) {}

  async processPayment(amount: number): Promise<boolean> {
    // 実際の決済処理
    console.log(`Bank transfer: ${amount}`);
    return true;
  }
}

class PaymentProcessor {
  async process(
    method: PaymentMethod,
    amount: number
  ): Promise<boolean> {
    return await method.processPayment(amount);
  }
}

アンチパターン 3: ミックスインの乱用

typescript// 悪い例:関係のない機能を無理やりミックスイン
function WithEverything<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    // 全部入りのミックスイン(アンチパターン)
    log(msg: string) {
      console.log(msg);
    }
    cache = new Map();
    validate() {
      return true;
    }
    serialize() {
      return JSON.stringify(this);
    }
    encrypt(data: string) {
      return btoa(data);
    }
    // ... さらに多くの無関係な機能
  };
}

// 良い例:機能ごとに分離したミックスイン
function WithLogging<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private logLevel: 'info' | 'warn' | 'error' = 'info';

    log(
      message: string,
      level: 'info' | 'warn' | 'error' = 'info'
    ): void {
      if (this.shouldLog(level)) {
        console.log(`[${level}] ${message}`);
      }
    }

    setLogLevel(level: 'info' | 'warn' | 'error'): void {
      this.logLevel = level;
    }

    private shouldLog(level: string): boolean {
      const levels = { info: 0, warn: 1, error: 2 };
      return levels[level] >= levels[this.logLevel];
    }
  };
}

function WithSerialization<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    serialize(): string {
      return JSON.stringify(this);
    }

    static deserialize<T>(
      this: new () => T,
      json: string
    ): T {
      const data = JSON.parse(json);
      const instance = new this();
      Object.assign(instance, data);
      return instance;
    }
  };
}

// 必要な機能のみを組み合わせ
class UserModel extends WithSerialization(
  WithLogging(
    class {
      constructor(public id: string, public name: string) {}
    }
  )
) {}

リファクタリング時の移行戦略

既存のコードを改善する際の段階的なアプローチを示します。

ステップ 1: 現状分析と問題の特定

typescript// リファクタリング前:問題のあるコード
class UserManagerV1 {
  private users: User[] = [];

  addUser(user: User): void {
    // バリデーション
    if (!user.email.includes('@')) {
      throw new Error('Invalid email');
    }

    this.users.push(user);

    // メール送信
    console.log(`Welcome email sent to ${user.email}`);

    // ログ記録
    console.log(`User ${user.name} added at ${new Date()}`);
  }

  removeUser(email: string): void {
    const index = this.users.findIndex(
      (u) => u.email === email
    );
    if (index >= 0) {
      this.users.splice(index, 1);
      console.log(`User removed: ${email}`);
    }
  }
}

ステップ 2: 責任の分離

typescript// ステップ2:インターフェースの導入と責任分離
interface UserValidator {
  validate(user: User): boolean;
}

interface EmailService {
  sendWelcomeEmail(email: string): void;
}

interface UserLogger {
  logUserAdded(user: User): void;
  logUserRemoved(email: string): void;
}

class UserManagerV2 {
  private users: User[] = [];

  constructor(
    private validator: UserValidator,
    private emailService: EmailService,
    private logger: UserLogger
  ) {}

  addUser(user: User): void {
    if (!this.validator.validate(user)) {
      throw new Error('Invalid user data');
    }

    this.users.push(user);
    this.emailService.sendWelcomeEmail(user.email);
    this.logger.logUserAdded(user);
  }

  removeUser(email: string): void {
    const index = this.users.findIndex(
      (u) => u.email === email
    );
    if (index >= 0) {
      const user = this.users[index];
      this.users.splice(index, 1);
      this.logger.logUserRemoved(email);
    }
  }
}

ステップ 3: ミックスインの導入

typescript// ステップ3:ミックスインによる柔軟な機能組み合わせ
function WithUserValidation<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    protected validateUser(user: User): boolean {
      return (
        user.email.includes('@') &&
        user.name.length > 0 &&
        user.id.length > 0
      );
    }
  };
}

function WithEmailNotification<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    protected sendWelcomeEmail(email: string): void {
      console.log(`Welcome email sent to ${email}`);
    }

    protected sendGoodbyeEmail(email: string): void {
      console.log(`Goodbye email sent to ${email}`);
    }
  };
}

function WithAuditLogging<TBase extends Constructor>(
  Base: TBase
) {
  return class extends Base {
    private auditLog: string[] = [];

    protected logAction(action: string): void {
      const logEntry = `${new Date().toISOString()}: ${action}`;
      this.auditLog.push(logEntry);
      console.log(logEntry);
    }

    getAuditLog(): readonly string[] {
      return this.auditLog;
    }
  };
}

// 最終版:機能を組み合わせた設計
class UserManagerV3 extends WithAuditLogging(
  WithEmailNotification(
    WithUserValidation(
      class {
        private users: User[] = [];

        getUsers(): readonly User[] {
          return this.users;
        }
      }
    )
  )
) {
  addUser(user: User): void {
    if (!this.validateUser(user)) {
      this.logAction(
        `Failed to add user: ${user.email} (validation failed)`
      );
      throw new Error('Invalid user data');
    }

    this.users.push(user);
    this.sendWelcomeEmail(user.email);
    this.logAction(
      `User added: ${user.name} (${user.email})`
    );
  }

  removeUser(email: string): void {
    const index = this.users.findIndex(
      (u) => u.email === email
    );
    if (index >= 0) {
      const user = this.users[index];
      this.users.splice(index, 1);
      this.sendGoodbyeEmail(email);
      this.logAction(
        `User removed: ${user.name} (${email})`
      );
    }
  }
}

移行のベストプラクティス

#フェーズアクション注意点
1分析責任の洗い出しSRP 違反の特定
2分離インターフェース導入段階的な変更
3抽象化継承または合成の選択将来の拡張性を考慮
4最適化パフォーマンス調整測定に基づく改善

まとめ

TypeScript におけるクラス設計は、継承・抽象クラス・ミックスインという 3 つの主要なパターンを理解し、適切に使い分けることが重要です。

本記事で解説した選択指針をまとめると:

継承を選ぶべき場面

  • 明確な IS-A 関係が存在する
  • 基底クラスが安定している
  • ポリモーフィズムが必要
  • 段階的な特化が求められる

抽象クラスを選ぶべき場面

  • 共通の実装を提供したい
  • テンプレートメソッドパターンが有効
  • 部分実装と強制実装を組み合わせたい

ミックスインを選ぶべき場面

  • 独立した機能の組み合わせが必要
  • 単一継承の制約を超えたい
  • 機能の再利用性を高めたい

インターフェースを選ぶべき場面

  • 純粋な契約定義が必要
  • 多重実装が必要
  • 依存性の注入を行いたい

設計の判断に迷った時は、SOLID 原則と DRY 原則に立ち返り、将来の変更可能性と保守性を重視した選択を心がけましょう。また、過度な抽象化や複雑なミックスインは避け、必要最小限のシンプルな設計を目指すことが、長期的な成功につながります。

TypeScript の型システムを活用することで、これらの設計パターンをより安全に、より表現力豊かに実装することができます。継続的なリファクタリングを通じて、コードベースの品質を向上させていくことが、優れたソフトウェア開発者への道筋となるでしょう。

関連リンク