TypeScriptの継承と実装の奥深さ:抽象クラスとインターフェースで探求する高度な設計
TypeScriptにおける抽象クラスの継承と実装
継承 (extends) と 実装 は、抽象クラスとサブクラスの関係性を定義する2つの主要な概念です。
継承 は、サブクラスが抽象クラスの構造と機能を継承することを意味します。具体的には、サブクラスは抽象クラスの:
- メソッド: 定義された動作
- プロパティ: データを保持する変数
を継承します。継承されたプロパティとメソッドは、サブクラス内で再定義することもそのまま使用することもできます。
一方、実装 は、抽象クラスで定義された抽象メソッドに具体的な動作を与えることを指します。抽象メソッドは、宣言のみを持ち、具体的な実装は含まれていません。サブクラスは、継承した抽象メソッドに対して独自の実装を提供する必要があります。
要約
- 実装: サブクラスが抽象クラスで定義された抽象メソッドに具体的な動作を与えること
- 継承: サブクラスが抽象クラスの構造と機能を共有する関係性
例
abstract class Shape {
abstract area(): number; // 抽象メソッド
constructor(public color: string) {}
}
class Circle extends Shape {
constructor(public radius: number, color: string) {
super(color); // 継承
}
area(): number {
return Math.PI * this.radius * this.radius; // 実装
}
}
const circle = new Circle(5, "red");
console.log(circle.area()); // 78.53975
この例では、Shape
クラスは抽象クラスとして定義され、area()
という抽象メソッドを持ちます。Circle
クラスはShape
クラスを継承し、area()
メソッドを独自に実装することで、円の面積を計算することができます。
いつ継承を使用するか
- オブジェクト間の関係性を明確に表現したい場合
- コードの再利用性を高めたい場合
- 共通の特性や振る舞いを共有するクラスの階層を定義したい場合
- 抽象クラスのテンプレートを特殊化したい場合
- サブクラスごとに異なる動作を定義したい場合
- 抽象クラスで定義された抽象メソッドに具体的な動作を与えたい場合
まず、Animal
という抽象クラスを定義します。このクラスは、すべての動物に共通する特性と振る舞いを定義します。
abstract class Animal {
constructor(public name: string) {}
abstract makeSound(): void; // 抽象メソッド
}
Animal
クラスには、name
というプロパティとmakeSound()
という抽象メソッドが定義されています。makeSound()
メソッドは抽象メソッドであるため、具体的な実装は含まれていません。
犬と猫のサブクラス
次に、Dog
とCat
というサブクラスを定義します。これらのクラスは、Animal
クラスを継承し、独自の特性と振る舞いを定義します。
class Dog extends Animal {
constructor(name: string) {
super(name); // 継承
}
makeSound(): void {
console.log(`${this.name} はワンワン吠えています`); // 実装
}
}
class Cat extends Animal {
constructor(name: string) {
super(name); // 継承
}
makeSound(): void {
console.log(`${this.name} はニャーニャー鳴いています`); // 実装
}
}
Dog
クラスとCat
クラスは、Animal
クラスからname
プロパティとmakeSound()
抽象メソッドを継承しています。さらに、それぞれのクラスはmakeSound()
メソッドを独自に実装し、犬と猫の鳴き声を表現しています。
インスタンスの作成と動作確認
最後に、Dog
とCat
のインスタンスを作成し、makeSound()
メソッドを呼び出して動作を確認します。
const dog = new Dog("ポチ");
dog.makeSound(); // ポチはワンワン吠えています
const cat = new Cat("ミケ");
cat.makeSound(); // ミケはニャーニャー鳴いています
このコードを実行すると、以下の出力結果が表示されます。
ポチはワンワン吠えています
ミケはニャーニャー鳴いています
ポイント
- 抽象メソッドは、サブクラスによって独自に実装される必要があります。
- サブクラスは、抽象クラスを継承して、独自の特性や振る舞いを定義することができます。
- 抽象クラスは、共通の特性や振る舞いを共有するクラスの階層を定義するために使用されます。
抽象クラスとインターフェースの使い分け
抽象クラス は、共通の特性や振る舞いを共有するクラスの階層を定義するために使用されます。抽象クラスには、以下の特徴があります。
- サブクラスは、抽象クラスを継承して、抽象メソッドを実装する必要があります。
- インスタンス化することはできません。
- 抽象メソッドを定義することができます。抽象メソッドは、具体的な実装を持たないメソッドです。
- プロパティとメソッドを定義することができます。
インターフェース は、クラスが実装しなければならないプロパティとメソッドのセットを定義するために使用されます。インターフェースには、以下の特徴があります。
- クラスは、インターフェースを実装することで、そのインターフェースで定義されたプロパティとメソッドを提供する必要があります。
- 抽象メソッドを定義することはできません。
使い分け
抽象クラスとインターフェースの使い分けは、以下の状況によって判断することができます。
- インスタンス化可能なクラスを作成したい場合: 抽象クラスまたはクラスを使用します。
- 複数のインターフェースを実装したい場合: クラスを使用します。
- 抽象メソッドを定義したい場合: 抽象クラスを使用します。
- クラスが実装しなければならないプロパティとメソッドのセットを定義したい場合: インターフェースを使用します。
- 共通の特性や振る舞いを共有するクラスの階層を定義したい場合: 抽象クラスを使用します。
例
以下の例は、抽象クラスとインターフェースを使い分ける方法を示しています。
抽象クラス
abstract class Shape {
abstract area(): number; // 抽象メソッド
constructor(public color: string) {}
}
この例では、Shape
という抽象クラスを定義しています。このクラスは、すべての形状に共通する特性と振る舞いを定義します。area()
という抽象メソッドは、形状の面積を計算するメソッドです。
インターフェース
interface Drawable {
draw(): void;
}
この例では、Drawable
というインターフェースを定義しています。このインターフェースは、描画できるオブジェクトを表します。draw()
というメソッドは、オブジェクトを描画するメソッドです。
クラス
class Circle extends Shape implements Drawable {
constructor(public radius: number, color: string) {
super(color); // 継承
}
area(): number {
return Math.PI * this.radius * this.radius; // 実装
}
draw(): void {
console.log(`半径 ${this.radius} の円を描画します。`); // 実装
}
}
この例では、Circle
というクラスを定義しています。このクラスは、Shape
クラスを継承し、Drawable
インターフェースを実装します。area()
メソッドは、円の面積を計算するメソッドです。draw()
メソッドは、円を描画するメソッドです。
- インターフェースは、クラスが実装しなければならないプロパティとメソッドのセットを定義するのに適しています。
- 抽象クラスとインターフェースは、状況に応じて適切に使い分けることが重要です。
typescript abstract-class extends