抽象クラス
直接インスタンス化できず、サブクラスに共通のインターフェースと部分的な実装を提供するクラス
オブジェクト指向設計
抽象クラスとは
抽象クラス (Abstract Class) は、直接インスタンス化できないクラスで、サブクラスに共通のインターフェースと部分的な実装を提供する。抽象メソッド (実装を持たないメソッド) を定義し、サブクラスに実装を強制する。
1967 年の Simula 67 で導入された概念で、オブジェクト指向プログラミングの基盤の 1 つだ。テンプレートメソッドパターンやストラテジーパターンなど、GoF デザインパターンの多くが抽象クラスを活用している。
基本的な使い方
abstract class Shape {
abstract area(): number; // サブクラスが必ず実装する
abstract perimeter(): number; // サブクラスが必ず実装する
describe(): string { // 共通の実装 (サブクラスで再利用)
return `面積: ${this.area().toFixed(2)}, 周長: ${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
area(): number { return Math.PI * this.radius ** 2; }
perimeter(): number { return 2 * Math.PI * this.radius; }
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) { super(); }
area(): number { return this.width * this.height; }
perimeter(): number { return 2 * (this.width + this.height); }
}
// const shape = new Shape(); // コンパイルエラー: 抽象クラスはインスタンス化できない
const circle = new Circle(5);
console.log(circle.describe()); // "面積: 78.54, 周長: 31.42"
インターフェースとの違い
| 観点 | 抽象クラス | インターフェース |
|---|---|---|
| 実装の提供 | 部分的な実装を持てる | 実装を持てない (TypeScript) |
| 状態 (フィールド) | 持てる | 持てない |
| コンストラクタ | 持てる | 持てない |
| 多重継承 | 不可 (単一継承) | 可能 (複数実装) |
| アクセス修飾子 | protected, private を使える | すべて public |
| 適するケース | 共通の実装を共有したい | 契約だけを定義したい |
判断フローチャート
共通の実装コードを共有したい?
├── Yes → 抽象クラス
└── No → 複数の型を同時に実装したい?
├── Yes → インターフェース
└── No → どちらでもよい (インターフェースが軽量で推奨)
テンプレートメソッドパターンとの関係
抽象クラスの最も典型的な活用が、テンプレートメソッドパターンだ。アルゴリズムの骨格を抽象クラスで定義し、具体的なステップをサブクラスに委譲する。
abstract class DataExporter {
// テンプレートメソッド: アルゴリズムの骨格
async export(): Promise<void> {
const data = await this.fetchData();
const formatted = this.format(data);
await this.save(formatted);
}
abstract fetchData(): Promise<unknown[]>;
abstract format(data: unknown[]): string;
abstract save(content: string): Promise<void>;
}
class CsvExporter extends DataExporter {
async fetchData() { return db.query('SELECT * FROM orders'); }
format(data: unknown[]) { return data.map(row => Object.values(row).join(',')).join('\n'); }
async save(content: string) { await fs.writeFile('export.csv', content); }
}
抽象クラスよりコンポジションを優先すべきケース
「継承よりコンポジション (Composition over Inheritance)」は、GoF が「デザインパターン」で提唱した原則だ。抽象クラスによる継承は強い結合を生むため、以下のケースではコンポジションを優先する。
- 振る舞いの組み合わせが必要: 「飛べる + 泳げる」のように複数の能力を組み合わせたい場合、単一継承の抽象クラスでは表現できない
- 継承階層が深くなる: 3 段以上の継承は理解が困難になる。フラットなコンポジションに置き換える
- 基底クラスの変更が頻繁: 抽象クラスの変更はすべてのサブクラスに影響する。変更の影響範囲を限定したい場合はコンポジションが安全
// コンポジションの例: 振る舞いを注入する
interface Formatter { format(data: unknown[]): string; }
interface Storage { save(content: string): Promise<void>; }
class DataExporter {
constructor(
private formatter: Formatter,
private storage: Storage,
) {}
async export(data: unknown[]): Promise<void> {
const formatted = this.formatter.format(data);
await this.storage.save(formatted);
}
}
言語ごとの抽象クラスの扱い
| 言語 | 抽象クラスのサポート | 代替手段 |
|---|---|---|
| Java | abstract class キーワード |
インターフェースの default メソッド (Java 8+) |
| TypeScript | abstract class キーワード |
インターフェース + ミックスイン |
| Python | abc.ABC + @abstractmethod |
ダックタイピング |
| Go | サポートなし | インターフェース (暗黙的実装) |
| Rust | サポートなし | トレイト (デフォルト実装付き) |
Go と Rust は抽象クラスを持たず、インターフェース/トレイトで同等の機能を実現する。これは「継承よりコンポジション」の思想を言語設計に反映した結果だ。
よくある落とし穴
- 継承の乱用: 「is-a 関係」がないのに継承を使うと、不自然な設計になる。「犬は動物である」は is-a だが、「スタックはリストである」は is-a ではない
- 脆い基底クラス問題: 抽象クラスの実装を変更すると、サブクラスが予期しない動作をする。抽象クラスの公開メソッドは慎重に設計する
- God クラス化: 共通処理を抽象クラスに詰め込みすぎると、巨大な基底クラスになる。責務を分割し、小さな抽象クラスに保つ
「デザインパターン」(GoF 著) と「Effective Java」(Joshua Bloch 著) で、抽象クラスの設計原則が詳しく解説されている。
抽象クラスの関連書籍も参考になる。