C++ でファクトリーメソッドパターンを正しく実装する方法 質問する

C++ でファクトリーメソッドパターンを正しく実装する方法 質問する

C++ には、簡単に聞こえるにもかかわらず、正直に言ってやり方がわからないため、かなり長い間私を不安にさせてきたことが 1 つあります。

C++ でファクトリ メソッドを正しく実装するにはどうすればよいですか?

目標: 許容できない結果やパフォーマンスの低下を招くことなく、クライアントがオブジェクトのコンストラクターではなくファクトリ メソッドを使用してオブジェクトをインスタンス化できるようにします。

「ファクトリ メソッド パターン」とは、オブジェクト内の静的ファクトリ メソッド、または別のクラスで定義されたメソッド、またはグローバル関数の両方を意味します。一般的には、「クラス X のインスタンス化の通常の方法をコンストラクター以外の場所にリダイレクトするという概念」です。

私が考えたいくつかの可能な答えをざっと見てみましょう。


0) ファクトリーを作るのではなく、コンストラクターを作ります。

これは良い方法のように思えますが (実際、多くの場合、最善の解決策です)、一般的な解決策ではありません。まず、オブジェクトの構築が、別のクラスへの抽出を正当化するほど複雑なタスクである場合があります。しかし、その事実を脇に置いても、単純なオブジェクトであっても、コンストラクターだけを使用するのは多くの場合適切ではありません。

私が知っている最も単純な例は、2D Vector クラスです。とても単純ですが、扱いにくいです。直交座標と極座標の両方からこれを構築できるようにしたいのですが、明らかに、次のことはできません。

struct Vec2 {
    Vec2(float x, float y);
    Vec2(float angle, float magnitude); // not a valid overload!
    // ...
};

私の自然な考え方は次のようになります。

struct Vec2 {
    static Vec2 fromLinear(float x, float y);
    static Vec2 fromPolar(float angle, float magnitude);
    // ...
};

これにより、コンストラクターの代わりに静的ファクトリー メソッドを使用することになります。つまり、基本的には、何らかの方法でファクトリー パターンを実装することになります (「クラスが独自のファクトリーになる」)。これは見た目は良いですが (この特定のケースには適しています)、場合によっては失敗します。これについては、ポイント 2 で説明します。ぜひ読み進めてください。

別のケース: ある API の 2 つの不透明な typedef (無関係なドメインの GUID や、GUID とビットフィールドなど) によってオーバーロードしようとすると、意味的にはまったく異なる型 (つまり、理論上は有効なオーバーロード) になりますが、実際には unsigned int や void ポインターのように同じものになります。


1) Java の方法

Java では動的に割り当てられたオブジェクトのみを使用するため、シンプルです。ファクトリの作成は次のように簡単です。

class FooFactory {
    public Foo createFooInSomeWay() {
        // can be a static method as well,
        //  if we don't need the factory to provide its own object semantics
        //  and just serve as a group of methods
        return new Foo(some, args);
    }
}

C++ では、これは次のように変換されます。

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

クールですか? 確かに、よくあることです。しかし、これにより、ユーザーは動的割り当てのみを使用するよう強制されます。静的割り当ては C++ を複雑にするものですが、多くの場合、強力にするものです。また、動的割り当てを許可しないターゲット (キーワード: 組み込み) もいくつか存在すると思います。また、これらのプラットフォームのユーザーがクリーンな OOP を記述することを好むということではありません。

とにかく、哲学はさておき、一般的なケースでは、ファクトリーのユーザーに動的な割り当てを強制的に制限させたくはありません。


2) 価値によるリターン

さて、動的割り当てが必要な場合、1) が優れていることはわかりました。その上に静的割り当てを追加しないのはなぜでしょうか?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};

何ですか? 戻り値の型でオーバーロードできないのですか? ああ、もちろんできません。では、それを反映するようにメソッド名を変更しましょう。そして、はい、上記の無効なコード例は、メソッド名を変更する必要性がいかに嫌いかを強調するためだけに書きました。たとえば、名前を変更する必要があるため、言語に依存しないファクトリ設計を適切に実装できないためです。また、このコードのすべてのユーザーは、実装と仕様の違いを覚えておく必要があります。

class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};

OK... これで完成です。メソッド名を変更する必要があるため、見苦しいです。同じコードを 2 回記述する必要があるため、不完全です。しかし、一度完成すれば、動作します。そうですよね?

まあ、たいていはそうです。でも、そうでないこともあります。Foo を作成するとき、私たちは実際にコンパイラーが戻り値の最適化を行うことに依存しています。なぜなら、C++ 標準は、C++ で一時オブジェクトを値で返すときに、いつオブジェクトがインプレースで作成され、いつコピーされるかをコンパイラー ベンダーが指定しないほど寛大だからです。したがって、Foo のコピーにコストがかかる場合、このアプローチは危険です。

Foo がまったくコピー可能でない場合はどうなるでしょうか? まあ、仕方ありません。(コピー省略が保証された C++17 では、上記のコードではコピー可能でないこと自体が問題にならないことに注意してください)

