多態性と設計

共通の窓口で異なる実装を扱う。拡張しやすい設計の中心。

多態性とは

多態性は、同じ呼び出しでも実際のオブジェクトに応じて 異なる処理が実行される性質です。

C++ では virtual 関数、抽象クラス、基底クラスの参照・ポインタで実現します。

抽象クラスとインターフェース

#include <iostream>
#include <memory>
#include <vector>

class DataSource {
public:
    virtual ~DataSource() = default;
    virtual std::string read() const = 0;
    virtual void write(const std::string& data) = 0;
};

class FileDataSource : public DataSource {
private:
    std::string filename;

public:
    FileDataSource(const std::string& f) : filename(f) {}

    std::string read() const override {
        return "File: " + filename;
    }

    void write(const std::string& data) override {
        std::cout << "ファイルに書き込み: " << data << std::endl;
    }
};

class DatabaseDataSource : public DataSource {
private:
    std::string connection;

public:
    DatabaseDataSource(const std::string& c) : connection(c) {}

    std::string read() const override {
        return "DB: " + connection;
    }

    void write(const std::string& data) override {
        std::cout << "DBに書き込み: " << data << std::endl;
    }
};

void processData(DataSource& source) {
    std::cout << source.read() << std::endl;
    source.write("新しいデータ");
}

int main() {
    FileDataSource file("data.txt");
    DatabaseDataSource db("localhost");

    processData(file);
    processData(db);

    return 0;
}

File: data.txt
ファイルに書き込み: 新しいデータ
DB: localhost
DBに書き込み: 新しいデータ

仮想デストラクタの重要性

基底クラスをポインタで扱う場合、デストラクタはvirtual にする必要があります。 そうしないと派生クラス側の後始末が呼ばれません。

#include <iostream>

class Logger {
public:
    virtual ~Logger() {
        std::cout << "Logger 解放" << std::endl;
    }

    virtual void log(const std::string& message) = 0;
};

class FileLogger : public Logger {
public:
    ~FileLogger() override {
        std::cout << "FileLogger 解放" << std::endl;
    }

    void log(const std::string& message) override {
        std::cout << "FILE: " << message << std::endl;
    }
};

int main() {
    Logger* logger = new FileLogger();
    logger->log("hello");
    delete logger;
    return 0;
}

FILE: hello
FileLogger 解放
Logger 解放

Strategy パターン

実装を差し替えたい処理は、Strategy パターンでまとめると拡張しやすくなります。

#include <iostream>
#include <memory>

class SortStrategy {
public:
    virtual ~SortStrategy() = default;
    virtual void sort(int arr[], int size) = 0;
};

class BubbleSort : public SortStrategy {
public:
    void sort(int arr[], int size) override {
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        std::cout << "バブルソート実行" << std::endl;
    }
};

class QuickSort : public SortStrategy {
public:
    void sort(int arr[], int size) override {
        std::cout << "クイックソート実行(簡略版)" << std::endl;
    }
};

class Sorter {
private:
    std::unique_ptr<SortStrategy> strategy;

public:
    explicit Sorter(std::unique_ptr<SortStrategy> s)
        : strategy(std::move(s)) {}

    void setStrategy(std::unique_ptr<SortStrategy> s) {
        strategy = std::move(s);
    }

    void execute(int arr[], int size) {
        strategy->sort(arr, size);
    }
};

int main() {
    int data[] = {3, 1, 4, 1, 5};

    Sorter sorter(std::make_unique<BubbleSort>());
    sorter.execute(data, 5);

    sorter.setStrategy(std::make_unique<QuickSort>());
    sorter.execute(data, 5);

    return 0;
}

バブルソート実行
クイックソート実行(簡略版)

Liskov Substitution の感覚

派生クラスは、基底クラスの代わりとして使える必要があります。 使えないなら、継承の設計を見直したほうがよいです。

class Bird {
public:
    virtual ~Bird() = default;
    virtual void move() const {
        std::cout << "飛ぶ" << std::endl;
    }
};

class Penguin : public Bird {
public:
    void move() const override {
        std::cout << "泳ぐ" << std::endl;
    }
};

このように、共通の見た目だけでなく、意味まで一致するかを確認するのが大事です。

よくある誤り

誤り1: 抽象クラスの参照先を delete するのに virtual デストラクタがない

class Base {
public:
    ~Base() { }
    virtual void f() = 0;
};

誤り2: インターフェースに状態を持たせすぎる

class BadInterface {
protected:
    int value;
public:
    virtual void run() = 0;
};

誤り3: 多態性を使わず条件分岐だけで実装する

if (type == 1) {
    // ...
} else if (type == 2) {
    // ...
}

ポイント

  • 抽象クラスは共通インターフェースを定義する
  • virtual デストラクタで安全に破棄する
  • Strategy パターンは処理差し替えに向く
  • 多態性は条件分岐の増殖を防ぐ
  • LSP を意識すると継承の質が上がる

やってみよう

練習1: ログ出力先を切り替える Logger インターフェースを作る

練習2: 支払い方法を差し替えられる PaymentStrategy を作る

練習3: Shape 系に draw() を追加して描画処理を統一する

チャレンジ: Bird の例が設計として妥当か考え直す

まとめ

  • 多態性で実装を差し替えやすくなる
  • 抽象クラスと仮想関数で依存を減らせる
  • 設計パターンは Phase 8 以降の土台になる