16

チェス ゲームの次の 2 つのクラスを想像してください。

TChessBoard = class
private
  FBoard : array [1..8, 1..8] of TChessPiece;
...
end;

TChessPiece = class abstract
public
   procedure GetMoveTargets (BoardPos : TPoint; Board : TChessBoard; MoveTargetList : TList <TPoint>);
...
end;

2 つのクラスをChessBoard.pasChessPiece.pasという 2 つの別個のユニットで定義したいと考えています。

ここで発生する循環ユニット参照を回避するにはどうすればよいですか (各ユニットは他のユニットのインターフェイス セクションで必要です)。

4

9 に答える 9

24

Delphi ユニットは「根本的に壊れている」わけではありません。それらが機能する方法により、コンパイラの驚異的な速度が促進され、クリーンなクラス設計が促進されます。

Prims/.NET が可能にする方法でクラスをユニットに分散できることは、開発者がフレームワークを適切に設計する必要性を無視できるようにすることでクラスの無秩序な編成を促進し、任意の強制を促進するため、間違いなく根本的に壊れているアプローチです。 「ユニットごとに1つのクラス」などのコード構造規則。これには、普遍的な格言としての技術的または組織的なメリットはありません。

このケースでは、この循環参照のジレンマから生じるクラス設計の特異性にすぐに気付きました。

つまり、なぜ作品がボードを参照する必要があるのでしょうか?

駒がボードから取られた場合、そのような参照は意味をなさないか、または削除された駒の有効な「MoveTargets」は、新しいゲームの「開始位置」としてその駒に対して有効なものだけですか? しかし、これは、GetMoveTargets が NIL ボード参照を使用した呼び出しをサポートすることを要求する場合の恣意的な正当化以外には意味がないと思います。

任意の時点での個々の駒の特定の配置は、チェスの個々のゲームの特性であり、同様に、任意の駒に対して可能な有効な動きは、ゲーム内の他のの配置に依存します。

TChessPiece.GetMoveTargetsは、現在のゲームの状態を知る必要はありません。これはTChessGameの責任です。また、TChessPieceは、指定された現在の位置から有効な移動ターゲットを決定するために、ゲームまたはボードへの参照を必要としません。ボードの制約 (8 つのランクとファイル) はドメイン定数であり、特定のボード インスタンスのプロパティではありません。

したがって、ボード、駒、そして重要なことにルールの認識を組み合わせた知識をカプセル化するTChessGameが必要ですが、ボードと駒は互いの知識やゲームの知識を必要としません。

ピース タイプ自体のクラスに別のピースに関連するルールを配置するのは魅力的に思えるかもしれませんが、ルールの多くは他のピースとの相互作用に基づいており、場合によっては特定のピース タイプとの相互作用に基づいているため、これは間違いです。このような「全体像」の動作には、特定のピース クラスでは適切ではない、ゲーム全体の状態のある程度の監視 (概要を読む) が必要です。

たとえば、TChessPawn は、有効な移動ターゲットが前方の 1 つか 2 つの正方形、または斜め前方の 1 つの正方形であると、これらの斜めの正方形のいずれかが占有されている場合に決定する場合があります。ただし、ポーンの動きによってキングが CHECK 状況にさらされる場合、ポーンはまったく移動できません。

私は単純に、ポーン クラスがすべての可能な移動ターゲットを示すようにすることでこれに取り組みます。次に、TChessGameは、これらの移動ターゲットの占有率とゲームの状態を参照して、これらのどれが有効かを判断します。2 マス前方に移動できるのは、ポーンがホーム ランクにあり、前方のマスが占有されている場合にのみ可能です。移動をブロックする = 無効なターゲット、占有されていない対角線のマスが移動を促進し、それ以外の有効な移動がキングを露出する場合、その移動も無効です。

繰り返しになりますが、一般的に適用されるルールをベースのTChessPieceクラスに配置したくなるかもしれません (たとえば、特定の動きはキングを公開しますか?)。TChessGameクラスの一般化された動作として適切に属します。

移動ターゲットに加えて、ピースは CaptureTargets も示す必要があります。これは、ほとんどのピースの場合は同じですが、場合によってはまったく異なります - ポーンが良い例です。しかし、繰り返しになりますが、すべての潜在的なキャプチャのうち、特定の動きに対して効果的なのは - 私見 - ゲームルールの評価であり、ピースまたはピースのクラスの動作ではありません.

そのような状況 (ime - ymmv) の 99% の場合と同様に、ジレンマは、クラス設計を任意のファイル編成に押し込む方法を見つけるのではなく、モデル化されている問題をより適切に表現するようにクラス設計を変更することによって、おそらくより適切に解決されます。

于 2009-08-16T22:43:03.227 に答える
16

解決策の 1 つは、インターフェイス宣言 (IBoard および IPiece) を含む 3 番目のユニットを導入することです。

次に、クラス宣言を持つ 2 つのユニットのインターフェイス セクションは、そのインターフェイスによって他のクラスを参照できます。

