3.

ポリモーフィズム(多様性)とは何か? Java / C++ コード例と OCP・拡張性の理由

編集
この記事の要点
  • ポリモーフィズム(多態性 / 多様性)とは「同名のメソッドが、対象オブジェクトのクラスによって異なる挙動をする」性質
  • 実現方法: 継承 + メソッドオーバーライド(親型変数に子インスタンス)、インターフェース実装C++ の virtual
  • Animal a = new Dog(); a.cry(); → "ワン" が呼ばれる(コンパイル時型ではなく実行時型で決定)
  • 必要性: Open-Closed Principle(拡張に開かれ、修正に閉じる)。新しい型の追加で既存コードを書き換えずに済む
  • 代表パターン: Strategy(アルゴリズム差し替え)、Factory(生成の抽象化)、Template Method
  • 誤用注意: 何でも継承で繋ぐと多層化して保守困難 → 「継承より委譲」原則も併用

ポリモーフィズムとは

ポリモーフィズム(Polymorphism, 多態性, 多様性)はオブジェクト指向の三大要素(カプセル化・継承・ポリモーフィズム)の一つ。同じインタフェース(メソッド名・シグネチャ)で、対象オブジェクトの実体型に応じて異なる動作をする性質を指します。

Java の基本例: 動物の鳴き声

// 親クラス
public abstract class Animal {
    public abstract String cry();
}

// 子クラス1
public class Dog extends Animal {
    @Override
    public String cry() { return "ワン!"; }
}

// 子クラス2
public class Cat extends Animal {
    @Override
    public String cry() { return "ニャー"; }
}

// 子クラス3
public class Cow extends Animal {
    @Override
    public String cry() { return "モー"; }
}

// 利用側
public class Main {
    public static void main(String[] args) {
        // 親型の変数に子の実体を入れる ← ここがポリモーフィズム
        Animal[] zoo = { new Dog(), new Cat(), new Cow() };

        for (Animal a : zoo) {
            System.out.println(a.cry());
            // 出力:
            // ワン!
            // ニャー
            // モー
        }
    }
}

ポイントは Animal a(コンパイル時型)であっても、a.cry() 呼出時に実行時型(Dog/Cat/Cow)の cry() がディスパッチされること。これを 動的束縛 / late binding / virtual call と呼びます。

インターフェースによるポリモーフィズム

// 継承関係が無くても、共通インタフェースで多態化できる
public interface Payable {
    void pay(int amount);
}

public class CreditCard implements Payable {
    @Override public void pay(int amount) {
        System.out.println("クレジットで " + amount + " 円決済");
    }
}

public class PayPay implements Payable {
    @Override public void pay(int amount) {
        System.out.println("PayPay で " + amount + " 円決済");
    }
}

public class BankTransfer implements Payable {
    @Override public void pay(int amount) {
        System.out.println("銀行振込で " + amount + " 円送金");
    }
}

// 呼出側
public void checkout(Payable method, int amount) {
    method.pay(amount);   // method の実体次第で動作が変わる
}

C++ の virtual

C++ ではメソッドに virtual を付けないと動的束縛になりません:

#include 
#include 
#include 

class Animal {
public:
    virtual ~Animal() = default;          // 仮想デストラクタ必須
    virtual std::string cry() const = 0;  // 純粋仮想関数
};

class Dog : public Animal {
public:
    std::string cry() const override { return "ワン!"; }
};

class Cat : public Animal {
public:
    std::string cry() const override { return "ニャー"; }
};

int main() {
    std::vector> zoo;
    zoo.push_back(std::make_unique());
    zoo.push_back(std::make_unique());

    for (const auto& a : zoo) {
        std::cout << a->cry() << std::endl;
    }
    return 0;
}

もし cry() から virtual を外すと、コンパイル時型(Animal)の cry() が呼ばれてしまい多態化しません。

Python は鴨型付け(Duck Typing)

Python は型宣言なしで自然にポリモーフィズムを実現します:

class Dog:
    def cry(self): return "ワン!"

class Cat:
    def cry(self): return "ニャー"

