想象一下国际象棋比赛的以下两类:
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;
我希望在两个单独的单元 ChessBoard.pas 和 ChessPiece.pas 中定义两个类。
如何避免我遇到的圆形单位参考(在其他单位的界面部分需要每个单位)?
答案 0 :(得分:24)
德尔福单位并未“从根本上打破”。它们的工作方式有助于提高编译器的速度,并促进干净的类设计。
能够以Prims / .NET允许的方式在单元上传播类是可以从根本上打破的方法,因为它通过允许开发人员忽略正确设计他们的框架的需要来促进类的混乱组织,促进强制执行任意代码结构规则,例如“每单元一类”,它没有技术或组织价值作为普遍的格言。
在这种情况下,我立即注意到这种循环参考困境引起的课堂设计中的特殊性。
那就是,
如果从棋盘上取下一块,那么这样的参考就毫无意义,或者对于一个被移除的棋子,有效的“MoveTargets”是否仅仅是那个作为新游戏中“起始位置”的那个棋子?但我不认为除了对需要GetMoveTargets支持使用NIL板引用调用的案例的任意理由之外,这是有意义的。
在任何给定时间单个棋子的特定位置是国际象棋的单个游戏的属性,同样 VALID 的移动可能是可能的任何特定作品都取决于游戏中 OTHER pieces 的位置。
TChessPiece.GetMoveTargets 不需要了解当前的游戏状态。这是 TChessGame 的责任。并且 TChessPiece 不需要参考游戏或棋盘来确定来自给定当前位置的有效移动目标。板约束(8个等级和文件)是域常量,而不是给定板实例的属性。
因此,需要一个 TChessGame 来封装知识,这些知识结合了董事会,部分和 - 至关重要 - 规则,但董事会和部分不需要彼此了解或游戏。
将类别中不同部分的规则放在片段类型本身似乎很诱人,但这是一个错误imho,因为许多规则是基于与其他片段的交互,在某些情况下与特定片段的交互类型。这种“大局”行为需要对整个游戏状态进行一定程度的超视(阅读:概述),这在特定的棋子类中是不合适的。
e.g。如果这些对角线方块中的任何一个被占用,则TChessPawn可以确定有效移动目标是向前一个或两个方格或者对角方向前方一个方格。但是,如果棋子的移动使国王暴露于CHECK状态,则棋子根本不可移动。
我会通过简单地允许pawn类指示所有可能的移动目标 - 前方1或2个方格以及两个对角前方方格来实现此目的。 TChessGame 然后通过参考那些移动目标和游戏状态的占用来确定哪些是有效的。只有当棋子位于其主场等级,正方形被占用时才有2个方向前进,BLOCK a move =无效目标,空置对角线方块FACILITATE移动,如果任何其他有效移动暴露King,则该移动也无效。
同样,诱惑可能是将普遍适用的规则放在基础 TChessPiece 类中(例如,给定的移动会暴露国王吗?),但应用该规则需要了解整体游戏状态 - 即放置其他部分 - 因此它更恰当地属于 TChessGame 类的广义行为,imho
除了移动目标之外,碎片还需要指示CaptureTargets,在大多数碎片的情况下它们是相同的,但在某些情况下完全不同 - pawn是一个很好的例子。但同样,所有潜在捕获对任何特定行动都有效 - 如果有的话 - 是对游戏规则的评估,而不是一件作品或一类作品的行为。
正如99%的这种情况(ime-ymmv)中的情况一样,通过改变类设计以更好地表示被建模的问题,而不是找到将类设计变为任意文件的方法,可能更好地解决了这种困境。组织。
答案 1 :(得分:16)
一个解决方案可能是引入包含接口声明的第三个单元(IBoard和IPiece)。
然后具有类声明的两个单元的接口部分可以通过其接口引用另一个类:
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修饰符可以避免不必要的引用计数)
答案 2 :(得分: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;
这允许您将具体实例传递给国际象棋棋子,而不必担心循环引用。由于董事会私人使用Tchesspieces,因此在Tchesspiece声明之前它不一定存在,就像占位符一样。当然,tChessPiece必须知道的任何状态变量都应该放在tBaseChessBoard中,两者都可以使用它们。
答案 3 :(得分:3)
将ChessPiece课程移到ChessBoard单位会更好 如果出于某种原因你不能尝试将一个使用子句放在一个单元中的实现部分中,而将另一个保留在接口部分中。
答案 4 :(得分:1)
使用 Delphi Prism ,您可以将命名空间分散到单独的文件中,这样您就可以以干净的方式解决它。
单位的工作方式从根本上打破了他们当前的Delphi实施。只需看看“db.pas”如何在一个可怕的.pas文件中使用TField,TDataset,TParam等,因为它们的接口相互引用。
无论如何,您始终可以将代码移动到单独的文件中,并将其包含在 {$include ChessBoard_impl.inc}
中。这样你就可以在文件上分割内容,并通过你的vcs拥有单独的版本。但是,以这种方式编辑文件只是有点不方便。
最好的长期解决方案是敦促embarcadero抛弃1970年pascal出生时有意义的一些想法,但这对于现在的开发人员而言并不仅仅是痛苦。单程编译器就是其中之一。
答案 5 :(得分:0)
它看起来不像ChessBoard.Board需要是一个ChestPiece数组,它也可以是TObject并在ChessPiece.pas中被低估。
答案 6 :(得分:0)
另一种方法:
制作你的tBaseChessPiece董事会。它是抽象的,但包含您需要引用的定义。
内部工作在tChessPiece中,它来自tBaseChessPiece。
我确实同意Delphi处理相互引用的东西是不好的 - 关于该语言最糟糕的特性。我长期以来一直呼吁各部门都有前瞻性声明。编译器将拥有它所需的信息,它不会破坏使其如此快速的一次通过性质。
答案 7 :(得分: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;
...
在这个方法中,棋盘单元将仅在实现部分中包含片段单元的引用(由于实际上将创建对象的方法)并且片段单元将在界面中引用棋盘单元。 如果我没有错,这可以轻松处理你的问题......
答案 8 :(得分:0)
从TObject
导出TChessBoardTChessBoard = class(TObject)
然后你可以申报 过程GetMoveTargets(BoardPos:TPoint; Board:TObject; MoveTargetList:TList);
当你调用proc时,使用SELF作为Board对象(如果你从那里调用它),那么你可以用
引用它(董事会作为TChessBoard)。并从中访问属性等。