プログラミングの柔軟性を高める:TypeScriptにおけるインターフェースと抽象クラス
TypeScriptにおけるインターフェースと抽象クラスの違い
インターフェースは、型のみを定義するものです。具体的には、以下の要素を定義できます。
- プロパティ: 継承するクラスが持つべきプロパティの名前と型
- メソッド名: 継承するクラスが実装しなければならないメソッドの名前と型
一方、抽象クラスは、型と実装の両方を定義します。具体的には、以下の要素を定義できます。
- プロパティ: 継承するクラスが持つべきプロパティ。抽象クラス自身が初期値を設定することもできます。
- 具象メソッド: 継承するクラスが任意で実装できるメソッド。抽象クラス自身が実装を提供します。
- 抽象メソッド: 継承するクラスが必ず実装しなければならないメソッド。抽象メソッドには具体的な実装は記述されません。
インターフェースと抽象クラスの主な違いは以下の通りです。
項目 | インターフェース | 抽象クラス |
---|---|---|
型 | 型のみを定義 | 型と実装を定義 |
抽象メソッド | あり | あり |
具象メソッド | なし | あり |
プロパティ | あり | あり |
継承 | 複数インターフェースを実装可能 | 1つの抽象クラスのみ継承可能 |
インスタンス化 | 不可 | 可能 |
アクセス修飾子 | publicのみ | publicとprotected |
使い分け
- 抽象クラス: 同じ基盤を共有するクラス間で共通の振る舞いを定義したい場合や、部分的な実装を提供したい場合に適しています。
- インターフェース: 異なる種類のクラス間で共通の振る舞いを定義したい場合や、既存のクラスに新しい機能を追加したい場合に適しています。
例
インターフェースを使って、形状を持つオブジェクトを表す場合を考えます。
interface Shape {
draw(): void;
}
class Circle implements Shape {
constructor(private radius: number) {}
draw(): void {
console.log(`円を描画します。半径: ${this.radius}`);
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
draw(): void {
console.log(`矩形を描画します。幅: ${this.width}、高さ: ${this.height}`);
}
}
この例では、Shape
インターフェースは、draw()
メソッドという共通の振る舞いを定義しています。Circle
クラスとRectangle
クラスは、それぞれ異なる形状を表す具象クラスですが、Shape
インターフェースを実装することで、共通の描画機能を提供することができます。
抽象クラスを使って、動物を表すクラスを階層的に定義する場合を考えます。
abstract class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
abstract makeSound(): void;
getName(): string {
return this.name;
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
makeSound(): void {
console.log(`ワンワン`);
}
}
class Cat extends Animal {
constructor(name: string) {
super(name);
}
makeSound(): void {
console.log(`ニャーニャー`);
}
}
この例では、Animal
抽象クラスは、name
プロパティとgetName()
メソッドという共通の要素を定義しています。また、makeSound()
という抽象メソッドを定義し、具体的な鳴き声の実装は継承するクラスに任せています。Dog
クラスとCat
クラスは、それぞれAnimal
抽象クラスを継承し、makeSound()
メソッドを独自に実装することで、それぞれの鳴き声を表現することができます。
この例では、家電製品を表すインターフェースと抽象クラスを定義し、具体的な実装を持つクラスを作成します。
// 家電製品を表すインターフェース
interface ElectronicDevice {
turnOn(): void;
turnOff(): void;
}
// 電源機器を表す抽象クラス
abstract class PoweredDevice implements ElectronicDevice {
protected isOn: boolean = false;
// 電源オン
turnOn(): void {
this.isOn = true;
console.log(`${this.name}を電源オンにしました。`);
}
// 電源オフ
turnOff(): void {
this.isOn = false;
console.log(`${this.name}を電源オフにしました。`);
}
// コンストラクタ(名前を設定)
constructor(protected name: string) {}
}
// 具体的な家電製品を表すクラス
class Television extends PoweredDevice {
constructor() {
super('テレビ');
}
}
class Refrigerator extends PoweredDevice {
constructor() {
super('冷蔵庫');
}
}
// 具体的な家電製品を使用する例
const tv = new Television();
tv.turnOn();
tv.watchMovie(); // テレビ特有の機能
tv.turnOff();
const refrigerator = new Refrigerator();
refrigerator.turnOn();
refrigerator.storeFood(); // 冷蔵庫特有の機能
refrigerator.turnOff();
解説
- また、それぞれのクラスは、
watchMovie
やstoreFood
といった、個別の機能を持つこともできます。 Television
クラスとRefrigerator
クラスは、PoweredDevice
抽象クラスを継承し、turnOn
、turnOff
メソッドを独自に実装することができます。PoweredDevice
抽象クラスは、ElectronicDevice
インターフェースを実装し、isOn
プロパティと基底となる電源オンオフの処理 (turnOn
、turnOff
メソッド) を定義します。ElectronicDevice
インターフェースは、電源のオンオフという共通の振る舞いを定義します。
図形を描画するインターフェースと抽象クラス
// 図形を描画するインターフェース
interface Drawable {
draw(): void;
}
// 図形を表す抽象クラス
abstract class Shape implements Drawable {
protected color: string;
// コンストラクタ(色を設定)
constructor(color: string) {
this.color = color;
}
// 描画
abstract draw(): void;
}
// 具体的な図形を表すクラス
class Circle extends Shape {
constructor(color: string, private radius: number) {
super(color);
}
// 円を描画
draw(): void {
console.log(`色: ${this.color}、半径: ${this.radius} の円を描画します。`);
}
}
class Rectangle extends Shape {
constructor(color: string, private width: number, private height: number) {
super(color);
}
// 矩形を描画
draw(): void {
console.log(`色: ${this.color}、幅: ${this.width}、高さ: ${this.height} の矩形を描画します。`);
}
}
// 具体的な図形を使用する例
const circle = new Circle('赤', 10);
circle.draw();
const rectangle = new Rectangle('青', 5, 10);
rectangle.draw();
Circle
クラスとRectangle
クラスは、Shape
抽象クラスを継承し、draw
メソッドを独自に実装することで、それぞれの図形の描画処理を実現します。Shape
抽象クラスは、Drawable
インターフェースを実装し、color
プロパティと基底となる描画処理 (draw
メソッド) を定義します。Drawable
インターフェースは、draw()
という共通の振る舞いを定義します。
インターフェースと抽象クラスの代替手段
関数型
関数型プログラミングでは、関数をデータ構造として使用し、コードをより宣言的かつテストしやすいものにすることができます。
例えば、形状を描画する機能を関数型で実装する場合を考えます。
type Shape = (color: string) => void;
const circle: Shape = (color) => {
console.log(`色: ${color}、半径: 10 の円を描画します。`);
};
const rectangle: Shape = (color) => {
console.log(`色: ${color}、幅: 5、高さ: 10 の矩形を描画します。`);
};
circle('赤');
rectangle('青');
この例では、Shape
型は、color
を引数に何も返さない関数を表します。circle
とrectangle
関数は、それぞれ円と矩形の描画処理を定義する具体的な関数です。
利点
- 状態を保持する必要がないため、コードがより純粋になる
- テストが容易になる
- コードがより簡潔で読みやすくなる
欠点
- 複雑な振る舞いを表現するのに適していない場合がある
- オブジェクト指向プログラミングの慣習に慣れている開発者にとっては理解しにくい場合がある
代替の設計パターン
状況によっては、インターフェースや抽象クラスよりも適した設計パターンが存在する場合があります。
- ストラテジーパターン: アルゴリズムを異なるバリエーションで実装するパターン。実行時にアルゴリズムを選択的に切り替えるのに適しています。
- デコレータパターン: オブジェクトに動的に機能を追加するパターン。既存のオブジェクトを変更せずに機能を拡張するのに適しています。
インターフェースと抽象クラスは、強力なツールですが、万能ではありません。状況に応じて、関数型プログラミングや代替の設計パターンを検討することが重要です。
typescript oop