【TypeScript】抽象クラスとインターフェースの使い分け - 効果的なオブジェクト設計

【TypeScript】抽象クラスとインターフェースの使い分け - 効果的なオブジェクト設計

2024-10-25

2024-10-25

TypeScriptにおける「抽象クラス」と「インターフェース」は、オブジェクト指向プログラミング(OOP)の重要な概念です。どちらも、クラス設計やコードの再利用性を高めるために使われますが、それぞれの役割や使いどころが異なります。この記事では、抽象クラスとインターフェースの違い、適切な使い分け方を解説し、効果的なオブジェクト設計のポイントを紹介します。

抽象クラスとは?

抽象クラスは、クラスの基本的な構造を定義しつつ、他のクラスで共通のロジックを再利用できるようにするクラスです。直接インスタンス化はできませんが、派生クラスで継承され、具体的な実装が行われます。抽象クラスは、オブジェクト指向プログラミングの「共通の振る舞い」を持つクラスに対して有効です。

抽象クラスの基本的な特徴

  1. インスタンス化不可
    抽象クラスはそのままではインスタンス化できません。他のクラスで継承し、具象クラスとして初めてインスタンス化が可能です。
  2. 抽象メソッドを持てる
    抽象クラスは、具象クラスで必ず実装されるべき抽象メソッドを定義できます。抽象メソッドは、実装を持たないメソッドで、派生クラスで具象的な実装が求められます。
  3. 実装も含められる
    抽象クラスは、抽象メソッドだけでなく、具体的なメソッドやプロパティも含むことができます。この点がインターフェースとの大きな違いです。

抽象クラスの例

abstract class Animal {
  constructor(public name: string) {}
  // 抽象メソッド(派生クラスで必ず実装する)
  abstract makeSound(): void;
  // 共通のメソッド
  move(): void {
    console.log(`${this.name} is moving.`);
  }
}
class Dog extends Animal {
  makeSound(): void {
    console.log("Woof! Woof!");
  }
}
const myDog = new Dog("Buddy");
myDog.makeSound();  // "Woof! Woof!"
myDog.move();       // "Buddy is moving."

この例では、Animalという抽象クラスが定義されています。makeSoundという抽象メソッドは、派生クラス(ここではDog)で具体的な実装が提供されます。一方、moveメソッドは共通の動作として抽象クラスで定義されています。

抽象クラスを使うべき場面

  • 複数のクラスで共通のロジックを持たせたいとき。たとえば、Animalクラスのように、moveメソッドのロジックをすべての動物で共有したい場合に抽象クラスを使います。
  • 一部のメソッドを派生クラスで強制的に実装させたい場合。抽象メソッドを定義して、派生クラスでの具体的な実装を要求します。

インターフェースとは?

インターフェースは、クラスが実装すべき契約を定義します。インターフェースそのものには実装は含まれておらず、プロパティやメソッドのシグネチャ(型のみ)を定義します。クラスはこのインターフェースを「実装」し、指定された契約に従った具体的な処理を提供します。

インターフェースの基本的な特徴

  1. 実装を持たない
    インターフェースは、メソッドやプロパティのシグネチャだけを定義し、具体的な処理は含まれません。
  2. 複数のインターフェースを実装できる
    TypeScriptでは、クラスは複数のインターフェースを実装することができます。これにより、クラスが複数の異なる責任を持つことが可能です。
  3. 型としても利用可能
    インターフェースは、オブジェクトやクラスに対して型定義としても使用でき、柔軟な型安全を提供します。

インターフェースの例

interface Flyable {
  fly(): void;
}
interface Swimmable {
  swim(): void;
}
class Bird implements Flyable {
  fly(): void {
    console.log("Bird is flying.");
  }
}
class Fish implements Swimmable {
  swim(): void {
    console.log("Fish is swimming.");
  }
}
const bird = new Bird();
bird.fly();  // "Bird is flying."
const fish = new Fish();
fish.swim(); // "Fish is swimming."

この例では、FlyableSwimmableという2つのインターフェースが定義されており、BirdクラスとFishクラスがそれぞれのインターフェースを実装しています。インターフェースを使うことで、異なるクラスが共通のメソッドシグネチャを持つことを保証できます。

インターフェースを使うべき場面

  • 複数のクラスが共通の契約を持つ必要がある場合。たとえば、異なるクラスが共通のメソッドシグネチャを持つべき場合に、インターフェースを使います。
  • 特定の実装に依存せず、柔軟な拡張性を持たせたいとき。インターフェースは具象クラスの制約を設けないため、さまざまなクラスが同じ契約に従いながらも異なる実装を提供できます。

抽象クラスとインターフェースの違い

特徴抽象クラスインターフェース
実装の有無実装を含むことができる実装を含まない
インスタンス化直接インスタンス化できない直接インスタンス化できない
継承の制限単一のクラスしか継承できない複数のインターフェースを実装可能
使用用途共通のロジックと抽象メソッドを提供メソッドやプロパティの契約を定義
型としての利用クラスの基盤として利用クラス、オブジェクト、関数の型として利用

抽象クラスとインターフェースの使い分け

抽象クラスを使うべきケース

  1. 共通の実装を共有したいとき
    いくつかのクラス間で共通のメソッドやプロパティを持たせ、再利用したい場合は抽象クラスが適しています。
  2. クラスに対して厳密な継承関係を持たせたいとき
    抽象クラスはクラス間の関係を強く制約するため、特定のロジックをクラス階層全体で一貫して保持したい場合に使用します。

インターフェースを使うべきケース

  1. 複数の異なるクラスが共通の契約を実装する必要があるとき
    クラスが異なる具体的な実装を持ちながら、共通のメソッドやプロパティを持たせたい場合、インターフェースが最適です。
  2. クラスに限らず、関数やオブジェクトにも型として利用したいとき
    インターフェースはクラスだけでなく、オブジェクトや関数の型定義にも使えるため、型の再利用性を高めたい場面で有効です。

まとめ

TypeScriptにおける抽象クラスとインターフェースは、どちらもオブジェクト指向設計の重要な要素です。抽象クラスは共通のロジックを持たせつつ、具体的な実装を強制する場合に便利です。一方、インターフェースは、共通の契約を定義し、柔軟で拡張性の高い設計を実現するのに適しています。これらを適切に使い分けることで、より保守性の高い、拡張可能なアプリケーションを構築できます。

Recommend