チェスの駒の継承と構成


9

この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つだけを選ぶのは気分が悪いです。私の最終的な解決策は、ここのいくつかの投稿からインスピレーションを得ます(まだ取り組んでいます)。回答に時間を割いてくださった皆さんに感謝します。


私は両方が並んで存在することができると思います、これら2つは異なる目的のためですが、これらは互いに排他的ではありません。チェスは異なるピースとボードで構成され、ピースは異なるタイプです。これらの部分はいくつかの基本的なプロパティと動作を共有し、特定のプロパティと動作も持っています。したがって、私によると、コンポジションはボードとピースに適用する必要があり、継承はピースのタイプに適用する必要があります。
Hitesh Gaur

すべての継承が悪いと主張する人もいるかもしれませんが、一般的な考え方は、インターフェイスの継承は良い/細かいことであり、実装の継承は有用ですが問題がある可能性があるということです。私の経験では、単一レベルの継承を超えるものには疑問があります。それ以上は問題ないかもしれませんが、複雑な混乱を起こさずに引き抜くことは簡単ではありません。
JimmyJames

戦略パターンは、理由があるのでゲームプログラミングでは一般的です。var Pawn = new Piece(howIMove, howITake, whatILookLike)私には、継承階層よりもはるかに単純で、管理しやすく、保守しやすいようです。
Ant P

@AntPそれは良い点です、ありがとう!
CanISleepまだ2019年

@AntPそれを答えにしてください!...それには多くのメリットがあります!
svidgen

回答:


4

一見すると、あなたの答えは「構成」ソリューションが継承を使用していないように見せかけています。しかし、これを追加するのを忘れただけだと思います。

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(およびこれらの派生のより多く、6ピースタイプのそれぞれに1つ)。これは、従来の「戦略」パターンに似ており、継承も利用しています。

残念ながら、このソリューションには欠点があります。それぞれにPieceタイプ情報が重複して2回保持されています。

  • メンバー変数内 type

  • 内部movementComponent(サブタイプで表現)

この冗長性は本当に問題を引き起こす可能性があります-のような単純なクラスは、Piece2つのソースではなく「単一の真のソース」を提供する必要があります。

もちろん、型情報をにのみ保存して、そのtype子クラスも作成しないMovementComponentようにすることもできます。しかし、この設計はおそらくswitch(type)、の実装において巨大な「」ステートメントにつながるでしょうGetValidMoveSquares。そして、それは間違いなく継承がここでより良い選択であることを強く示しています

「継承」設計では、「type」フィールドを非冗長な方法で提供するのは非常に簡単です。仮想メソッドGetType()を追加し、BasePieceそれに応じて各基本クラスに実装します。

「プロモーション」について:私は@svidgenと一緒にここにいますが、@ TheCatWhispererの回答で提示された議論は議論の余地があります。

「プロモーション」を、同じ作品のタイプの変化として解釈するのではなく、作品の物理的な交換として解釈することは、私にとってかなり自然なことです。したがって、これを同様の方法で実装する-あるタイプを別のタイプの別のものと交換することによって-おそらくチェスの特定のケースでは、大きな問題を引き起こさないでしょう。


パターンの名前を教えてくれてありがとう、それが戦略と呼ばれていることに気づきませんでした。私の質問では、運動コンポーネントの継承を逃しました。タイプは本当に冗長ですか?ボード上のすべてのクイーンを照らしたい場合、それらが持っている動作コンポーネントを確認するのは奇妙に思えます。さらに、動作コンポーネントをキャストして、それがどのタイプであるかを確認する必要があります。
CanISleepまだ2019年

@CanISleepYet:提案された「タイプ」フィールドの問題は、のサブタイプと異なる可能性があることですmovementComponent。"継承"設計でタイプフィールドを安全な方法で提供する方法については、私の編集を参照してください。
Doc Brown、

4

私はおそらくしたいシンプルなオプションを好むあなたは、明示的な、よく連結式の理由か持っていない限り、強力な拡張性とメンテナンスの懸念を。

