2009-08-16 23 views
15

imaginar las dos clases siguientes de una partida de ajedrez:¿Cómo evitar la referencia circular de la unidad?

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 las dos clases que se definirán 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)?

Respuesta

11

cambiar la unidad que define TChessPiece a tener el siguiente aspecto:

TYPE 
    tBaseChessBoard = class; 

    TChessPiece = class 
    procedure GetMoveTargets (BoardPos : TPoint; Board : TBaseChessBoard; ...  
    ... 
    end;  

A continuación, modifique la unidad que define TChessBoard a tener el siguiente aspecto:

USES 
    unit_containing_tBaseChessboard; 

TYPE 
    TChessBoard = class(tBaseChessBoard) 
    private 
    FBoard : array [1..8, 1..8] of TChessPiece; 
    ... 
    end; 

Esto le permite pasar casos concretos a la pieza de ajedrez sin tener que preocuparse por una referencia circular. Dado que el tablero utiliza los Tchesspieces en su versión privada, realmente no tiene que existir antes de la declaración Tchesspiece, solo como marcador de posición. Cualquier variable de estado que tChessPiece debe conocer, por supuesto, debe colocarse en tBaseChessBoard, donde estarán disponibles para ambos.

+0

Aceptado esto, ya que es corto y proporciona una solución a mi pregunta. Voté todas las otras buenas respuestas. – jpfollenius

+0

Me aparece el error "Tipo E2086 'tBaseChessBoard' aún no está completamente definido". – Ampere

+0

@Alaun, esto no fue un snippit totalmente funcional, solo lo suficiente para mostrar el camino a una solución. El tBaseChessBoard debe tener métodos que sean necesarios para GetMoveTargets pero implementados como abstractos o virtuales. – skamradt

2

Sería mejor mover la clase ChessPiece a la unidad ChessBoard.
Si por alguna razón no puede, trate de poner uno usa la cláusula en la parte de implementación en una unidad y deje la otra en la parte de la interfaz.

+0

Con respecto a su segundo punto: como mencioné, esto no funciona porque necesito la definición de la otra unidad en la sección de interfaz. – jpfollenius

+1

Oh, eché de menos eso :-) ¿Realmente es necesario ChessPiece para * saber * ChessBoard, o al revés? –

+0

Bueno, la pieza de ajedrez debe decidir dónde se puede mover (habrá subclases para las diferentes piezas) y necesita conocer la tabla para determinar dónde puede ir. La otra dirección es bastante clara. – jpfollenius

16

Una solución podría ser la introducción de una tercera unidad que contenga declaraciones de interfaz (IBoard e IPiece).

Entonces las secciones de la interfaz de las dos unidades con declaraciones de clase puede 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 recuento de referencias innecesario)

+0

no es el uso de interfaces demasiado (en este caso) ??? – Ampere

+0

@ user928177263 tal vez. pero una vez que se aprende, la solución puede aplicarse a variaciones mayores del mismo problema – mjn

0

No parece que TChessBoard.FBoard debe ser una matriz de TChessPiece, también puede ser de TObject y puede ser desclasificado en Chess Piece.pas.

+0

-1 No, no puede. Hay métodos en TChessBoard que llaman, es decir, FBoards [I, J] .GetMoveTargets (...). Además de eso, un downcast no es la solución limpia que estaba buscando. – jpfollenius

+0

¿por qué no lo dijiste en tu pregunta? Solo te estaba dando una pista simple, no es necesario que me rechaces porque no sabía acerca de tus clases. – Ozan

+0

Esa no es la razón por la que rechacé. La razón por la que voté negativamente es que no considero una solución con una solución clara. Pero como no pedí explícitamente una solución limpia orientada a objetos, recupero mi voto. Lo siento por eso. – jpfollenius

1

Con Delphi Prism puede separar sus espacios de nombres en archivos separados, por lo que allí podría resolverlo de una manera limpia.

