Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid circular unit reference?

Imagine the following two classes of a chess game:

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;

I want the two classes to be defined in two separate units ChessBoard.pas and ChessPiece.pas.

How can I avoid the circular unit reference that I run into here (each unit is needed in the other unit's interface section)?

like image 865
jpfollenius Avatar asked Aug 16 '09 18:08

jpfollenius


1 Answers

Delphi units are not "fundamentally broken". The way they work facilitates the phenomenal speed of the compiler and promotes clean class designs.

Being able to spread classes over units in the way that Prims/.NET allows is the approach that is arguably fundamentally broken as it promotes chaotic organisation of classes by allowing the developer to ignore the need to properly design their framework, promoting the imposition of arbitrary code structure rules such as "one class per unit", which has no technical or organisation merit as a universal dictum.

In this case, I immediately noticed an idiosynchracy in the class design arising from this circular reference dilemma.

That is, why would a piece ever have any need to reference a board?

If a piece is taken from a board, such a reference then makes no sense, or perhaps the valid "MoveTargets" for a removed piece are only those valid for that piece as a "starting position" in a new game? But I don't think this makes sense as anything other than a arbitrary justification for a case that demands that GetMoveTargets support invocation with a NIL board reference.

The particular placement of an individual piece at any given time is a property of an individual game of chess, and equally the VALID moves that may be POSSIBLE for any given piece are dependent upon the placement of OTHER pieces in the game.

TChessPiece.GetMoveTargets does not need knowledge of the current game state. This is the responsibility of a TChessGame. And a TChessPiece does not need a reference to a game or to a board to determine the valid move targets from a given current position. The board constraints (8 ranks and files) are domain constants, not properties of a given board instance.

So, a TChessGame is required that encapsulates the knowledge that combines awareness of a board, the pieces and - crucially - the rules, but the board and the pieces do not need knowledge of each other OR of the game.

It may seem tempting to put the rules pertaining to different pieces in the class for the piece type itself, but this is a mistake imho, since many of the rules are based on interactions with other pieces and in some cases with specific piece types. Such "big picture" behaviours require a degree of over-sight (read: overview) of the total game state that is not appropriate in a specific piece class.

e.g. a TChessPawn may determine that a valid move target is one or two squares forward or one square diagonally forward if either of those diagonal squares are occupied. However, if the movement of the pawn exposes the King to a CHECK situation, then the pawn is not movable at all.

I would approach this by simply allowing the pawn class to indicate all POSSIBLE move targets - 1 or 2 squares forward and both diagonally forward squares. The TChessGame then determines which of these is valid by reference to occupancy of those move targets and game state. 2 squares forward is only possible if the pawn is on it's home rank, forward squares being occupied BLOCK a move = invalid target, UNoccupied diagonal squares FACILITATE a move, and if any otherwise valid move exposes the King, then that move is also invalid.

Again, the temptation might be to put the generally applicable rules in the base TChessPiece class (e.g. does a given move expose the King?), but applying that rule requires awareness of the overall game state - i.e. placement of other pieces - so it more properly belongs as a generalised behaviour of the TChessGame class, imho

In addition to move targets, pieces also need to indicate CaptureTargets, which in the case of most pieces is the same, but in some cases quite different - pawn being a good example. But again, which - if any - of all potential captures is effective for any given move is - imho - an assessment of the game rules, not the behaviour of a piece or class of pieces.

As is the case in 99% of such situations (ime - ymmv) the dilemma is perhaps better solved by changing the class design to better represent the problem being modelled, not finding a way to shoehorn the class design into an arbitrary file organisation.

like image 112
Deltics Avatar answered Oct 12 '22 22:10

Deltics