継承オプションは、コード行が少なく、複雑さが少ないことです。ピースの各タイプには、その移動特性と「不変」の1対1の関係があります。(1対1の表現とは異なりますが、必ずしも不変ではありません。さまざまな「スキン」を提供したい場合があります。)

あなたが複数のクラスに作品とその動作/運動の間に、この不変の1対1の関係を壊した場合、あなたはしている、おそらく唯一の複雑さを加えること -とあまり他。そのため、そのコードをレビューまたは継承している場合、文書化された複雑な理由が文書化されているのがよくわかります。

そうは言ってさらに簡単なオプションであるIMOは、個々のピースが実装するPiece インターフェースを作成することです。ほとんどの言語では、これは継承とは非常に異なります。これは、インターフェイスが別のインターフェイスの実装を制限しないためです。...その場合、無料で基本クラスの動作を取得しないだけです。共有動作を別の場所に配置する必要があります。


継承ソリューションが「少ないコード行」であるとは思いません。おそらく、この1つのクラスではありますが、全体ではありません。
JimmyJames

@JimmyJames ほんと?... OPのコード行を数えるだけです...確かにソリューション全体ではありませんが、LOCと維持が複雑なソリューションがどれであるかを示す悪い指標ではありません。
svidgen

この時点ではすべて概念的なものなので、あまり細かく言いすぎないようにしますが、そうだと思います。アプリケーション全体と比較すると、このコードはごくわずかであるというのが私の主張です。これによりルールの実装が簡素化される場合(議論の余地があります)、コードの数十行または数百行を節約できる可能性があります。
JimmyJames

おかげで、インターフェースについては考えていませんでしたが、これが最善の方法だと思うかもしれません。ほとんどの場合、シンプルな方が優れています。
CanISleepまだ

2

チェスの特徴は、ゲームのプレイとピースのルールが規定され、固定されていることです。機能するデザインはすべて問題ありません。好きなように使用してください。いろいろ試してみてください。

しかし、ビジネスの世界では、このように厳密に規定されているものはありません。ビジネスルールと要件は時間とともに変化し、プログラムはそれに対応するために時間とともに変化する必要があります。これが、is-aとhas-aのデータの違いです。ここでは、単純さにより、複雑なソリューションを長期にわたって維持し、要件を変更することが容易になります。一般に、ビジネスでは、永続化にも対処する必要があります。これには、データベースも含まれる場合があります。したがって、単一のクラスが機能する場合は複数のクラスを使用しない、構成が十分な場合は継承を使用しないなどのルールがあります。しかし、これらのルールはすべて、ルールと要件の変更に直面してコードを長期にわたって保守可能にすることを目的としています。これはチェスの場合とは異なります。

チェスを使用する場合、長期的なメンテナンスパスとして最も可能性が高いのは、プログラムをよりスマートにする必要があることです。つまり、最終的には速度とストレージの最適化が優先されます。そのためには、一般に、パフォーマンスの読みやすさを犠牲にするトレードオフを行う必要があるため、最高のOOデザインでさえ、最終的には道を譲ってしまいます。


コンセプトを取り入れ、いくつかの新しいものをミックスに投入する成功したゲームは数多くあることに注意してください。将来チェスゲームでこれを行う場合は、実際に起こり得る将来の変更を考慮する必要があります。
Flater

2

まず、問題のある領域(つまり、チェスのルール)について観察します。

  • 駒の有効な動きのセットは、駒のタイプだけでなくボードの状態にも依存します(駒が空の場合、ポーンは前方に移動できます/駒をキャプチャすると斜めに移動できます)。
  • 特定のアクションには、既存のピースをプレイから削除する(プロモーション/ピースをキャプチャする)または複数のピースを移動する(キャスティング)が含まれます。

これらは作品自体の責任としてモデル化するのは厄介であり、継承も合成もここでは素晴らしく感じません。これらのルールを一流の市民としてモデル化するほうが自然です。

