delphi - ¿Cómo evitar la unidad circular de referencia?
project-organization circular-reference (9)
¿Qué pasa con este enfoque?
unidad de tablero de ajedrez:
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;
...
unidad de piezas:
TYourPiece = class TBaseChessPiece
public
procedure GetMoveTargets (BoardPos : TPoint; Board : TChessBoard; MoveTargetList : TList <TPoint>);override;
...
En este enfoque, la unidad del tablero de ajedrez incluirá la referencia de la unidad de piezas solo en la sección de implementación (debido al método que de hecho creará los objetos) y la unidad de piezas tendrá una referencia a la unidad del tablero de ajedrez en la interfaz. Si no me equivoco, manejaré tu problema de una manera fácil ...
Imagina las siguientes dos clases de un juego de ajedrez:
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;
Quiero que las dos clases se definan en dos unidades separadas ChessBoard.pas y ChessPiece.pas .
¿Cómo puedo evitar la referencia de unidad circular que encuentro aquí (cada unidad es necesaria en la sección de interfaz de la otra unidad)?
Cambie la unidad que define TChessPiece para que se parezca a lo siguiente:
TYPE
tBaseChessBoard = class;
TChessPiece = class
procedure GetMoveTargets (BoardPos : TPoint; Board : TBaseChessBoard; ...
...
end;
luego modifique la unidad que define TChessBoard para que se parezca a lo siguiente:
USES
unit_containing_tBaseChessboard;
TYPE
TChessBoard = class(tBaseChessBoard)
private
FBoard : array [1..8, 1..8] of TChessPiece;
...
end;
Esto le permite pasar instancias concretas a la pieza de ajedrez sin tener que preocuparse por una referencia circular. Dado que la placa utiliza las Tchesspieces en su versión privada, realmente no tiene que existir antes de la declaración de Tchesspiece, solo como titular de posición. Cualquier variable de estado que el chessPiece debe conocer, por supuesto, debe colocarse en el tBaseChessBoard, donde estarán disponibles para ambos.
Con Delphi Prism puede distribuir sus espacios de nombres en archivos separados, de modo que allí podrá resolverlo de una manera limpia.
La forma en que funcionan las unidades se rompe fundamentalmente con su implementación actual de Delphi. Simplemente observe cómo "db.pas" necesitaba tener TField, TDataset, TParam, etc. en un monstruoso archivo .pas porque sus interfaces se hacen referencia entre sí.
De todos modos, siempre puede mover el código a un archivo separado e incluirlos con {$include ChessBoard_impl.inc}
por ejemplo. De esa manera, puede dividir cosas sobre archivos y tener versiones separadas a través de su vcs. Sin embargo, es un poco desagradable editar archivos de esa manera.
La mejor solución a largo plazo sería instar a que el embarcadero abandone algunas de las ideas que tuvieron sentido en 1970 cuando nació Pascal, pero que no son mucho más que un problema para los desarrolladores en la actualidad. Un compilador de una pasada es uno de esos.
Derivar TChessBoard de TObject
TChessBoard = class (TObject)
luego puede declarar el procedimiento GetMoveTargets (BoardPos: TPoint; Board: TObject; MoveTargetList: TList);
cuando llame al proc, use SELF como el objeto del tablero (si lo llama desde allí), entonces puede hacer referencia a él con
(Junta como tablero de ajedrez). y acceder a las propiedades etc desde eso.
Las unidades Delphi no están "fundamentalmente rotas". La forma en que trabajan facilita la velocidad fenomenal del compilador y promueve diseños de clase limpia.
Ser capaz de repartir clases en unidades de la manera que Prims / .NET permite es el enfoque que posiblemente se rompe fundamentalmente ya que promueve la organización caótica de clases al permitir que el desarrollador ignore la necesidad de diseñar adecuadamente su marco, promoviendo la imposición de arbitrarios. reglas de estructura de código, como "una clase por unidad", que no tiene mérito técnico ni de organización como dictamen universal.
En este caso, inmediatamente noté una idiosincracia en el diseño de la clase que surge de este dilema de referencia circular.
Es decir, ¿por qué una pieza tendría alguna vez la necesidad de hacer referencia a una tabla?
Si se toma una pieza de un tablero, una referencia de este tipo no tiene sentido, o tal vez, ¿los "MoveTargets" válidos para una pieza eliminada son solo aquellos válidos para esa pieza como una "posición inicial" en un juego nuevo? Pero no creo que esto tenga sentido como algo más que una justificación arbitraria para un caso que exige que GetMoveTargets soporte la invocación con una referencia de la junta NIL.
La ubicación particular de una pieza individual en un momento dado es una propiedad de un juego individual de ajedrez, e igualmente los movimientos VÁLIDOS que pueden ser POSIBLES para cualquier pieza determinada dependen de la colocación de OTRAS piezas en el juego.
TChessPiece.GetMoveTargets no necesita conocer el estado actual del juego. Esta es la responsabilidad de un juego de ajedrez . Y una TChessPiece no necesita una referencia a un juego oa un tablero para determinar los objetivos de movimiento válidos desde una posición actual dada. Las restricciones de la placa (8 rangos y archivos) son constantes de dominio, no propiedades de una instancia de placa determinada.
Por lo tanto, se requiere un TChessGame que encapsule el conocimiento que combina el conocimiento de un tablero, las piezas y, lo que es crucial, las reglas, pero el tablero y las piezas no necesitan conocimiento el uno del otro ni el juego.
Puede parecer tentador poner las reglas correspondientes a diferentes piezas en la clase para el tipo de pieza en sí, pero esto es un error, ya que muchas de las reglas se basan en interacciones con otras piezas y, en algunos casos, con tipos de piezas específicos. Tales comportamientos de "imagen general" requieren un grado de supervisión (lea: descripción general) del estado total del juego que no es apropiado en una clase de pieza específica.
por ejemplo, un TChessPawn puede determinar que un objetivo de movimiento válido es uno o dos cuadrados adelante o uno cuadrado diagonalmente hacia adelante si alguno de esos cuadrados diagonales está ocupado. Sin embargo, si el movimiento del peón expone al Rey a una situación de VERIFICACIÓN, entonces el peón no es movible en absoluto.
Me acercaría a esto simplemente permitiendo que la clase de peón indique todos los POSIBLES objetivos de movimiento: 1 o 2 casillas hacia adelante y ambas casillas diagonalmente hacia adelante. El TChessGame luego determina cuál de estos es válido por referencia a la ocupación de esos objetivos de movimiento y el estado del juego. 2 casillas adelante solo es posible si el peón está en su rango inicial, las casillas delanteras están ocupadas BLOQUEANDO un movimiento = objetivo no válido, los cuadrados en diagonal desocupados FACILITAN un movimiento, y si cualquier movimiento válido expone al Rey, entonces ese movimiento también es inválido.
Una vez más, la tentación podría ser poner las reglas generalmente aplicables en la clase base de ChessPiece (por ejemplo, ¿expone un movimiento al Rey?), Pero aplicar esa regla requiere conocimiento del estado general del juego, es decir, la colocación de otras piezas, por lo que es más pertenece apropiadamente como un comportamiento generalizado de la clase TChessGame , imho
Además de mover objetivos, las piezas también deben indicar CaptureTargets, que en el caso de la mayoría de las piezas es el mismo, pero en algunos casos muy diferente, el peón es un buen ejemplo. Pero una vez más, la cual, si alguna, de todas las capturas potenciales es efectiva para cualquier movimiento dado, es una evaluación de las reglas del juego, no el comportamiento de una pieza o clase de piezas.
Como es el caso en el 99% de tales situaciones (ime - ymmv), el dilema quizás se resuelva mejor cambiando el diseño de la clase para representar mejor el problema que se está modelando, no encontrando una manera de encajar el diseño de la clase en una organización de archivos arbitraria.
No parece que TChessBoard.FBoard tenga que ser una variedad de TChessPiece, también puede ser de TObject y descargarse en ChessPiece.pas.
Otro enfoque:
Haga su tablero de tBaseChessPiece. Es abstracto pero contiene las definiciones a las que debe referirse.
El funcionamiento interno está en tChessPiece que desciende de tBaseChessPiece.
Estoy de acuerdo en que el manejo que hace Delphi de las cosas que se refieren entre sí es malo, sobre la peor característica del lenguaje. Hace tiempo que solicito declaraciones a plazo que funcionen a través de unidades. El compilador tendría la información que necesita, no rompería la naturaleza de un pase que lo hace tan rápido.
Sería mejor mover la clase ChessPiece a la unidad ChessBoard.
Si por alguna razón no puede, intente poner una cláusula de usos en la parte de implementación en una unidad y deje la otra en la parte de interfaz.
Una solución podría ser la introducción de una tercera unidad que contenga declaraciones de interfaz (IBoard e IPiece).
Luego, las secciones de la interfaz de las dos unidades con declaraciones de clase pueden referirse a la otra clase por su interfaz:
TChessBoard = class(TInterfacedObject, IBoard)
private
FBoard : array [1..8, 1..8] of IPiece;
...
end;
y
TChessPiece = class abstract(TInterfacedObject, IPiece)
public
procedure GetMoveTargets (BoardPos: TPoint; const Board: IBoard;
MoveTargetList: TList <TPoint>);
...
end;
(El modificador const en GetMoveTargets evita el conteo de referencias innecesarias)