How to avoid circular unit reference?

不打扰是莪最后的温柔 提交于 2019-11-27 11:55:03

问题


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)?


回答1:


Change the unit that defines TChessPiece to look like the following:

TYPE
  tBaseChessBoard = class;

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

then modify the unit that defines TChessBoard to look like the following:

USES
  unit_containing_tBaseChessboard;

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

This allows you to pass concrete instances to the chess piece without having to worry about a circular reference. Since the board uses the Tchesspieces in its private, it really doesn't have to exist prior to the Tchesspiece declaration, just as place holder. Any state variables which the tChessPiece must know about of course should be placed in the tBaseChessBoard, where they will be available to both.




回答2:


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.




回答3:


One solution could be the introduction of a third unit which contains interface declarations (IBoard and IPiece).

Then the interface sections of the two units with class declarations can refer to the other class by its interface:

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

and

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

(The const modifier in GetMoveTargets avoids unnecessary reference counting)




回答4:


It would be better to move ChessPiece class into ChessBoard unit.
If for some reason you can't, try to put one uses clause to the implementation part in one unit and leave the other one in the interface part.




回答5:


With Delphi Prism you can spread your namespaces over separate files, so there you would be able to solve it in a clean way.

The way units work are just fundamentally broken with their current Delphi implementation. Just look at how "db.pas" needed to have TField, TDataset, TParam, etc in one monstrous .pas file because their interfaces reference each other.

Anyway, you can always move code to a separate file and include them with {$include ChessBoard_impl.inc} for example. That way you can split stuff over files and have separate versions via your vcs. However, it's just a bit unhandy to edit files that way.

The best long-term solution would be to urge embarcadero to ditch some of the ideas that made sense in 1970 when pascal was born, but that are not much more than a pain in the ass for developers nowadays. A one-pass compiler is one of those.




回答6:


It does not look like TChessBoard.FBoard needs to be an array of TChessPiece, it can as well be of TObject and be downcasted in ChessPiece.pas.




回答7:


Another approach:

Make your board of tBaseChessPiece. It's abstract but contains the definitions you need to refer to.

The internal workings are in tChessPiece which descends from tBaseChessPiece.

I do agree that Delphi's handling of things that refer to each other is bad--about the worst feature of the language. I've long called for forward declarations that work across units. The compiler would have the information it needs, it wouldn't break the one-pass nature that makes it so fast.




回答8:


what about this approach:

chess board unit:

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;
...

pieces unit:

TYourPiece = class TBaseChessPiece

public 

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

...

In this aproach chess board unit will include the reference of pieces unit only in implementation section(due to the method that will in fact create the objects) and pieces unit will have a reference to chess board unit in interface. If I'm not wrong this handle your problem in a easy way...




回答9:


Derive TChessBoard from TObject

TChessBoard = class(TObject)

then you can declare procedure GetMoveTargets (BoardPos : TPoint; Board : TObject; MoveTargetList : TList );

when you call the proc, use SELF as the Board object (if you are calling it from there), then you can reference it with

(Board as TChessBoard). and access the properties etc from that.



来源:https://stackoverflow.com/questions/1284959/how-to-avoid-circular-unit-reference

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!