このstackexchangeをすばやく検索すると、一般的に構成は継承よりも柔軟であると一般に考えられていますが、常にプロジェクトなどに依存し、継承の方が適している場合があります。各ピースにメッシュがあり、場合によっては異なるアニメーションが含まれる3Dチェスゲームを作成したいと思います。この具体的な例では、両方のアプローチのケースが間違っていると主張できるように思われますか?
継承は次のようになります(適切なコンストラクターなどを使用)
class BasePiece
{
virtual Squares GetValidMoveSquares() = 0;
Mesh* mesh;
// Other fields
}
class Pawn : public BasePiece
{
Squares GetValidMoveSquares() override;
}
これは確かに「is-a」の原則に従いますが、構成は次のようになります。
class MovementComponent
{
virtual Squares GetValidMoveSquares() = 0;
}
class PawnMovementComponent
{
Squares GetValidMoveSquares() override;
}
enum class Type
{
PAWN,
BISHOP, //etc
}
class Piece
{
MovementComponent* movementComponent;
MeshComponent* mesh;
Type type;
// Other fields
}
それは個人的な好みの問題なのでしょうか、それともここでの1つのアプローチが他のアプローチより明らかに賢い選択なのでしょうか?
編集:私はすべての答えから何かを学んだと思うので、1つだけを選ぶのは気分が悪いです。私の最終的な解決策は、ここのいくつかの投稿からインスピレーションを得ます(まだ取り組んでいます)。回答に時間を割いてくださった皆さんに感謝します。
一見すると、あなたの答えは「構成」ソリューションが継承を使用していないように見せかけています。しかし、これを追加するのを忘れただけだと思います。
_class PawnMovementComponent : MovementComponent
{
Squares GetValidMoveSquares() override;
}
_
(およびこれらの派生のより多く、6ピースタイプのそれぞれに1つ)。これは従来の「戦略」パターンに似ており、継承も利用しています。
残念ながら、このソリューションには欠点があります。それぞれのPiece
は、型情報を重複して2回保持しています。
メンバー変数type
内
内部movementComponent
(サブタイプによって表現される)
この冗長性は本当に問題を引き起こす可能性があります-Piece
のような単純なクラスは、2つのソースではなく「単一の真のソース」を提供する必要があります。
もちろん、型情報をtype
にのみ保存し、MovementComponent
の子クラスも作成しないようにすることもできます。しかし、この設計では、おそらくGetValidMoveSquares
の実装で巨大な "switch(type)
"ステートメントが発生します。そして、それは間違いなくここで継承がより良い選択であることを強く示しています。
「継承」設計では、「type」フィールドを非冗長な方法で提供するのは非常に簡単です。仮想メソッドGetType()
をBasePiece
に追加し、それぞれに適宜実装します。基本クラス。
「プロモーション」について:私は@svidgenと一緒にここにいます、- @ TheCatWhisperer's answer で提示された議論は議論の余地があります。
「プロモーション」を同じ作品のタイプの変化として解釈するのではなく、作品の物理的な交換として解釈することは、私にとってかなり自然なことです。したがって、これを同様の方法で実装する-あるタイプを別のタイプの別のものと交換することにより-おそらくチェスの特定のケースでは、大きな問題を引き起こさないでしょう。
私はおそらく より単純なオプションを好む 明確で明確な理由がない限り、またはstrong拡張性とメンテナンスの懸念がない限り。
継承オプションは、コード行が少なく、複雑さが少ないことです。ピースの各タイプには、その移動特性と「不変」の1対1の関係があります。 (1対1の表現とは異なりますが、必ずしも不変ではありません。さまざまな「スキン」を提供したい場合があります。)
この不変の1対1の関係と、それらの動作/動きを複数のクラスに分割する場合、あなたはおそらくonly追加しています複雑さ —他にはあまりありません。そのため、そのコードを確認または継承している場合は、文書化された複雑な理由が文書化されていることを期待しています。
つまり、さらに簡単なオプションであるIMOは、Piece
interfaceを作成することです。ピース実装。ほとんどの言語では、これは実際にはveryinheritanceとは異なります。これは、インターフェイスが実装を制限しないためですanother interface。 ...その場合、無料で基本クラスの動作を取得しないだけです。共有動作を別の場所に配置する必要があります。
チェスで重要なのは、ゲームのプレイとピースのルールが規定され、固定されていることです。機能するデザインはすべて問題ありません。好きなものを使用してください。いろいろ試してみてください。
ただし、ビジネスの世界では、このように厳密に規定されているものはありません。ビジネスルールと要件は時間とともに変化し、プログラムはそれに対応するために時間とともに変化する必要があります。これがis-aとhas-aのデータの違いです。ここでは、単純さにより、複雑なソリューションを長期にわたって維持し、要件を変更することが容易になります。一般に、ビジネスでは、永続化にも対処する必要があります。これには、データベースも含まれる場合があります。したがって、単一のクラスが機能する場合は複数のクラスを使用しない、構成が十分な場合は継承を使用しないなどのルールがあります。しかし、これらのルールはすべて、ルールと要件の変更に直面してもコードを長期にわたって保守可能にすることを目的としています。これはチェスの場合とは異なります。
チェスを使用する場合、長期的なメンテナンスパスとして最も可能性が高いのは、プログラムをよりスマートにする必要があることです。つまり、最終的には速度とストレージの最適化が優先されます。そのためには、一般にパフォーマンスと読みやすさを犠牲にするトレードオフを行う必要があるため、最高のOOデザインでも最終的には道を譲ります。
私は問題のあるドメイン(つまり、チェスのルール)についていくつかの観察を行うことから始めます。
これらは作品自体の責任としてモデル化するのは厄介であり、継承も合成もここでは素晴らしく感じません。これらのルールを一流の市民としてモデル化するほうが自然です。
// pseudo-code, not pure C++
interface MovementRule {
set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}
class Game {
Board board;
map<PieceType, MovementRule> rules;
}
MovementRule
は、キャスティングやプロモーションなどの複雑な動きをサポートするために必要な方法で実装がボードの状態を更新できるようにするシンプルで柔軟なインターフェイスです。標準のチェスの場合、ピースのタイプごとに1つの実装がありますが、Game
インスタンスに異なるルールをプラグインして、チェスのさまざまなバリアントをサポートすることもできます。
この場合、継承がより適切な選択になると思います。構成はもう少しエレガントかもしれませんが、私にはもう少し強制されているようです。
動作が異なる可動ピースを使用する他のゲームの開発を計画している場合は、特にファクトリーパターンを使用して各ゲームに必要なピースを生成する場合は、コンポジションがより適切な選択になる場合があります。
これは確かに「is-a」の原則に従います
そう、あなたは継承を使います。あなたはまだあなたのPieceクラス内で構成することができますが、少なくともバックボーンがあります。構成はより柔軟ですが、一般に柔軟性が過大評価されています。確かに、この場合は確かに、剛体モデルを放棄する必要がある新しい種類の作品を誰かが紹介する可能性はゼロです。チェスのゲームはすぐには変わりません。継承は、指導モデル、関連する何か、教育のコード、方向性を提供します。構成はそれほど多くありませんが、最も重要なドメインエンティティは目立ちません。
ここでは継承を使用しないでください。他の答えには確かに多くの知恵がありますが、ここでの継承の使用は間違いです。 compositionを使用すると、予期しない問題への対処が容易になるだけでなく、継承では正常に処理できないという問題がすでにあります。
ポーンがより価値のあるピースに変換されると、プロモーションは継承の問題になる可能性があります。技術的には、1つの部品を別の部品に置き換えることでこれに対処できますが、このアプローチには制限があります。これらの制限の1つは、昇格時にピース情報をリセットしない(またはそれらをコピーするための追加コードを記述する)必要がない場合があるピースごとの統計の追跡です。
さて、あなたの質問におけるあなたの構成デザインに関しては、それは不必要に複雑であると思います。アクションごとに個別のコンポーネントが必要であり、ピースタイプごとに1つのクラスに固執する可能性があるとは思いません。タイプenumも不要な場合があります。
(すでにあるように)Piece
クラスとPieceType
クラスを定義します。 PieceType
クラスは、ポーン、クイーン、ectが定義する必要があるすべてのメソッドを定義します。たとえば、CanMoveTo
、GetPieceTypeName
、ectなどです。次に、Pawn
およびQueen
、ectクラスはPieceType
クラスから仮想的に継承します。
私の見解では、提供された情報を使用して、継承と構成のどちらを選択すべきかを答えることは賢明ではありません。
モデルはまったく異なります。最初のモデルはチェスの駒の概念を中心にモデル化し、2番目のモデルは駒の動きの概念を中心にモデル化しました。それらは機能的に同等である可能性があり、ドメインをよりよく表し、ドメインについてより簡単に推論するのに役立つものを選択します。
また、2番目のデザインでは、Piece
クラスにtype
フィールドがあるという事実から、ピース自体が複数のタイプになる可能性があるため、ここにデザインの問題があることが明らかになります。これはおそらく、ある種の継承を使用する必要があるように聞こえませんか?ピース自体がタイプです。
つまり、ソリューションについて議論することは非常に困難です。重要なのは、継承と構成のどちらを使用したかではなく、モデルが問題の領域を正確に反映しているかどうか、そしてそれを推論して実装を提供することが役立つかどうかです。
継承と構成を適切に使用するにはある程度の経験が必要ですが、それらはまったく異なるツールであり、構成のみを使用してシステムを完全に設計できることには同意できますが、一方を他方に「置き換える」ことができるとは思いません。