// 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インスタンスにさまざまなルールをプラグインして、さまざまな種類のチェスをサポートすることもできます。


興味深いアプローチ-考えられる良い食べ物
CanISleepYet

1

この場合、継承がより適切な選択になると思います。構成はもう少しエレガントかもしれませんが、私にはもう少し強制されているようです。

動作が異なる可動ピースを使用する他のゲームを開発する予定である場合、特に、各ゲームに必要なピースを生成するためにファクトリーパターンを採用している場合は、コンポジションがより適切な選択になる場合があります。


2
継承を使用したプロモーションにどのように対処しますか?
TheCatWhisperer

4
@TheCatWhisperer 物理的にゲームをプレイしているときに、どのように処理しますか?(うまくいけば、駒を交換します -ポーンを再彫刻しないのですか?)
svidgen

0

確かに「is-a」の原則に従います

そう、継承を使用します。あなたはまだあなたのPieceクラス内で構成することができますが、少なくともバックボーンがあります。構成はより柔軟ですが、一般に柔軟性が過大評価されています。確かに、この場合は、剛体モデルを放棄する必要がある新しい種類の作品を誰かが紹介する可能性がゼロであるためです。チェスのゲームはすぐには変わりません。継承はあなたにガイドモデル、関連する何か、コード、方向性を教えます。構成はそれほど多くありませんが、最も重要なドメインエンティティは目立ちません。


構成ではコードについて
推論

0

ここでは継承を使用しないでください。他の答えには確かに多くの知恵がありますが、ここでの継承の使用は間違いです。コンポジションを使用すると、予期しない問題への対処に役立つだけでなく、継承では適切に処理できないという問題がすでにあります。

ポーンがより価値のあるピースに変換されると、プロモーションは継承の問題になる可能性があります。技術的には、1つの部品を別の部品に置き換えることでこれに対処できますが、このアプローチには制限があります。これらの制限の1つは、昇格時にピース情報をリセットしたくない(またはコピーするための追加のコードを記述したくない)場合のピースごとの統計の追跡です。

さて、あなたの質問におけるあなたの構成デザインに関しては、それは不必要に複雑であると思います。アクションごとに個別のコンポーネントが必要であり、ピースタイプごとに1つのクラスに固執する可能性があるとは思いません。タイプenumも不要な場合があります。

私はPieceクラスを定義します(既に持っているように)だけでなく、PieceTypeクラスも。PieceTypeポーン、クイーン全ての方法、ECTクラス定義は、例えば、のように、定義する必要がありCanMoveToGetPieceTypeName電気ショック療法、。次に、PawnおよびQueenectクラスは、PieceTypeクラスから仮想的に継承します。


3
昇格と置換で発生する問題は些細なことです、IMO。懸念の分離は、そのような「部分ごとの追跡」(または何でも)がとにかく独立し処理されることを要求します。...ソリューションが提供する潜在的な利点は、IMOが対処しようとしている架空の懸念に負けています。
svidgen

5
明確にするために:提案されたソリューションには間違いなくいくつかのメリットがあります。しかし、「継承ソリューション」で本当に本当に簡単に解決できる問題に主に焦点を当てた答えでは、それらを見つけるのは困難です。...そのようなことは、あなたが自分の意見について伝えようとしているあらゆるメリットを(とにかく私にとって)損ないます。
svidgen

1
Pieceが仮想でない場合、私はこの解決策に同意します。クラスの設計は表面上は少し複雑に見えるかもしれませんが、実装は全体的に単純になると思います。に関連付けられる主なものPieceは、PieceTypeアイデンティティではなく、場合によってはその移動履歴です。
JimmyJames

1
@svidgen構成されたピースを使用する実装と継承ベースのピースを使用する実装は、このソリューションで説明されている詳細を除いて、基本的に同じに見えます。この合成ソリューションは、単一レベルの間接参照を追加します。私はそれを避ける価値のあるものだとは思いません。
JimmyJames

