抽象クラス

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

オブジェクト指向設計

抽象クラスとは

抽象クラス (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);
  }
}

言語ごとの抽象クラスの扱い

言語ごとの抽象クラスの扱いを以下にまとめる。

言語抽象クラスのサポート代替手段
Javaabstract class キーワードインターフェースの default メソッド (Java 8+)
TypeScriptabstract class キーワードインターフェース + ミックスイン
Pythonabc.ABC + @abstractmethodダックタイピング
Goサポートなしインターフェース (暗黙的実装)
Rustサポートなしトレイト (デフォルト実装付き)

Go と Rust は抽象クラスを持たず、インターフェース/トレイトで同等の機能を実現する。これは「継承よりコンポジション」の思想を言語設計に反映した結果だ。

よくある落とし穴

  • 継承の乱用: 「is-a 関係」がないのに継承を使うと、不自然な設計になる。「犬は動物である」は is-a だが、「スタックはリストである」は is-a ではない
  • 脆い基底クラス問題: 抽象クラスの実装を変更すると、サブクラスが予期しない動作をする。抽象クラスの公開メソッドは慎重に設計する
  • God クラス化: 共通処理を抽象クラスに詰め込みすぎると、巨大な基底クラスになる。責務を分割し、小さな抽象クラスに保つ

「デザインパターン」(GoF 著) と「Effective Java」(Joshua Bloch 著) で、抽象クラスの設計原則が詳しく解説されている。

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

この記事は役に立ちましたか?

関連用語

関連する記事