結論: オブジェクトを返すことでファクトリを作成することは、確かにいくつかのケース (前述の 2 次元ベクトルなど) では解決策となりますが、それでもコンストラクターの一般的な代替にはなりません。


3) 2段階構造

おそらく誰かが思いつくもう 1 つのことは、オブジェクトの割り当てとその初期化の問題を分離することです。これは通常、次のようなコードになります。

class Foo {
public:
    Foo() {
        // empty or almost empty
    }
    // ...
};

class FooFactory {
public:
    void createFooInSomeWay(Foo& foo, some, args);
};

void clientCode() {
    Foo staticFoo;
    auto_ptr<Foo> dynamicFoo = new Foo();
    FooFactory factory;
    factory.createFooInSomeWay(&staticFoo);
    factory.createFooInSomeWay(&dynamicFoo.get());
    // ...
}

うまく機能すると思う人もいるかもしれません。コードで支払う唯一の代償は...

ここまで書いて、最後に残しておいたので、私もこれが嫌いなのでしょう。:) なぜでしょうか?

まず第一に...私は 2 段階構築の概念が本当に嫌いで、それを使用すると罪悪感を感じます。「存在する場合は有効な状態である」というアサーションを使用してオブジェクトを設計すると、コードがより安全になり、エラーが発生しにくくなると感じます。私はその方法が好きです。

その慣習を放棄し、ファクトリを作成するためだけにオブジェクトの設計を変更するのは、まあ、扱いにくいです。

上記は多くの人を納得させられないことは分かっていますので、もう少し説得力のある議論をしましょう。2 段階構造を使用すると、次のことはできません。

  • constメンバー変数を初期化または参照する、
  • 基本クラスのコンストラクターとメンバー オブジェクトのコンストラクターに引数を渡します。

そして、おそらく、今は思いつかないような欠点が他にもいくつかあるかもしれませんが、上記の箇条書きですでに納得しているので、特に思いつく必要も感じません。

つまり、ファクトリを実装するための一般的なソリューションにはまったく近づいていません。


結論:

私たちが求めているのは、次のようなオブジェクトのインスタンス化の方法です。

  • 割り当てに関係なく均一なインスタンス化を可能にする、
  • 構築メソッドに異なる意味のある名前を付ける(引数によるオーバーロードに依存しない)
  • 特にクライアント側で、パフォーマンスに大きな影響を及ぼさず、できればコードの肥大化も起こさないこと。
  • あらゆるクラスに導入できるなど、汎用的であること。

私が述べた方法ではそれらの要件を満たしていないことが証明されたと思います。

何かヒントはありますか? 解決策を教えてください。この言語ではこのような些細な概念を適切に実装できないとは思いたくありません。

ベストアンサー1

まず、オブジェクトの構築が、別のクラスへの抽出を正当化するほど複雑なタスクである場合があります。

この点は間違っていると思います。複雑さはそれほど重要ではありません。重要なのは関連性です。オブジェクトを 1 つのステップで構築できる場合 (ビルダー パターンとは異なります)、コンストラクターが適切な場所です。ジョブを実行するために別のクラスが本当に必要な場合は、いずれにしてもコンストラクターから使用されるヘルパー クラスにする必要があります。

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

これには簡単な回避策があります:

struct Cartesian {
  inline Cartesian(float x, float y): x(x), y(y) {}
  float x, y;
};
struct Polar {
  inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
  float angle, magnitude;
};
Vec2(const Cartesian &cartesian);
Vec2(const Polar &polar);

唯一の欠点は、少し冗長に見えることです。

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));

しかし、良い点は、使用している座標タイプをすぐに確認でき、同時にコピーについて心配する必要がないことです。コピーが必要な場合、そしてそれが高価である場合(もちろんプロファイリングによって証明されているように)、次のようなものを使用することをお勧めします。Qtの共有クラスコピーのオーバーヘッドを回避するためです。

割り当てタイプに関しては、ファクトリーパターンを使用する主な理由は通常、ポリモーフィズムです。コンストラクタは仮想化できませんし、できたとしてもあまり意味がありません。静的またはスタック割り当てを使用する場合、コンパイラが正確なサイズを知る必要があるため、ポリモーフィックな方法でオブジェクトを作成することはできません。したがって、ポインタと参照でのみ機能します。また、ファクトリーから参照を返すことも機能しません。技術的にはオブジェクトを参照によって削除できますが、混乱を招き、バグが発生しやすいためです。C++ 参照変数を返すという習慣は悪なのでしょうか?たとえば、ポインタだけが残り、スマート ポインタも含まれます。言い換えると、ファクトリは動的割り当てで使用すると最も便利なので、次のようなことができます。

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();

その他のケースでは、ファクトリは、あなたが言及したような過負荷の問題などの軽微な問題を解決するのに役立つだけです。それらを統一された方法で使用できればよいのですが、おそらく不可能だとしてもそれほど問題にはなりません。

おすすめ記事