C++プログラマへの道 #22 - 構造に関するデザインパターン

Last Edited: 1/25/2025

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

CPP Structural

前回の記事では、オブジェクトの生成をより柔軟かつ効率的にするための生成に関するデザインパターンについて紹介しました。 今回は、オブジェクト間の関係を柔軟で再利用可能かつ保守性の高い方法で管理することを目的とした構造に関するデザインパターンをいくつか紹介します。

アダプターパターン

アダプターパターンは、その名の通り、互換性のない2つのオブジェクトを橋渡しするアダプターを作成することで、 互いに動作できるようにするパターンです。例えば、大文字のみを受け付けるレガシープリンターと、 小文字の文字列を送信するモダンなコンピューターがある場合、アダプターを用意して文字列を大文字に変換し、 レガシープリンターに送ることができます。

class LegacyPrinter {
public:
    void print(string upperCaseString) {
        cout << upperCaseString << endl;
    };
};
 
class ModernComputer {
public:
    string sendMessage() {
        return "message in lower case.";
    }
};
 
class PrinterAdopter {
public:
    string convertMessage(string message) {
        string upperCaseMessage = message;
        for (char& c : upperCaseMessage) {
            c = toupper(c);
        }
        return upperCaseMessage;
    }
};
 
int main() {
    LegacyPrinter printer;
    ModernComputer computer;
    PrinterAdopter adopter;
 
    string message = computer.sendMessage();
    printer.print(adopter.convertMessage(message));
    return 0;
};

PrinterAdapterは、コンピューターから送信されるメッセージを大文字に変換し、プリンターがそのメッセージを印刷できるようにします。 アダプターパターンは直感的で、さまざまなライブラリやオブジェクトを柔軟かつ保守性の高い方法で活用できるようにするデザインです。

ファサードパターン

ファサードパターンは、その名の通り、背後にあるオブジェクトとやり取りするためのインターフェースとして機能するファサードオブジェクトを使用するパターンです。 構成クラスを厳密に非公開の属性として隠すことで、ユーザーはファサードクラスのメソッドを通じてのみやり取りを行います。

class Car {
private:
    Engine engine;
    Lights lights;
 
public:
    void StartCar()
    {
        engine.Start();
        lights.TurnOn();
        std::cout << "Car is ready to drive" << std::endl;
    }
 
    void StopCar()
    {
        lights.TurnOff();
        engine.Stop();
        std::cout << "Car has stopped" << std::endl;
    }
};
int main()
{
    Car car;
    car.StartCar();
    car.StopCar();
    return 0;
}

上記のCarクラスの実装は、ファサードパターンの例です。このクラスは、EngineLightsオブジェクトをカプセル化しているため、 ユーザーはエンジンやライトの実装を気にせずに車とやり取りできます。

プロキシパターン

プロキシパターンは、その名の通り、プロキシオブジェクトを利用して、遅延初期化、アクセス制御、監視などを行うパターンです。 プロキシは仲介者として機能し、自身で処理を行うべきか、ラップしている実際のオブジェクトに委任すべきかを判断します。

// Image Interface
class IImage {
public:
    void display() = 0;
};
 
// Real Image
class RealImage: public IImage {
public:
    RealImage(const std::string& filename) : filename(filename) {
        cout << "Loading image: " << filename << endl; // Heavy operation
    };
 
    void display() override {
        cout << "Displaying image: " << filename << endl;
    };
};
 
// Image Proxy
class ImageProxy: public IImage {
private:
    RealImage *realImage;
    string filename;
public:
    // Do not initialize real image yet
    ImageProxy(const std::string& filename) : filename(filename), realImage(nullptr) {}
 
    // Initialize real object when necessary
    void display() override {
        if (realImage == nullptr) {
            realImage = new RealImage(filename);
        } // If real image has not been loaded, initialize it
        realImage->display();
    }
};

上記の例では、遅延初期化を実装しており、ImageProxyを使用して画像が必要になったときにのみロードを行います。 また、プロキシには、実際の画像オブジェクトへのアクセス制御に関するロジックや、 アクセス情報を記録するロガーオブジェクトを含めることも可能です。

結論

この記事では、構造に関するデザインパターンの例として、アダプターパターン、ファサードパターン、プロキシパターンを紹介しました。 これらのパターンは名前から直感的に理解しやすく、その利点も明確です。しかし、すべての状況においてこれらのパターンを使用するのが適切であるわけではないことを認識することが重要です。 問題に応じて慎重にクラス設計を行う必要があります。次回の記事では、振る舞いに関するデザインパターンを紹介し、デザインパターンに関する議論をまとめます。

リソース