如何避免循环单位参考?

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.pasChessPiece.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)中的情况一样,通过改变类设计以更好地表示被建模的问题,而不是找到将类设计变为任意文件组织的方法,可能更好地解决了这种困境.

  • 编译速度论证直到15年前才有意义.今天,我从来没有遇到任何语言的编译速度问题.只有Delphi开发人员似乎愿意放弃灵活性,每次编译只需几毫秒.从我在现实世界中看到的,这种限制根本不会促进清洁的OO设计.它只是迫使人们制作具有2934938个类的巨型.pas文件,这些类可以互相看到,甚至可以访问彼此的私人成员. (11认同)
  • @Loren:编译器不强制执行任何此类操作.VCL属性流机制与链接器和表单设计器共谋,每个单元强制一个FORM类,但您可以在表单单元中引入其他非表单类,就像您对任何其他单元一样.无论如何,表单的独立可移动"部件"应该可以说是组件,如果您想要一个可视化设计表面来创建这些组件,您可以使用框架来创建这些部件.但是,每帧(即形式)单位只有一个"片段". (2认同)
  • 你说的没错.Delphi的单元文件没有"破坏",但你的断言"每个单元/文件一个类"没有技术或组织价值是有缺陷的.它的优点在于理解的速度.作为开发人员,我们花费80%-90%的时间阅读和理解源代码,只有10%-20%的修改它.在文件中筛选30个边缘相关的类以仅修改其中的1个是浪费时间.Delphi的库设计为每个文件有多个类,但编译器肯定不会强迫任何人使用它.正如@Wouter指出的那样,它只会促使紧密耦合. (2认同)
  • 我并不是说每个单位应该盲目坚持一个班级.只有它具有你所忽略的非常真实和显着的好处.除了可读性之外,它还减少了合并冲突的可能性以及更改类时重新编译的需要.此外,大多数编译器消息仅报告文件名和行号.每个单元有1个类,在你打开文件之前,很明显哪个类有错误.是的,规则总是有例外.我只想到每单位1班是规则而不是例外. (2认同)

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中,两者都可以使用它们.