多態性と設計
共通の窓口で異なる実装を扱う。拡張しやすい設計の中心。
多態性とは
多態性は、同じ呼び出しでも実際のオブジェクトに応じて 異なる処理が実行される性質です。
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 以降の土台になる