La forma en que funcionan las unidades simplemente se rompe con su implementación actual de Delphi. Basta con ver cómo "db.pas" necesitaba tener TField, TDataset, TParam, etc. en un monstruoso archivo .pas porque sus interfaces se referencian 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 esta manera puede dividir cosas sobre archivos y tener versiones separadas a través de sus vcs. Sin embargo, es un poco desagradable editar archivos de esa manera.

La mejor solución a largo plazo sería instar al embarcadero a deshacerse de algunas de las ideas que tenían sentido en 1970 cuando nació Pascal, pero que no son mucho más que un dolor de trasero para los desarrolladores de hoy en día. Un compilador de un solo paso es uno de esos.

+1

Bummer que la gente simplemente baja en masa sin comentar en qué parte no están de acuerdo. –

+2

+1 por sugerir que el compilador de un solo pase debe ser histórico. Supongo que el aumento promedio del tiempo de compilación no será más del 5% para un compilador de dos pasos. Tal vez alguien sabe las cifras exactas :)? – mjn

+1

Los archivos de la unidad no están "rotos". Lo único que impide que esos enormes archivos .pas se separen es la compatibilidad con versiones anteriores. Gran parte de ese acoplamiento podría evitarse mediante el uso liberal de interfaces. Sin embargo, estoy de acuerdo con el compilador de una sola pasada. Coloca requisitos innecesarios en el desarrollador y evita algunas optimizaciones de tiempo de ejecución muy útiles que requieren múltiples pases para lograr. –

0

Otro enfoque:

Haga su junta tBaseChessPiece. Es abstracto pero contiene las definiciones a las que necesita hacer referencia.

El funcionamiento interno está en tChessPiece que desciende de tBaseChessPiece.

Estoy de acuerdo en que el manejo de Delphi de cosas que se refieren entre sí es malo, acerca de la peor característica del lenguaje. Hace tiempo solicité declaraciones directas que funcionen en todas las unidades. El compilador tendría la información que necesita, no rompería la naturaleza de un solo paso que lo hace tan rápido.

22

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 limpios.

Ser capaz de distribuir clases sobre unidades de la manera que permite Prims/.NET es el enfoque que se puede decir que fundamentalmente se rompe, ya que promueve la organización caótica de clases al permitir al desarrollador ignorar la necesidad de diseñar adecuadamente su marco, promoviendo la imposición de reglas de estructura de código arbitrarias tales como "una clase por unidad", que no tiene ningún mérito técnico u organizativo como dictum universal.

En este caso, inmediatamente noté una idiosincrasia en el diseño de clase que surge de este dilema de referencia circular.

Es decir, ¿por qué una pieza alguna vez tiene alguna necesidad de hacer referencia a una placa?

Si una pieza es sacada de un tablero, tal referencia no tiene sentido, o tal vez los "MoveTargets" válidos para una pieza eliminada son solo los válidos para esa pieza como una "posición inicial" en un nuevo juego. Pero no creo que esto tenga sentido como algo más que una justificación arbitraria para un caso que exige que GetMoveTargets admita la invocación con una referencia de placa NIL.

La colocación particular de una pieza individual en un momento dado es una propiedad de un juego individual de ajedrez, y por igual las VÁLIDA mueve que puede ser POSIBLE para cualquier pieza dada dependen de la colocación de OTRAS piezas en el juego.

TChessPiece.GetMoveTargets no necesita el conocimiento del estado actual del juego. Esta es la responsabilidad de un TChessGame. Y un 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 determinada de la placa.

Así, un TChessGame se requiere que encapsula el conocimiento que combina el conocimiento de un tablero, las piezas y - especialmente - las reglas, pero el tablero y las piezas no necesitan el conocimiento de la otra o del juego.

Puede parecer tentador poner las reglas correspondientes a las diferentes piezas de 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 Tales comportamientos de "panorama general" requieren un grado de visión excesiva (léase: descripción general) del estado total del juego que no es apropiado en una clase de pieza específica.

