抽象クラス

直接インスタンス化できず、サブクラスに共通のインターフェースと部分的な実装を提供するクラス

オブジェクト指向設計

抽象クラスとは

抽象クラス (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 著) で、抽象クラスの設計原則が詳しく解説されている。

抽象クラスの関連書籍も参考になる。

関連用語