簡単に聞こえますが、正直に言うとどうすればいいのかわからないので、C ++には長い間不快に感じていたものが1つあります。
C ++でFactoryメソッドを正しく実装するにはどうすればよいですか?
目標:許容できない結果やパフォーマンスへの影響なしに、クライアントがオブジェクトのコンストラクタの代わりにファクトリメソッドを使用してオブジェクトをインスタンス化できるようにすること。
「ファクトリー・メソッド・パターン」とは、オブジェクト内の静的なファクトリー・メソッドまたは別のクラスで定義されたメソッド、またはグローバル関数の両方を意味します。一般的には、「クラス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 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);
}
};
わかった…そこにある。メソッド名を変更する必要があるため、醜いです。同じコードを2回書く必要があるため、これは不完全です。しかし、一度完了すると、機能します。正しい?
まあ、通常。しかし、時々それはしません。Fooを作成するとき、実際にコンパイラーに依存して戻り値の最適化を行っています。C++標準は、コンパイラーベンダーがオブジェクトをインプレースで作成するタイミングと、オブジェクトを返すときにコピーするタイミングを指定しないように十分に慈善的であるためです。 C ++の値による一時オブジェクト。したがって、Fooのコピーにコストがかかる場合、このアプローチは危険です。
また、Fooがまったくコピーできない場合はどうなりますか?まあ、そう。(コピーの省略が保証されたC ++ 17では、コピー不可であっても上記のコードでは問題ありません)
結論:オブジェクトを返すことでファクトリーを作成することは、確かにいくつかのケース(前述の2Dベクトルなど)の解決策ですが、それでもコンストラクターの一般的な置き換えではありません。
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
メンバー変数の初期化または参照、- 基本クラスコンストラクターとメンバーオブジェクトコンストラクターに引数を渡します。
そして、おそらく今は考えられないいくつかの欠点があるかもしれませんし、上記の箇条書きがすでに私を納得させているので、特に義務付けられているとさえ感じていません。
したがって、ファクトリを実装するための優れた一般的なソリューションにさえ近づきません。
結論:
次のようなオブジェクトのインスタンス化の方法が必要です。
- 割り当てに関係なく、均一なインスタンス化を可能にし、
- 構築メソッドに異なる意味のある名前を付けます(したがって、引数によるオーバーロードに依存しません)。
- 特にクライアント側で、重大なパフォーマンスヒット、およびできれば、重要なコードブロートヒットを導入しない
- 次のように一般的である:どのクラスにも導入できる。
私が述べた方法はそれらの要件を満たしていないことを私は証明したと信じています。
ヒントはありますか?私に解決策を提供してください、この言語では私がそのような些細な概念を適切に実装することができないとは思わないでください。
delete
。これらの種類のメソッドは、呼び出し元がポインターの所有権を取得することが「ドキュメント化」されている限り(ソースコードはドキュメント;-)完全に問題ありません(読み取り:必要に応じて削除する責任があります)。
unique_ptr<T>
では、の代わりにを返すことで、それを非常に明示的にすることもできますT*
。