p. Ej. un TChessPawn puede determinar que un objetivo de movimiento válido es uno o dos cuadrados hacia adelante o un cuadrado diagonalmente hacia delante si cualquiera de esos cuadrados diagonales está ocupado. Sin embargo, si el movimiento del peón expone al Rey a una situación de CHECK, entonces el peón no se puede mover en absoluto.

Me acercaría a esto simplemente permitiendo que la clase de peones indique todos los objetivos de movimiento POSIBLES: 1 o 2 casillas hacia delante y diagonalmente hacia adelante. El TChessGame determina cuál de estos es válido por referencia a la ocupación de esos objetivos de movimiento y estado del juego.2 cuadros hacia adelante solo son posibles si el peón está en su rango principal, los cuadros delanteros están ocupados BLOQUEAR un movimiento = objetivo inválido, cuadrados diagonales NO ocupados FACILITAR un movimiento, y si cualquier movimiento válido expone al Rey, ese movimiento tampoco es válido.

Una vez más, la tentación podría ser la de poner las normas de aplicación general en la base TChessPiece clase (por ejemplo, no exponga un movimiento dado el rey?), Pero la aplicación de esa norma requiere el conocimiento del estado del juego en general - es decir, la colocación de otras piezas - por lo que más bien pertenece como un comportamiento generalizado de la clase TChessGame, en mi humilde opinión

Además de mover los objetivos, las piezas también tienen que indicar CaptureTargets, que en el caso de la mayoría de las piezas es el mismo, pero en algunos casos bastante diferentes: el peón es un buen ejemplo. Pero, una vez más, la captura de todas las posibles capturas, si es que la hay, es efectiva para cualquier movimiento dado, es una evaluación de las reglas del juego, no del comportamiento de una pieza o clase de piezas.

Como es el caso en el 99% de estas situaciones (ime - ymmv) el dilema quizás se resuelva mejor cambiando el diseño de clase para representar mejor el problema modelado, no encontrando la manera de calzar el diseño de clase en un arbitrario organización de archivos.

+1

El compilador * HACE * imponer una clase por unidad al tratar con formularios. Por lo tanto, debe saltar a través de aros para permitir que dos formas cooperen. A menudo, esto significa que debe ser de una forma, pero a veces quieres piezas móviles independientes. –

+0

¿Qué es "sucio" en un diseño de clase que contiene relaciones M: N, por ejemplo? ¿Debe QA rechazar las dependencias M: N en el diseño de la clase, con la razón de que conducirán al caos? – mjn

+0

Solo un comentario sobre el diseño que propone: tendría que agregar una declaración condicional grande en su clase TChessGame, una rama para cada pieza. Las piezas perderían toda la funcionalidad en ese caso. Eso es lo que quería evitar. Pero admito que mi diseño podría no ser el mejor. Fue solo un primer intento. Tal vez obtengamos otras opiniones sobre eso. – jpfollenius

0

¿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 esta unidad de tablero de ajedrez aproach incluirá la referencia de la unidad de piezas sólo en la ejecución sección (debido al método que de hecho creará los objetos) y la unidad de piezas tendrá una referencia a la unidad de tablero de ajedrez en la interfaz. Si no estoy mal esta manejar su problema de una manera fácil ...

+0

sí, esa es prácticamente la misma solución que ya se propuso solo con clases intercambiadas. Por cierto: ¿por qué no usas sangrías para hacer que el código sea más legible? Solo usa indentación de 4 espacios – jpfollenius

0

Derivar TChessBoard de TObject

TChessBoard = class (TObject)

entonces se puede declarar GetMoveTargets procedimiento (BoardPos: TPoint; Junta: TObject; MoveTargetList: TList);

cuando se llama al proc, uso propio como el objeto Junta (si llama desde allí), entonces se puede hacer referencia a ella con

(Junta como TChessBoard). y acceder a las propiedades, etc. de eso.

Cuestiones relacionadas