1. ホーム
  2. c++

[解決済み】C++でファクトリーメソッドパターンを正しく実装する方法

2022-04-28 01:48:11

質問

C++で、ずっと気になっていたことがあるんですが、簡単そうなのに正直やり方がわかりません。

C++でファクトリーメソッドを正しく実装するには?

目標: クライアントがオブジェクトのコンストラクタの代わりにファクトリーメソッドを使用して、あるオブジェクトをインスタンス化できるようにすることです。

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

私が考えた答えの候補をざっと挙げてみます。


0) ファクトリーを作らず、コンストラクタを作れ。

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

私が知っている最もシンプルな例は、2次元のVectorクラスです。とてもシンプルでありながら、厄介だ。Cartesian座標とPolar座標の両方から構築できるようにしたいのです。明らかに、私はできません。

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);
    // ...
};

つまり、コンストラクタの代わりに静的なファクトリーメソッドを使うことになります。これは見た目はいいのですが(そしてこの特定のケースに適しています)、いくつかのケースで失敗します。続きを読んでください。

別のケースとして、ある API の 2 つの不透明な型定義(無関係なドメインの GUID や GUID とビットフィールドなど)でオーバーロードしようとした場合、意味的にはまったく異なる型(つまり理論的には有効なオーバーロード)ですが、実際には同じものであることが判明しました(符号なし 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回書かなければならないので、不完全です。しかし、一旦完成すれば、うまくいくのです。そうでしょ?

まあ、たいていはね。しかし、そうでない場合もあります。なぜなら、C++の標準規格は、C++で一時的なオブジェクトを値で返すときに、いつインプレースで作成され、いつコピーされるかをコンパイラベンダーが指定しないほど慈悲深いものだからです。ですから、もしFooがコピーするのに高価であれば、この方法は危険です。

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

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


3) 二段階構造

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

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 またはメンバ変数を参照します。
  • ベースクラスコンストラクタとメンバーオブジェクトコンストラクタに引数を渡します。

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

つまり、ファクトリーを実装するための一般的な解決策にはほど遠いということです。


結論

オブジェクトのインスタンス化の方法として、以下のようなものが欲しい。

  • は、アロケーションに関係なく均一なインスタンス化を可能にします。
  • コンストラクションメソッドに異なる意味のある名前を与える(したがって、引数によるオーバーロードに依存しない)。
  • 特にクライアント側で、大幅なパフォーマンスの低下と、できれば大幅なコードの肥大化を招かないこと。
  • 一般的であること。つまり、どのクラスにも導入可能であること。

私が挙げた方法は、これらの要件を満たしていないことを証明したつもりです。

何かヒントはありますか?この言語ではこのような些細なコンセプトを適切に実装することができないと思いたくないので、解決策を提示してください。

どのように解決するのですか?

<ブロッククオート

まず第一に、以下のような場合があります。 オブジェクトの構築は複雑なタスクである に抽出することが正当化されるほど 別のクラスです。

この指摘は間違っていると思います。複雑さはあまり重要ではありません。重要なのは関連性です。もしオブジェクトが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();

他のケースでは、ファクトリーは、あなたが言及したオーバーロードのような小さな問題の解決に役立つだけです。統一された方法で使用することができればいいのですが、おそらく不可能であることはあまり痛手ではありません。