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
メンバー変数の初期化または参照、- 基本クラスのコンストラクターとメンバー オブジェクトのコンストラクターに引数を渡します。
そしておそらく、今は考えられないいくつかの欠点があるかもしれません。上記の箇条書きがすでに私を納得させているので、特に義務付けられているとは感じていません.
そのため、ファクトリを実装するための優れた一般的なソリューションにはほど遠いものです。
結論:
次のようなオブジェクトのインスタンス化の方法が必要です。
- 割り当てに関係なく均一なインスタンス化を可能にし、
- 構築メソッドに異なる意味のある名前を付けます (したがって、引数によるオーバーロードに依存しません)。
- 特にクライアント側で、重大なパフォーマンス ヒット、できれば重大なコード膨張ヒットを導入しない
- のように、一般的である: どのクラスにも導入できる。
私が言及した方法がそれらの要件を満たさないことを証明したと思います。
ヒントはありますか?解決策を教えてください。この言語では、このような些細な概念を適切に実装できないとは思いたくありません。