2
@JimmyJamesそうです。ただし、複数のピースの移動履歴を追跡して相互に関連付ける必要があります。IMOが1つのピースの責任を負うべきものではありません。あなたがSOLIDのようなものに興味があるなら、それは「別個の懸念」です。
svidgen

0

私の見解では、提供された情報を使用して、継承と構成のどちらを選択すべきかについて答えることは賢明ではありません。

  • 継承と構成は、オブジェクト指向のデザイナーのツールベルトにおける単なるツールです。
  • これらのツールを使用して、問題のドメインのさまざまなモデルを設計できます。
  • ドメインを表すことができるモデルは多数あるため、設計者の要件の観点に応じて、継承と構成を複数の方法で組み合わせて、機能的に同等のモデルを作成できます。

モデルはまったく異なります。最初のモデルはチェスの駒の概念を中心にモデル化し、2番目のモデルは駒の動きの概念を中心にモデル化しました。それらは機能的に同等である可能性があり、ドメインをよりよく表し、私がそれについてより簡単に推論するのに役立つものを選択します。

また、2番目のデザインでは、Pieceクラスにtypeフィールドがあるという事実から、ピース自体が複数のタイプになる可能性があるため、ここにデザインの問題があることが明らかになります。これはおそらく、ある種の継承を使用する必要があるように聞こえませんか?ピース自体がタイプです。

つまり、ソリューションについて議論するのは非常に困難です。重要なのは、継承と構成のどちらを使用したかではなく、モデルが問題のドメインを正確に反映しているかどうか、そしてそれを推論して実装を提供することが役立つかどうかです。

継承と構成を適切に使用するにはある程度の経験が必要ですが、それらはまったく異なるツールであり、構成のみを使用してシステムを完全に設計できることには同意できますが、一方を他方に「置き換える」ことができるとは思いません。


重要なのはツールではなくモデルであるということをよく説明します。冗長タイプに関して、継承は本当に役立ちますか?たとえば、すべてのポーンを強調表示したい場合や、駒が王かどうかを確認したい場合は、キャストする必要はありませんか?
CanISleepまだ2019年

-1

まあ、どちらもお勧めしません。

オブジェクト指向は優れたツールであり、多くの場合、物事を簡素化するのに役立ちますが、ツールシェッドの唯一のツールではありません。

代わりに、単純なバイト配列を含むボードクラスの実装を検討してください。
それを解釈し、それについて計算することは、メンバー関数でビットマスキングとスイッチングを使用して行われます。

所有者にはMSBを使用し、ピースには7ビットを残しますが、空の場合はゼロを予約します。
または、空の場合はゼロ、所有者の場合は署名、ピースの絶対値。

必要に応じて、移植性はありませんが、手動マスキングを回避するためにビットフィールドを使用できます。

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};

興味深い...そしてC ++の質問だと考えるのが適切です。しかし、C ++プログラマではないので、これは私の最初(または2番目または3番目)の本能ではありません!...これはおそらくここで最も C ++の答えになるでしょう!
svidgen

7
これは、実際のアプリケーションが従うべきではないマイクロ最適化です。
リーライアン

@LieRyanあなたが持っているすべてがOOPなら、すべてがオブジェクトのようなにおいがします。そして、それが本当に「マイクロ」であるかどうかも興味深い質問です。それをどうするかによって、それがかなり重要になるかもしれません。
Deduplicator

Heh ... とはいえ、C ++ オブジェクト指向です。OPがC だけでなくC ++を使用している理由があると思います。
svidgen

3
ここでは、OOPの欠如についてはあまり気にしていませんが、コードを少しいじることで難読化しています。8ビットの任天堂用のコードを書いている場合や、ボード状態の何百万ものコピーを処理する必要があるアルゴリズムを作成している場合を除き、これは時期尚早なマイクロ最適化であり、お勧めできません。通常のシナリオでは、境界整列されていないビットアクセスには追加のビット操作が必要になるため、おそらく高速化はされません。
リーライアン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.