16

想象一下以下两类国际象棋游戏:

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.pasChessPiece.pas中定义这两个类。

我怎样才能避免在这里遇到的循环单元引用(每个单元都需要在另一个单元的接口部分)?

4

9 回答 9

24

德尔福单位没有“从根本上坏掉”。它们的工作方式促进了编译器的惊人速度并促进了干净的类设计。

能够以 Prims/.NET 允许的方式将类分布在单元上,这种方法可以说从根本上被破坏了,因为它允许开发人员忽略正确设计其框架的需要,从而促进了类的混乱组织,从而促进了任意代码结构规则,例如“每个单元一个类”,作为通用格言没有技术或组织价值。

在这种情况下,我立即注意到由于这种循环引用困境而导致的类设计中的一种特殊性。

也就是说,为什么一件作品需要参考一块木板

如果一块棋子是从棋盘上取下来的,那么这样的引用就没有意义了,或者被移除的棋子的有效“移动目标”可能只是那些对该棋子有效的作为新游戏中的“起始位置”的那些?但我认为这对于一个要求 GetMoveTargets 支持使用 NIL 板引用的调用的案例来说,除了任意理由之外没有任何意义。

在任何给定时间单个棋子的特定位置是单个国际象棋游戏的属性,同样,任何给定棋子可能的有效移动取决于游戏中其他棋子的位置。

TChessPiece.GetMoveTargets不需要当前游戏状态的知识。这是TChessGame的职责。并且TChessPiece不需要参考游戏或棋盘来确定给定当前位置的有效移动目标。棋盘约束(8 个等级和文件)是域常量,而不是给定棋盘实例的属性。

因此,需要一个TChessGame来封装包含对棋盘、棋子和 - 至关重要的是 - 规则的认识的知识,但棋盘和棋子不需要相互了解或了解游戏。

将与不同片段有关的规则放在片段类型本身的类中似乎很诱人,但这是一个错误恕我直言,因为许多规则是基于与其他片段的交互,在某些情况下与特定片段类型的交互。这种“大局”行为需要对整个游戏状态有一定程度的监督(阅读:概述),这在特定的棋子类别中是不合适的。

例如,如果这些对角格中的任何一个被占用,则 TChessPawn 可以确定有效的移动目标是向前一格或两格,或者向前对角格一格。但是,如果棋子的移动使国王处于 CHECK 状态,则棋子根本无法移动。

我会通过简单地允许 pawn 类指示所有可能的移动目标来解决这个问题 - 向前 1 或 2 个方格和两个对角线前方方格。TChessGame然后通过参考这些移动目标的占用率和游戏状态来确定其中哪些是有效的仅当兵在其本垒上时,才可能向前走 2 个方格,向前方格被占用阻止移动 = 无效目标,未占用的对角线方格促进移动,如果任何其他有效的移动暴露了国王,那么该移动也是无效的。

再一次,诱惑可能是将普遍适用的规则放在基础TChessPiece类中(例如,给定的移动是否暴露了国王?),但应用该规则需要了解整体游戏状态 - 即其他棋子的放置 - 所以它更多正确地属于TChessGame类的一般行为,恕我直言

除了移动目标之外,棋子还需要指明 CaptureTargets,这在大多数棋子的情况下是相同的,但在某些情况下却完全不同 - pawn 就是一个很好的例子。但同样,所有潜在捕获中的任何一个 - 如果有的话 - 对于任何给定的动作都是有效的 - 恕我直言 - 对游戏规则的评估,而不是一块或一类棋子的行为。

与 99% 的此类情况(ime - ymmv)一样,通过更改类设计以更好地表示正在建模的问题,而不是找到将类设计硬塞到任意文件组织中的方法,或许可以更好地解决困境。

于 2009-08-16T22:43:03.227 回答
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 修饰符避免了不必要的引用计数)

于 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”如何在一个可怕的 .pas 文件中包含 TField、TDataset、TParam 等,因为它们的接口相互引用。

无论如何,您总是可以将代码移动到一个单独的文件中,并将它们包含{$include ChessBoard_impl.inc}在其中。这样你就可以在文件中分割东西并通过你的 vcs 拥有单独的版本。但是,以这种方式编辑文件有点不方便。

最好的长期解决方案是敦促 embarcadero 放弃一些在 1970 年帕斯卡诞生时有意义的想法,但这对于当今的开发人员来说只不过是一种痛苦。一次性编译器就是其中之一。

于 2009-08-16T22:10:54.503 回答
0

看起来 TChessBoard.FBoard 不需要是 TChessPiece 的数组,它也可以是 TObject 并在 ChessPiece.pas 中向下转换。

于 2009-08-16T19:21:13.977 回答
0

另一种方法:

制作您的 tBaseChessPiece 棋盘。它是抽象的,但包含您需要参考的定义。

内部工作在 tChessPiece 中,它源自 tBaseChessPiece。

我确实同意 Delphi 对相互引用事物的处理很糟糕——这是该语言最糟糕的特性。我一直呼吁跨单位工作的前向声明。编译器将拥有它需要的信息,它不会破坏使其如此快速的一次性性质。

于 2009-08-16T22:24:14.783 回答
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

从 TObject 派生 TChessBoard

TChessBoard = 类(TObject)

然后你可以声明过程 GetMoveTargets (BoardPos : TPoint; Board : TObject; MoveTargetList : TList );

当您调用 proc 时,使用 SELF 作为 Board 对象(如果您从那里调用它),那么您可以使用

(棋盘为 TChessBoard)。并从中访问属性等。

于 2012-05-03T19:47:39.773 回答