人々がOOPについて学ぶとき、私は次のパターンを見続けます。
問題:異なるが関連するタイプのオブジェクトをコンテナに入れるにはどうすればよいですか?
解決策:共通の基本クラスから継承します。
新しい問題:基本クラスのオブジェクトがたくさんありますが、実際の型を取得するにはどうすればよいですか?
解決: ???
古典的な継承の例は、幾何学的オブジェクトを処理するグラフィックプログラムを作成することです。 Circle
s、Ellipse
s、およびTriangle
sがあります。これらのオブジェクトはすべて、共通ベースDrawable
を持っています。
C++では、これは次のようになります。
struct Drawable{
virtual ~Drawable(){}
virtual void draw() const = 0;
};
struct Triangle : Drawable{
Point p1, p2, p3;
void draw() const override;
void stretch(double xfactor, double yfactor);
std::touple<double, double, double> getAngles() const;
//...
};
struct Circle : Drawable{
double radius;
void draw() const override;
//...
};
struct Ellipse : Drawable{
double radius;
void draw() const override;
//...
};
std::vector<Drawable *> drawables = {new Circle, new Triangle, new Ellipse};
for (const auto &d : drawables)
d->draw();
ここまでは順調ですね。ここで、Circle
sとEllipse
sの半径を大きくしたいと思います。私が知る限り、それを行うには4つの方法があります。
1:すべてを基本クラスに入れます。基本クラスは、仮想関数getAngles
、getRadius
、stretch
、およびgetEccentricity
を取得します。それぞれにデフォルトの実装があり、たとえばgetRadius
を処理しているときに、Triangle
がダミー値を返すようになっています。これが行われた後、Drawable
sを簡単に反復して、それらの半径を増やすことができます。これは、基本クラスを途方もなく巨大にし、vector<Drawable *>
での作業は、ほとんどの要素に対して関数が何もしないため非効率になります。
2:dynamic_cast
ハックを使用します。
for (const auto &d : drawables){
if (dynamic_cast<Circle *>(d))
((Circle *)d)->radius++;
else if (dynamic_cast<Ellipse *>(d))
((Ellipse *)d)->radius++;
}
残念ながら、このようなことをする必要があるたびに、この醜い構文を繰り返す必要があります。関数やマクロに入れることはできません。新しいDrawable
が追加されるたびに、コードベース全体を調べて、必要に応じて追加する必要があります。また、if
/else
- chainはかなり非効率的です。
3:最初のメンバー変数としてタグを追加し、それに応じてキャストします。
for (const auto &d : drawables){
switch(d->tag){
case Tag::Circle:
((Circle *)d)->radius++;
break;
case Tag::Ellipse:
((Ellipse *)d)->radius++;
break;
}
}
if
/else
-chainの代わりにスイッチがあるため、少し効率的ですが、動的タイプを示す一種のタグがすでに存在するため、タグは冗長です。また、2と同じ問題があります。新しいDrawable
が来るたびにコードベース全体を変更する必要があるためです。
4:オブジェクトを別々のコンテナに保管するだけです。
vector<Circle> circles;
vector<Triangle> triangles;
vector<Ellipse> ellipses;
for (auto &c : circles)
c.radius++;
for (auto &e : ellipses)
e.radius++;
これは非常に効率的なメモリレイアウトであり、重要なオブジェクトのみを反復処理します。これは私のお気に入りですが、残念ながら、異なるタイプのオブジェクトを1つのコンテナーに保持しないという点で、最初の解決策に失敗します。
タイプごとに内部的にvector
を持ち、挿入を適切なコンテナに転送する可変個引数テンプレートを作成することで、問題を解決しようとしました。理想的には、フードの下にさまざまなコンテナがあるという事実を完全に隠すでしょう。使用法は次のようになります。
MultiVector<Triangle, Circle, Ellipse> mv;
mv.Push_back(Triangle());
mv.Push_back(Circle());
mv.Push_back(Ellipse());
for (auto &d : mv.get<Circle, Ellipse>())
d.radius++;
解決策は完全ではありません。たとえば、異なるタイプは順序を維持せず、反復の実装には注意が必要です。
私はこの問題と解決策の試みを私の(大学の)研究グループに提示し、「クラスを正しく設計すればこれは問題ではない」と「提案されたソリューションは、非常に特定の問題を解決するための非常に特定のツールであり、そのような細かい詳細のためにc ++にさらに複雑さを追加することはせいぜい疑わしいです "。私の意見では、これは修正が必要な非常に基本的な問題であり、より良いクラス設計の通常の解決策は、実際に問題を解決することなく、特定のケースで4つの回避策の中で最も悪いものを見つけようとするだけです。この問題は、継承が使用されるたびに発生します。
それで最後にここに質問があります:あなたもこの問題に遭遇しましたか/これは本当の問題ですか?別の回避策/解決策を知っていますか?他の言語は問題にうまく対処しますか?以下の2つのコメントを使用して投票をシミュレートしたいので、「この問題は発生していません/問題は簡単に解決します。」または「私もこの問題を抱えていて、満足のいく解決には至りませんでした。」
編集:主に意見に基づく/議論によるクローズを防ぐために:問題を解決する明確な答え、または何もないと言うリソースが必要です。
編集:関連: 私は本当に私のガレージに車を持っていますか?
これは古典的なOOPの問題であり、オブジェクト指向の考え方に問題があることを示しているわけではありませんが、設計を慎重に検討する必要があります。
オブジェクトのコンテナを反復処理し、オブジェクトの一部のみが持つプロパティを変更したい場合、さらに重要なのは、オブジェクトの一部に対してのみ意味があります、それは私の意見です問題となるデザイン。ここでの問題は、なぜジェネリックリストのクラス固有のプロパティを変更したいのかということです。あなたの状況ではそれは理にかなっていると思いますが、しばらく考えてみてください。
図形のリストがあります。これは、正方形、長方形、三角形、円、多角形などです。次に、それらすべてがアクションをサポートしている場合は、それらに対して何らかのアクションを実行する必要があります。しかし、明らかにサポートされていないオブジェクトのプロパティを変更しても意味がありません。図のリストを繰り返して半径を設定するのは直感に反しますが、notですが、円のリストを繰り返して半径を設定するのは直感に反します。
これは、いくつかの複雑な、または単にスマートなデザインの選択と言えば、これを実現できるという意味ではありませんが、何らかの方法でカプセル化を回避しようとすることにより、オブジェクト指向の概念に反します。これは多態性の考え方に反すると主張します。
しかし、代わりにそれを行う方法は何か違うものであり、現時点では答えを出すことができないのではないかと思います。しかし、私はあなたがそのような状況に身を置くことを避けるように最善を尽くすべきだと信じています。
調査できる代替案の1つは、半径の変更を許可するオブジェクトタイプを受け入れるビジターがいるビジターパターンを使用することです。例えば:
class Visitor {
public:
void Visit(Circle& circle) { /* Do something circle specific */ )
void Visit(Square& square) { /* Do something square specific */ )
// ...
};
Problem: How do I put objects of different but related types into a container?
Solution: Inherit from a common base class.
これが問題です。デザインで個別のタイプが必要な場合は、コンテナーに入れるためにそれらをマージする必要はありません。共通の基本クラスの下でそれらをすべてマージした場合、誰もあなたを面白く見ない例を選択しましたが、タイプの共通性が低いか、プリミティブ/スカラータイプであるか、サードパーティである場合、このソリューションは明らかに機能しませんあなたがコントロールしないタイプ。
必要なのは タグ付き共用体 です。 C/C++にはこれらがありませんが、タグ付けできるunionがあり、自分でタグ付けできます。
enum ShapeType { TRIANGLE, CIRCLE, ELLIPSE };
struct ShapeUnion {
ShapeType type;
union {
Triangle triangle;
Circle circle;
Ellipse ellipse;
} data;
};
列挙型をチェックすることにより、アクセスするユニオンのメンバーを決定できます。
std::vector<ShapeUnion> shapes = getShapes();
for (auto& shape: shapes) {
switch (shape.type) {
case TRIANGLE: foo(shape.data.triangle); break;
case CIRCLE: bar(shape.data.circle); break;
case ELLIPSE: baz(shape.data.ellipse); break;
default: // raise some sort of error
}
}
ちょっとググれば、このテンプレートが見つかると思います。
免責事項:C/C++をあまり記述していません。上記のコードには構文エラーが含まれている可能性があります。
だからここに問題があります:古典的な継承は本当にすべて何ですか?分類法!厳密な階層など。階層は、タイプの共通性に基づいて機能します。だから今、あなたは「違うものを一緒にグループ化したい」と言おうとしているのです。わかりました、あなたはそれをすることができます。しかし、それらは同じタイプではないため、類似性に基づく問題解決パターンは、それらがグループとして動作するのにうまく機能しない可能性があります。それらが本当に類似している場合は、基本クラスを作成することで抽象的な方法でグループとして機能でき、型をオンにする必要はありません。それらがまったく類似していない場合は、タイプをオンにせずにそれらに対処することはできません。そして実際に、それに直面しましょう:ポリモーフィズムは本当にこれを行っていますどの実装が内部で呼び出されるかを切り替えることによって、それは奇妙な方法でそれもswitchステートメントのようです-ちょうどより速くそしてよりエレガントです。
したがって、このように考えるときは、スイッチをクラス自体(古典的な継承、おそらく奇妙な「DoStuff()」または「Process()」メソッドを使用)または作用するもののいずれかに配置する必要がありますクラス(ビジターパターンまたはスイッチオンタイプ)またはリストの分離(ソリューション#4)。ビジター/スイッチオンタイプの利点は、基本クラスを混乱させないため、モジュラーコードに適していることです。
さて、OOPは街の唯一のパラダイムではありません。ゲームデザイナーは、厳密な階層を形成せず、特定の「属性」を持っているオブジェクトについて考える必要があります。コンポーネントのデザインを見つけるかもしれません。またはこのタイプの問題に頻繁に遭遇する場合に興味深い関連バリアント。
私は個人的にダウンキャストを使用して完全に元気です。言語支援 ビジターパターン と同じだからです。 HasRadius
インターフェース/抽象クラスを作成すると、この変更を許可するクラスを指定できるため、さらに優れたものになります。これにより、潜在的に新しいDrawable
に追加でき、ifチェーンが削除されます。また、型の変更は型システムとコンパイラによって正しく行われることが保証されるため、このソリューションに関する不満の一部は、実際の訪問者を実装することで解決できます。
そして注意すべき1つのこと:あなたは効率についてたくさん話します。プログラミングはトレードオフに関するものであり、抽象化(特にオープン/クローズドプロパティをデザインに追加するもの)は常に効率に直接反します。効率が必要ないと言っているのではありません。 OOPを使い始めるときは、パフォーマンスへの影響を受け入れることができなければなりません。そして忘れないでください:コードをプロファイリングした後、パフォーマンスの最適化を残す必要があります。このような例でパフォーマンスを考えると、結果が極端に歪められます。
あなたは尋ねました:
あなたもこの問題に遭遇しましたか/これは本当の問題ですか?別の回避策/解決策を知っていますか?
それに対する私の答えは、大きなYESです。私は仕事でこれをたくさん扱っています。
あなたが言った、
次に、円と楕円の半径を大きくします。
その要件の詳細を掘り下げる際のソリューションラインの鍵。コードに「I」はありません。コードのどの部分でその操作を実行する必要があるかを特定する必要があります。コードのそのセクションは、円と楕円について知っている必要があります。コードのそのセクションが円と楕円について知っている場合、それらは基本クラスのポインターまたは参照を取得してdynamic_cast
を実行できます。 dynamic_cast
が成功すると、メンバー関数を直接呼び出すことができます。 dynamic_cast
が成功しない場合、そのオブジェクトを無視するか、例外をスローするか、または他の方法で処理することができます。
一般的なアプローチ
文字列ベースのキーと一般的な値を使用して、一般的なプロパティを扱いました。たとえば、CirclesとEllipsesに関連付けられた一般的なプロパティハンドラーは、オブジェクトの「半径」プロパティを処理する方法を知っていることを示す自身を登録します。汎用ディスパッチメカニズムは、Drawableの「半径」を設定する必要があるときに、そのプロパティハンドラーを呼び出します。
疑似コードでは、次のようになります。
void setProperty(Drawable& dr, std::string const& key, std::string const& value)
{
PropertyHandler* handler = getPropertyHandler(key);
if ( handler != NULL )
{
handler.setProperty(dr, key, value);
}
}