jpf*_*ius 16 delphi project-organization circular-reference
想象一下国际象棋游戏的以下两类:
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;
Run Code Online (Sandbox Code Playgroud)
我希望在两个单独的ChessBoard.pas和ChessPiece.pas单元中定义这两个类.
如何避免我遇到的圆形单元参考(在另一个单元的接口部分需要每个单元)?
Del*_*ics 24
德尔福单位并未"从根本上打破".它们的工作方式有助于提高编译器的速度并促进干净的类设计.
能够以Prims/.NET允许的方式在单元上传播类是可以从根本上打破的方法,因为它通过允许开发人员忽略正确设计他们的框架,促进强制任意的需要来促进类的混乱组织代码结构规则,例如"每单元一个类",它没有作为通用格言的技术或组织价值.
在这种情况下,我立即注意到这种循环参考困境引起的课堂设计中的特殊性.
也就是说,为什么会一片曾经有任何需要引用一个板?
如果从棋盘上取下一块,那么这样的参考就毫无意义,或者对于一个被移除的棋子,有效的"MoveTargets"是否仅仅是那个作为新游戏中"起始位置"的那个棋子的有效"MoveTargets"?但我不认为除了对需要GetMoveTargets支持使用NIL板引用调用的案例的任意理由之外,这是有意义的.
在任何给定时间单个棋子的特定位置是单独的国际象棋游戏的属性,并且对于任何给定棋子可能可能的VALID移动同样取决于游戏中其他棋子的位置.
TChessPiece.GetMoveTargets不需要了解当前的游戏状态.这是TChessGame的责任.并且TChessPiece不需要参考游戏或板来确定来自给定当前位置的有效移动目标.板约束(8个等级和文件)是域常量,而不是给定板实例的属性.
因此,需要一个TChessGame来封装知识,这些知识结合了董事会的意识,部分和 - 至关重要 - 规则,但董事会和部分不需要彼此了解游戏的OR.
将类型中的不同部分的规则放在片段类型本身中似乎很诱人,但这是一个错误,因为许多规则都是基于与其他片段的交互,在某些情况下是基于特定片段类型.这种"大画面"行为需要对整个游戏状态的一定程度的超视(阅读:概述),这在特定的片段类中是不合适的.
例如,如果这些对角线方块中的任何一个被占用,则TChessPawn可以确定有效移动目标是向前一个或两个方格或者对角方向前方一个方格.但是,如果棋子的移动使国王暴露于CHECK状态,那么棋子根本不可移动.
我会通过简单地允许pawn类指示所有可能的移动目标 - 前方1或2个方格以及两个对角前方方格来实现此目的.然后,TChessGame通过参考那些移动目标和游戏状态的占用来确定哪些是有效的.只有当棋子位于其主场等级,正方形被占用时才有2个方向前进,BLOCK a move =无效目标,空置对角线方块FACILITATE移动,如果任何其他有效移动暴露King,则该移动也无效.
同样,诱惑可能是将普遍适用的规则放在基础TChessPiece类中(例如,给定的移动是否暴露了King?),但应用该规则需要了解整体游戏状态 - 即放置其他部分 - 所以它更多正确属于TChessGame类的一般行为,即imho
除了移动目标之外,碎片还需要指示CaptureTargets,在大多数碎片的情况下它们是相同的,但在某些情况下完全不同 - pawn是一个很好的例子.但同样,如果有任何潜在的捕获对任何给定的动作都有效的话,那就是 - imho--对游戏规则的评估,而不是对一件作品或一类作品的行为.
与99%的此类情况(ime-ymmv)中的情况一样,通过改变类设计以更好地表示被建模的问题,而不是找到将类设计变为任意文件组织的方法,可能更好地解决了这种困境.
mjn*_*mjn 16
一种解决方案可能是引入包含接口声明(IBoard和IPiece)的第三个单元.
然后具有类声明的两个单元的接口部分可以通过其接口引用另一个类:
TChessBoard = class(TInterfacedObject, IBoard)
private
FBoard : array [1..8, 1..8] of IPiece;
...
end;
Run Code Online (Sandbox Code Playgroud)
和
TChessPiece = class abstract(TInterfacedObject, IPiece)
public
procedure GetMoveTargets (BoardPos: TPoint; const Board: IBoard;
MoveTargetList: TList <TPoint>);
...
end;
Run Code Online (Sandbox Code Playgroud)
(GetMoveTargets中的const修饰符可以避免不必要的引用计数)
ska*_*adt 11
将定义TChessPiece的单位更改为如下所示:
TYPE
tBaseChessBoard = class;
TChessPiece = class
procedure GetMoveTargets (BoardPos : TPoint; Board : TBaseChessBoard; ...
...
end;
Run Code Online (Sandbox Code Playgroud)
然后将定义TChessBoard的单元修改为如下所示:
USES
unit_containing_tBaseChessboard;
TYPE
TChessBoard = class(tBaseChessBoard)
private
FBoard : array [1..8, 1..8] of TChessPiece;
...
end;
Run Code Online (Sandbox Code Playgroud)
这允许您将具体实例传递给国际象棋,而不必担心循环引用.由于董事会私人使用Tchesspieces,因此在Tchesspiece声明之前它不一定存在,就像占位符一样.当然,tChessPiece必须知道的任何状态变量都应放在tBaseChessBoard中,两者都可以使用它们.