C++プログラマへの道 #23 - 振る舞いに関するデザインパターン

Last Edited: 1/29/2025

このブログ記事では、C++における振る舞いに関するデザインパターンを紹介します。

CPP Behavioral

前回の2つの記事では、オブジェクトの作成や関係の管理をより柔軟かつ効率的にするための生成的デザインパターンと構造的デザインパターン を紹介しました。本記事では、デザインパターンの最後のカテゴリである 振る舞いに関するデザインパターン を紹介します。 これは、オブジェクトの振る舞いを適切に調整し、タスクを効果的に実行できるようにすることを目的としています。

オブザーバーパターン

オブザーバーパターンは、その名前の通り、変更をすべてのオブザーバーに通知(ブロードキャスト)するパターンです。 例えば、YouTubeのすべての登録者に通知を送る仕組みは、オブザーバーパターンを用いて実装できます。

class Video {
public:
    string title;
public:
    Video(string title) : title(title) {};
};
 
class IObserver {
public:
    virtual void displayNotification(string notification) = 0;
};
 
class Channel {
public:
    string title;
    vector<Video*> videos;
private:
    vector<IObserver*> observers;
public:
    void registerObserver(IObserver* observer) {
        observers.push_back(observer);
    };
 
    void notifyObservers(string video_title) {
        for (int i = 0; i < observers.size(); i++) {
            observers[i] -> displayNotification("Check out the new video, "+video_title+", by "+title+"!");
        }
    };
 
    void registerVideo(Video* video) {
        videos.push_back(video);
        notifyObservers(video->title);
    };
 
    Channel(string title) : title(title) {};
};
 
class Subscriber : public IObserver {
public:
    string name;
public:
    void displayNotification(string notification) {
        cout << "Notification for " << name << ": " << notification << endl;
    };
 
    Subscriber(string name) : name(name) {};
};
 
int main() {
    Channel tk("TKBlog");
 
    Subscriber subscriber1("Subscriber 1");
    Subscriber subscriber2("Subscriber 2");
 
    tk.registerObserver(&subscriber1);
    tk.registerObserver(&subscriber2);
 
    Video newVideo("Hello World!");
    tk.registerVideo(&newVideo);
    // => Notification for Subscriber 1: Check out the new video, Hello World!, by TKBlog!
    //    Notification for Subscriber 2: Check out the new video, Hello World!, by TKBlog!
    return 0;
}

このパターンでは、オブザーバー(購読者)へのポインタを格納するベクトルを使用します。そのため、Channelオブジェクトが新しい動画を追加すると、 購読者のメソッドを自動的に呼び出し、通知を表示することができます。

ストラテジーパターン

ストラテジーパターンは、共通のインターフェースを持つ複数の 戦略を利用し、同じメソッドを通じて異なる操作を実行するパターンです。 以下はストラテジーパターンの例で、異なるフィルタリング戦略を使用してベクトルをフィルタリングしています。

class IFilterStrategy {
public:
    virtual vector<int> filter(vector<int> input) = 0;
};
 
class RemoveNegativeStrategy : public IFilterStrategy {
public:
    vector<int> filter(vector<int> input) {
        vector<int> result;
        for (int i = 0; i < input.size(); i++) {
            if (input[i] >= 0) {
                result.push_back(input[i]);
            }
        }
        return result;
    };
};
 
class RemoveEvenStrategy : public IFilterStrategy {
public:
    vector<int> filter(vector<int> input) {
        vector<int> result;
        for (int i = 0; i < input.size(); i++) {
            if (input[i] % 2 != 0) {
                result.push_back(input[i]);
            }
        }
        return result;
    };
};
 
class Values {
public:
    vector<int> values;
public:
    void filter(IFilterStrategy* strategy) {
        values = strategy -> filter(values);
    }
    void display() {
        cout << "Values: ";
        for (int i = 0; i < values.size(); i++) {
            cout << values[i] << " ";
        }
        cout << endl;
    };
    Values(vector<int> values) : values(values) {};
};
 
int main() {
    int arr[5] = {1, 2, -10, 5, -7};
    vector<int> vec(arr, arr + sizeof(arr) / sizeof(arr[0]));
    Values values(vec);
 
    RemoveEvenStrategy removeEven;
    RemoveNegativeStrategy removeNegative;
 
    values.filter(&removeEven);
    values.display(); // => Values: 1, 5, -7
 
    values.filter(&removeNegative);
    values.display(); // => Values: 1, 5
 
    return 0;
};

Valuesクラスの中に異なるフィルタリングメソッドを実装するのではなく、ストラテジーインターフェースを利用して戦略クラスを作成することで、 振る舞いを簡単に管理できます。例えば、新しい動作を追加する場合は、抽象クラスIFilterStrategyを継承した新しいストラテジーを作成するだけで済みます。

依存性注入

依存性注入は、異なる実装を持つが共通の目的を持つオブジェクトに対し、共通のインターフェースを提供するシンプルなデザインパターンです。 このパターンを利用することで、オブジェクトの内部ロジックとそのオブジェクトを利用する側のロジックを分離し、 よりスケーラブルで重複の少ないコードを実現できます。

class IFileStorage {
public:
    virtual void upload(string filename) = 0;
};
 
class AWSS3Storage : public IFileStorage {
public:
    void upload(string filename) {
        cout << "Uploading " << filename << " to AWS S3." << endl;
    };
};
 
class GoogleCloudStorage : public IFileStorage {
public:
    void upload(string filename) {
        cout << "Uploading " << filename << " to Google Cloud." << endl;
    };
};
 
class Uploader {
private:
    IFileStorage* storage;
public:
    void upload(string filename) {
        storage -> upload(filename);
    };
    Uploader(IFileStorage* storage) : storage(storage) {};
};
 
int main() {
    AWSS3Storage s3;
    GoogleCloudStorage gc;
 
    Uploader uploader1(&s3);
    uploader1.upload("example.jpg"); // => Uploading example.jpg to AWS S3.
 
    Uploader uploader2(&gc);
    uploader2.upload("example.jpg"); // =? Uploading example.jpg to Google Cloud.
 
    return 0;
};

例えば、UploaderクラスはIFileStorageへのポインタを持ち、そのインターフェースのメソッドを利用してアップロード処理を実行します。 この仕組みにより、どのファイルストレージを使用するかに関係なく、一貫したアップロード処理が可能になります。依存性注入はストラテジーパターンと似ていますが、 ストラテジーパターンは異なる戦略をメソッドの引数として受け取るのに対し、依存性注入は同じ目的を持つオブジェクトを属性として受け取るという点が異なります。

結論

本記事では、振る舞いに関するデザインパターンとして、オブザーバーパターン、ストラテジーパターン、依存性注入の3つを紹介しました。 これまでに紹介した多くのデザインパターンが、インターフェースとコンポジションを活用していることに気付いたかと思います。 これは、インターフェースとコンポジションの方が継承よりもスケーラブルで管理しやすい傾向があることを示しています。 しかし、デザインパターンは万能な解決策ではないことを強調しておきます。どの場面でどのデザインパターンを適用するべきか、 慎重に考慮することが重要です。

リソース