368

C++ には、かなり長い間不快に感じていたことが 1 つあります。

C++ で Factory Method を正しく実装するにはどうすればよいですか?

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

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

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


0) ファクトリを作成しないで、コンストラクタを作成します。

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

私が知っている最も単純な例は、2-D 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 ints や 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++ を複雑にするものですが、多くの場合、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);
    }
};

わかりました...これで終わりです。メソッド名を変更する必要があるため、見苦しいです。同じコードを 2 回記述する必要があるため、これは不完全です。しかし、一度完了すると機能します。右?

まあ、通常。しかし、そうでない場合もあります。Foo を作成するとき、実際にはコンパイラに依存して戻り値の最適化を行います。C++ 標準は、コンパイラ ベンダーがオブジェクトをいつインプレースで作成し、いつオブジェクトを返すときにコピーするかを指定しないように十分に親切であるためです。 C++ の値による一時オブジェクト。したがって、Foo をコピーするのにコストがかかる場合、このアプローチは危険です。

Foo がまったくコピー可能でない場合はどうなるでしょうか。ええと。(コピー省略が保証されている C++17 では、上記のコードではコピー不可であることはもはや問題ではないことに注意してください)

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


3) 二期工事

誰かが思いつくであろうもう 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 フェーズ構成を使用すると、次のことができなくなります。

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

そしておそらく、今は考えられないいくつかの欠点があるかもしれません。上記の箇条書きがすでに私を納得させているので、特に義務付けられているとは感じていません.

そのため、ファクトリを実装するための優れた一般的なソリューションにはほど遠いものです。


結論:

次のようなオブジェクトのインスタンス化の方法が必要です。

  • 割り当てに関係なく均一なインスタンス化を可能にし、
  • 構築メソッドに異なる意味のある名前を付けます (したがって、引数によるオーバーロードに依存しません)。
  • 特にクライアント側で、重大なパフォーマンス ヒット、できれば重大なコード膨張ヒットを導入しない
  • のように、一般的である: どのクラスにも導入できる。

私が言及した方法がそれらの要件を満たさないことを証明したと思います。

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

4

11 に答える 11

114

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

この点は間違っていると思います。複雑さはそれほど重要ではありません。関連性は何をするかです。オブジェクトを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();

他の場合では、工場はあなたが言及した過負荷のような小さな問題を解決するのを助けるだけです。均一に使えるといいのですが、無理だと思っても大丈夫です。

于 2011-02-25T19:07:08.367 に答える
42

ファクトリをまったく使用せず、代わりに型システムをうまく利用することを考えたことはありますか? この種のことを行う 2 つの異なるアプローチを考えることができます。

オプション1:

struct linear {
    linear(float x, float y) : x_(x), y_(y){}
    float x_;
    float y_;
};

struct polar {
    polar(float angle, float magnitude) : angle_(angle),  magnitude_(magnitude) {}
    float angle_;
    float magnitude_;
};


struct Vec2 {
    explicit Vec2(const linear &l) { /* ... */ }
    explicit Vec2(const polar &p) { /* ... */ }
};

次のようなものを書くことができます:

Vec2 v(linear(1.0, 2.0));

オプション 2:

STLがイテレータなどで行うように、「タグ」を使用できます。例えば:

struct linear_coord_tag linear_coord {}; // declare type and a global
struct polar_coord_tag polar_coord {};

struct Vec2 {
    Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
    Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
};

この 2 番目のアプローチでは、次のようなコードを記述できます。

Vec2 v(1.0, 2.0, linear_coord);

これは、各コンストラクターに固有のプロトタイプを作成できると同時に、表現力も優れています。

于 2011-02-28T19:44:19.273 に答える
20

私は受け入れられた回答にほとんど同意しますが、既存の回答でカバーされていない C++11 オプションがあります。

  • ファクトリ メソッドの結果を valueで返します。
  • 安価なmove コンストラクターを提供します。

例:

struct sandwich {
  // Factory methods.
  static sandwich ham();
  static sandwich spam();
  // Move constructor.
  sandwich(sandwich &&);
  // etc.
};

次に、スタック上にオブジェクトを構築できます。

sandwich mine{sandwich::ham()};

他のもののサブオブジェクトとして:

auto lunch = std::make_pair(sandwich::spam(), apple{});

または動的に割り当てられます:

auto ptr = std::make_shared<sandwich>(sandwich::ham());

いつこれを使用できますか?

パブリック コンストラクターで、事前の計算なしにすべてのクラス メンバーに意味のある初期化子を与えることができない場合は、そのコンストラクターを静的メソッドに変換することがあります。静的メソッドは予備計算を実行し、メンバーごとの初期化だけを行うプライベート コンストラクターを介して値の結果を返します。

私が「可能性がある」と言ったのは、不必要に非効率になることなく、どのアプローチが最も明確なコードを提供するかにかかっているからです。

于 2015-09-07T10:32:58.530 に答える
11

Loki にはFactory MethodAbstract Factoryの両方があります。両方とも、Andei Alexandrescu によるModern C++ Designに(広範に) 文書化されています。ファクトリ メソッドはおそらくあなたが求めているものに近いですが、それでも少し異なります (少なくともメモリが機能する場合は、ファクトリがその型のオブジェクトを作成する前に型を登録する必要があります)。

于 2011-02-25T18:06:08.707 に答える
5

範囲が広すぎると思うので、すべての質問に答えようとしているわけではありません。いくつかのメモ:

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

そのクラスは、実際にはFactory ではなくBuilderです。

一般的なケースでは、ファクトリのユーザーを動的割り当てに制限することを強制したくありません。

次に、工場でそれをスマート ポインターにカプセル化することができます。このようにして、あなたもケーキを食べて食べることができると信じています。

これにより、値による戻りに​​関連する問題も解消されます。

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

それはそう。すべての設計パターンには、(言語固有の) 制約と欠点があります。それら自体のためではなく、問題の解決に役立つ場合にのみ使用することをお勧めします。

「完璧な」ファクトリの実装を望んでいるなら、幸運を祈ります。

于 2011-02-25T18:06:59.080 に答える
2

工場パターン

class Point
{
public:
  static Point Cartesian(double x, double y);
private:
};

また、コンパイラが戻り値の最適化をサポートしていない場合は、それを捨ててください。おそらく最適化はほとんど含まれていません...

于 2011-02-25T19:29:42.600 に答える
1

この質問は 3 年前に回答済みですが、これはあなたが探していたものかもしれません。

Google は数週間前に、簡単で柔軟な動的オブジェクト割り当てを可能にするライブラリをリリースしました。ここにあります:http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

于 2014-02-14T13:26:55.053 に答える