C++プログラマへの道 #21 - 生成に関するデザインパターン

Last Edited: 1/22/2025

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

CPP Creational

デザインパターン

C++では、特定のソフトウェア開発の問題を解決するためにクラスやオブジェクトを設定するための無数の選択肢があります。 しかし、この選択肢の自由さが、問題特有のコードを書くことに繋がり、構造体やコンストラクタ、デストラクタ、継承や合成に関して不適切な設計が生じる可能性があります。 このようなコードは、可読性、保守性、スケーラビリティを低下させることがあります。

これらの課題に対処するため、ソフトウェア開発者はデザインパターン—共通のソフトウェア問題を解決するための標準的な方法—を作成しました。 これらのパターンは、スケーラブルで他の人が効果的に作業できるコードを開発者が書けるようにサポートします。デザインパターンには、 主に生成、構造、振る舞いに関するデザインパターンの3つのタイプがあり、それぞれが異なる目的に応じて機能します。 この記事では、オブジェクト生成をより柔軟で効率的にすることを目指す生成に関するデザインパターンのいくつかを紹介します。

ファクトリーメソッドパターン

ファクトリーメソッドパターンでは、異なるファクトリークラスが実装できるファクトリーインターフェースを作成します。 このアプローチにより、すべてのファクトリークラスは一貫した方法で異なるクラスのオブジェクトを作成できます。 以下は、ファクトリーメソッドパターンを使用して、異なる形状のオブジェクトを作成する例です:

// Abstract Class for Shapes
class IShape {
public:
    virtual void draw() = 0;
    virtual ~Shape(){};
};
 
// Abstract Class for Factories
class IShapeFactory {
public:
    virtual Shape* createShape() = 0;
    virtual ~ShapeFactory() {};
};
 
// Shape Circle
class Circle : public IShape {
public:
    void draw() override
    {
        std::cout<<"Drawing a Circle"<<std::endl;
    }
};
 
// Shape Square
class Square : public IShape {
public:
    void draw() override
    {
        std::cout<<"Drawing a Square"<<std::endl;
    }
};
 
// Circle Factory
class CircleFactory : public IShapeFactory {
public:
    Shape* createShape() override { return new Circle(); }
};
 
// Square Factory
class SquareFactory : public IShapeFactory {
public:
    Shape* createShape() override { return new Square(); }
};
 
int main() {
    ShapeFactory* circleFactory = new CircleFactory();
    ShapeFactory* squareFactory = new SquareFactory();
 
    Shape* circle = circleFactory->createShape();
    Shape* square = squareFactory->createShape();
 
    circle->draw(); // => Drawing a Circle
    square->draw(); // => Drawing a Square
 
    delete circleFactory;
    delete squareFactory;
    delete circle;
    delete square;
 
    return 0;
}

IShapeFactoryインターフェースを使用することで、CircleFactorySquareFactoryはそれぞれcreateShapeメソッドを実装し、CircleSquareオブジェクトを作成します。 この例ではその利点が分かりずらいかもしれませんが、ファクトリーメソッドパターンは、親クラスのオブジェクトを作成する際に複数の子クラスオブジェクトが必要な場合に特に有用です。 このようなシナリオでは、親オブジェクトの作成ロジックをファクトリー内にカプセル化することができます。 また、IShapeFactoryのようなインターフェースを使用することで、IShapeから派生した新しいクラスを追加しても、他のコード部分に影響を与えません。 新しい具体的なファクトリークラスをIShapeFactoryから派生させるだけで済みます。

ビルダーパターン

ビルダーパターンでは、ビルダークラスを作成し、オブジェクトを一歩ずつ構築することで、多くのパラメーターを必要とするオブジェクトの作成を簡素化します。 以下は、ビルダーパターンを使用する例です:

// Pizza class to build
class Pizza {
public:
    string size;
    string sauce;
    string cheese;
    string toppings;
public:
    void setSize(string size) {
        this->size=size;
    };
    void setSauce(string sauce) {
        this->sauce=sauce;
    };
    void setCheese(string cheese) {
        this->cheese=cheese;
    };
    void setToppings(string toppings) {
        this->toppings=toppings;
    };
    void print() {
        cout << "Size: " << this->size << endl;
        cout << "Sauce: " << this->sauce << endl;
        cout << "cheese: " << this->cheese << endl;
        cout << "Toppings: " << this->toppings << endl;
    }
};
 
// Builder for Pizza
class PizzaBuilder {
public:
    Pizza pizza;
public:
    PizzaBuilder &addSize(string size) {
        this->pizza.setSize(size);
        return *this;
    };
    PizzaBuilder &addSauce(string sauce) {
        this->pizza.setSauce(sauce);
        return *this;
    };
    PizzaBuilder &addCheese(string cheese) {
        this->pizza.setCheese(cheese);
        return *this;
    };
    PizzaBuilder &addToppings(string toppings) {
        this->pizza.setToppings(toppings);
        return *this;
    };
    Pizza &build() {
        return this -> pizza;
    };
};
 
int main () {
    PizzaBuilder builder;
    builder.addSize("Medium").addSauce("Tomato");
    Pizza pizza = builder.addCheese("Cheddar").addToppings("Pepperoni").build();
    pizza.print();
    return 0;
};

ビルダーはクラスのセッター関数を使用してメソッドチェイニングが可能になるように、各属性を構築・設定します。 また、各ビルダーメソッド内に複雑なロジックを実装することもできます。ビルダーパターンの利点は直感的に理解しやすいですが、 時には多くのシンプルなセッター関数やビルダーメソッドが冗長になりがちです。

シングルトンパターン

クラスのインスタンスが1つだけ作成され、アプリケーションの実行中に共有されることを保証したい場合があります。 例えば、バックグラウンドサービス(デーモンプロセス)や、ネットワーキング・データベース接続の管理を行う際などです。 このような場合にはシングルトンパターンが使用できます。

class Singleton {
private:
    static Singleton* instance;
private:
    Singleton() {
        cout << "Singleton instance created." << endl;
    };
    ~Singleton() {
        cout << "Singleton instance destroyed."<< endl;
    };
public:
    static Singleton& getInstance()
    {
        // If the instance doesn't exist, create it
        if (!instance) {
            instance = new Singleton();
        }
        return *instance;
    };
    void someOperations() {
        cout << "Singleton is performing some operation." << endl;
    };
};
 
// Initialize the static instance variable to nullptr
Singleton* Singleton::instance = nullptr;
 
int main()
{
    // Access the Singleton instance
    Singleton& singleton = Singleton::getInstance();
 
    // Use the Singleton instance
    singleton.someOperations();
 
    // Attempting to create another instance will not work
    // Singleton anotherInstance; // This line would not compile
 
    return 0;
}

このパターンでは、静的(static)属性(全インスタンスに共有されるグローバル変数)や静的メソッド(静的属性を使用するメソッド)を活用します。 getInstanceメソッドでは、シングルトンクラスのインスタンスが存在しない場合、新しいインスタンスが作成されます。以降の呼び出しは同じインスタンスを返します。 コンストラクタはプライベートであり、getInstanceメソッドからのみアクセスできるため、新しいインスタンスを作成することができない様になっています。

結論

この記事では、デザインパターンの概念、その有用性、そして生成的デザインパターンのいくつかの例について説明しました。 以降のいくつかの記事では、構造および振る舞いに関するデザインパターンの例を紹介し、大規模なプロジェクトでのコード作成や理解に役立つ方法を解説します。

リソース