TChessBoard = class(TInterfacedObject, IBoard)
private
  FBoard : array [1..8, 1..8] of IPiece;
...
end;

TChessPiece = class abstract(TInterfacedObject, IPiece)
public
   procedure GetMoveTargets (BoardPos: TPoint; const Board: IBoard; 
     MoveTargetList: TList <TPoint>);
...
end;

(GetMoveTargets の const 修飾子は、不要な参照カウントを回避します)

于 2009-08-16T18:55:57.283 に答える
11

TChessPiece を定義する単位を次のように変更します。

TYPE
  tBaseChessBoard = class;

  TChessPiece = class
    procedure GetMoveTargets (BoardPos : TPoint; Board : TBaseChessBoard; ...    
  ...
  end;    

次に、TChessBoard を定義するユニットを次のように変更します。

USES
  unit_containing_tBaseChessboard;

TYPE
  TChessBoard = class(tBaseChessBoard)
  private
    FBoard : array [1..8, 1..8] of TChessPiece;
  ...
  end;  

これにより、循環参照を気にすることなく、具体的なインスタンスをチェスの駒に渡すことができます。ボードは非公開で Tchesspiece を使用するため、Tchesspiece 宣言の前にプレース ホルダーとして存在する必要はありません。もちろん、tChessPiece が認識しなければならない状態変数は、tBaseChessBoard に配置する必要があります。

于 2009-08-17T22:17:54.940 に答える
3

ChessPiece クラスを ChessBoard ユニットに移動した方がよいでしょう。
何らかの理由でそれができない場合は、一方のuses句を一方のユニットの実装部分に配置し、もう一方をインターフェース部分に残すようにしてください。

于 2009-08-16T18:28:52.057 に答える
1

Delphi Prismを使用すると、名前空間を別々のファイルに分散できるため、クリーンな方法で解決できます。

ユニットの動作方法は、現在の Delphi 実装では根本的に壊れています。「db.pas」が 1 つの巨大な .pas ファイルに TField、TDataset、TParam などを含める必要があったことを見てください。これらのインターフェイスは相互に参照するためです。

とにかく、いつでもコードを別のファイルに移動して{$include ChessBoard_impl.inc}、たとえばそれらを含めることができます。そうすれば、ファイルを分割して、vcs を介して別々のバージョンを作成できます。ただし、そのようにファイルを編集するのは少し不便です。

最善の長期的な解決策は、パスカルが生まれた 1970 年には理にかなっていたアイデアの一部を捨てるようエンバカデロに促すことですが、それは今日の開発者にとってお尻の痛み以上のものではありません。ワンパス コンパイラはその 1 つです。

于 2009-08-16T22:10:54.503 に答える
0

TObjectからTChessBoardを派生させる

TChessBoard = class(TObject)

次に、プロシージャGetMoveTargets(BoardPos:TPoint; Board:TObject; MoveTargetList:TList);を宣言できます。

procを呼び出すときは、BoardオブジェクトとしてSELFを使用し(そこから呼び出す場合)、次のコマンドで参照できます。

(TChessBoardとしてのボード)。そこからプロパティなどにアクセスします。

于 2012-05-03T19:47:39.773 に答える
0

このアプローチはどうですか?

チェス盤ユニット:

TBaseChessPiece = class 

public

   procedure GetMoveTargets (BoardPos : TPoint; Board : TChessBoard; MoveTargetList : TList <TPoint>); virtual; abstract;

...

TChessBoard = class
private
  FBoard : array [1..8, 1..8] of TChessPiece;

  procedure InitializePiecesWithDesiredClass;
...

ピース単位:

TYourPiece = class TBaseChessPiece

public 

   procedure GetMoveTargets (BoardPos : TPoint; Board : TChessBoard; MoveTargetList : TList <TPoint>);override;

...

このアプローチでは、チェス盤ユニットは実装セクションにのみピースユニットの参照を含み(実際にオブジェクトを作成するメソッドのため)、ピースユニットはインターフェースでチェス盤ユニットへの参照を持ちます。私が間違っていなければ、これはあなたの問題を簡単な方法で処理します...

于 2009-08-18T18:14:43.710 に答える
0

別のアプローチ:

tBaseChessPiece でボードを作成します。抽象的ですが、参照する必要がある定義が含まれています。

内部の仕組みは、tBaseChessPiece から派生した tChessPiece にあります。

Delphi の相互参照の扱いが悪いことに同意します。つまり、言語の最悪の機能についてです。私は長い間、ユニット間で機能する前方宣言を求めてきました。コンパイラーは必要な情報を手に入れることができ、コンパイラーを非常に高速にするワンパスの性質を壊すことはありません。

于 2009-08-16T22:24:14.783 に答える
0

TChessBoard.FBoard が TChessPiece の配列である必要があるようには見えません。TObject であっても ChessPiece.pas でダウンキャストされてもかまいません。

于 2009-08-16T19:21:13.977 に答える