# 継承関係なし、ただ cry() を持っていれば良い
for a in [Dog(), Cat()]:
    print(a.cry())
# “Duck Typing”: アヒルのように鳴くものはアヒルとみなす

なぜポリモーフィズムが必要か

① Open-Closed Principle (OCP)

「拡張に対して開かれ、修正に対して閉じる」原則。新しい動物クラスを追加したとき、既存の呼出側コードを変更せずに済むのがポリモーフィズムの最大の利点です。

// ポリモーフィズム無し → 新しい動物を増やすたびに if 文を書き換える
public void cry(String kind) {
    if (kind.equals("dog"))      System.out.println("ワン!");
    else if (kind.equals("cat")) System.out.println("ニャー");
    else if (kind.equals("cow")) System.out.println("モー");
    // ← Bird 追加で if 増殖
}

// ポリモーフィズム有り → 新しいクラスを足すだけ
public void cry(Animal a) {
    System.out.println(a.cry());
}
// → class Bird extends Animal { String cry() { return "ピヨ"; }} を追加するだけ

② デザインパターンの基盤

パターンポリモーフィズムの使い方
Strategyアルゴリズム(ソート、料金計算)を差し替え可能に
Factory Method生成する具体型を実装に隠蔽、戻り値はインタフェース
Template Method基底クラスで骨組み、サブクラスで穴を埋める
Observerイベント受信者を Listener インタフェースで抽象化
Decorator同じインタフェースで機能を重ねる
State状態遷移を State 実装の差し替えで表現

③ テスト容易性

本物の DB / 外部 API のかわりにモック実装を差し込める。これは依存性注入 (DI) と組み合わせて初めて成立する仕組みで、ポリモーフィズムが前提です。

public interface UserRepository {
    User find(long id);
}

// 本番実装
public class JdbcUserRepository implements UserRepository { ... }

// テスト用モック
public class MockUserRepository implements UserRepository {
    public User find(long id) {
        return new User(id, "テストユーザ");
    }
}

// 利用側はインタフェース型で受ける
public class UserService {
    private final UserRepository repo;
    public UserService(UserRepository repo) { this.repo = repo; }
    public String greet(long id) { return "Hi " + repo.find(id).name(); }
}

// 本番: new UserService(new JdbcUserRepository())
// テスト: new UserService(new MockUserRepository())

ポリモーフィズムの種類

種類説明
サブタイプ(包含)継承 + override本記事の Animal / Dog / Cat
パラメトリック型パラメータで抽象化Java Generics, C++ template, Rust ジェネリクス
アドホック(オーバーロード)同名で異なるシグネチャJava の println(int) / println(String)
強制(暗黙変換)型変換でメソッド適用Java の int → long 自動昇格

誤用と限界

  • 継承の濫用: 何でも親に詰めて深い継承木 → 変更時の影響範囲が広がる。「継承より委譲」の原則も併用
  • Liskov 置換原則 (LSP) 違反: 子が親の契約を破ると、呼出側で意外な挙動。「正方形は長方形のサブクラスにすべきか?」問題が有名
  • ダウンキャストの多用: Animal a; if (a instanceof Dog) ... はポリモーフィズムを台無し。動作はメソッドに移譲すべき

FAQ

Q: オーバーロード(同名・異引数)もポリモーフィズム?
A: アドホック多態として分類されるが、実行時型による分岐ではない(コンパイル時に決まる)。「真の多態」は通常サブタイプ多態を指す。

Q: Java のジェネリクスは?
A: パラメトリック多態。List / List が同じ List インタフェースで動く点でポリモーフィズムの一種。

Q: virtual を全メソッドに付けるのは?
A: C++ では性能オーバヘッド(vtable 経由呼出)あり。Java は全メソッド事実上 virtual。性能が問題なら final でオーバーライド禁止に。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. オブジェクト指向の概念
  2. 継承の概念と必要性
  3. ポリモーフィズム(多様性)の概念と必要性
  4. 抽象クラスの概念と必要性
  5. インターフェースの概念と必要性
  6. カプセル化の概念と必要性