Как избежать круговой ссылки на единицу измерения?

Представьте себе следующие два класса шахматной игры:

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.

Как я могу избежать циклической ссылки на единицу, с которой я сталкиваюсь здесь (каждая единица необходима в разделе интерфейса другой единицы)?


person jpfollenius    schedule 16.08.2009    source источник


Ответы (9)


Измените модуль, который определяет 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, где они будут доступны обоим.

person skamradt    schedule 17.08.2009
comment
Принято это, так как оно короткое и дает решение моего вопроса. Я проголосовал за все остальные хорошие ответы. - person jpfollenius; 24.08.2009
comment
Я получаю сообщение об ошибке E2086 Type 'tBaseChessBoard' еще не полностью определено. - person Z80; 22.10.2017
comment
@Alaun, это был не полностью рабочий фрагмент, достаточно показать путь к решению. В tBaseChessBoard должны быть методы, необходимые для GetMoveTargets, но реализованные как абстрактные или виртуальные. - person skamradt; 29.11.2017

Модули Delphi не "принципиально сломаны". То, как они работают, способствует феноменальной скорости компилятора и способствует чистому дизайну классов.

Возможность распределять классы по модулям так, как это позволяет Prims/.NET, является подходом, который, возможно, в корне нарушен, поскольку он способствует хаотической организации классов, позволяя разработчику игнорировать необходимость правильного проектирования своей структуры, способствуя наложению произвольных правила структуры кода, такие как «один класс на единицу», которые не имеют технических или организационных достоинств в качестве универсального изречения.

В этом случае я сразу же заметил идиосинкразию в дизайне классов, возникающую из-за этой дилеммы круговых ссылок.

То есть, зачем элементу когда-либо нужна ссылка на доску?

Если фигура взята с доски, такая ссылка не имеет смысла, или, возможно, действительные «цели перемещения» для удаленной фигуры действительны только для этой фигуры в качестве «стартовой позиции» в новой игре? Но я не думаю, что это имеет смысл как нечто иное, как произвольное обоснование случая, требующего, чтобы GetMoveTargets поддерживал вызов с нулевой ссылкой на правление.

Конкретное размещение отдельной фигуры в любой момент времени является свойством отдельной игры в шахматы, а также ДЕЙСТВИТЕЛЬНЫМИ ходами, которые могут быть ВОЗМОЖНЫМИ для любой данной фигуры зависят от размещения ДРУГИХ фигур в игре.

TChessPiece.GetMoveTargets не требует сведений о текущем состоянии игры. За это отвечает TChessGame. И TChessPiece не нуждается в ссылке на игру или на доску, чтобы определить действительные цели хода из заданной текущей позиции. Ограничения доски (8 рангов и файлов) являются константами домена, а не свойствами данного экземпляра доски.

Таким образом, требуется TChessGame, инкапсулирующая знания, которые сочетают в себе понимание доски, фигур и, что особенно важно, правил, но доске и фигурам не нужны знания друг о друге ИЛИ об игре. .

Может показаться заманчивым поместить правила, относящиеся к разным фигурам, в класс для самого типа фигуры, но имхо это ошибка, поскольку многие правила основаны на взаимодействии с другими фигурами, а в некоторых случаях и с конкретными типами фигур. Такое поведение «в целом» требует определенного контроля (читай: обзора) общего состояния игры, что не подходит для определенного класса фигур.

например TChessPawn может определить, что допустимой целью хода является одна или две клетки вперед или одна клетка по диагонали вперед, если любая из этих диагональных клеток занята. Однако, если движение пешки ставит короля в положение ШАХ, то пешка вообще не может двигаться.

Я бы подошел к этому, просто позволив классу пешки указать все ВОЗМОЖНЫЕ цели хода - на 1 или 2 поля вперед и на оба поля по диагонали вперед. Затем TChessGame определяет, какие из них являются допустимыми, исходя из занятости этих целей перемещения и состояния игры. 2 поля вперед возможны только в том случае, если пешка находится на своей исходной горизонтали, занятые поля вперед БЛОКИРУЮТ ход = недопустимая цель, незанятые диагональные клетки ОБЛЕГЧАЮТ ход, и если любой другой допустимый ход обнажает короля, то этот ход также недействителен.

Опять же, может возникнуть искушение поместить общеприменимые правила в базовый класс TChessPiece (например, раскрывает ли данный ход короля?), но применение этого правила требует понимания общего состояния игры, т.е. других фигур - так что это более правильно относится к обобщенному поведению класса TChessGame, imho

В дополнение к целям перемещения фигуры также должны указывать CaptureTargets, что в случае большинства фигур одно и то же, но в некоторых случаях совершенно другое - хороший пример - пешка. Но опять же, что из всех потенциальных взятий, если оно вообще есть, является эффективным для любого данного хода, так это, имхо, оценка правил игры, а не поведения фигуры или класса фигур.

Как и в случае с 99% таких ситуаций (ime - ymmv), дилемма, возможно, лучше решается путем изменения дизайна класса, чтобы лучше представить моделируемую проблему, а не путем поиска способа впихнуть дизайн класса в произвольную файловую организацию.

person Deltics    schedule 16.08.2009
comment
Компилятор ДЕЙСТВИТЕЛЬНО применяет один класс на единицу при работе с формами. Таким образом, вы должны прыгать через обручи, чтобы позволить двум формам сотрудничать. Часто это означает, что это должна быть одна форма, но иногда вам нужны независимо движущиеся части. - person Loren Pechtel; 17.08.2009
comment
Что нечисто в дизайне класса, который, например, содержит отношения M:N? Должен ли QA отвергать зависимости M:N в дизайне классов по той причине, что они приведут к хаосу? - person mjn; 17.08.2009
comment
Всего одно замечание по поводу предлагаемого вами дизайна: вам нужно будет добавить большой условный оператор в свой класс TChessGame, по одной ветви для каждой фигуры. В этом случае части потеряют всю функциональность. Вот чего я хотел избежать. Но я признаю, что мой дизайн может быть не самым лучшим. Это была только первая попытка. Может быть, мы получим другие мнения по этому поводу. - person jpfollenius; 17.08.2009
comment
@Loren: это принудительное применение компилятора на самом деле хорошо. Это заставляет вас переосмыслить свои зависимости. - person Jeroen Wiert Pluimers; 17.08.2009
comment
@Loren: компилятор не применяет ничего подобного. Механизм потоковой передачи свойств VCL в сотрудничестве с компоновщиком и разработчиком формы задает один класс FORM на единицу, но вы можете ввести в единицу формы дополнительные классы, не относящиеся к форме, как и в любой другой единице. Независимо перемещаемые части формы, возможно, в любом случае должны быть компонентами, и если вы хотите, чтобы поверхность визуального дизайна создавала эти компоненты, вы можете использовать фреймы для создания этих частей. Но да, только одна часть на единицу кадра (т.е. формы). - person Deltics; 17.08.2009
comment
@Smasher: я не разработал полный дизайн, но я не думаю, что вы можете избежать условного ветвления при применении наборов правил, к чему сводится настольная игра. Вы можете использовать объектно-ориентированную архитектуру, чтобы свести к минимуму это ветвление, и, я думаю, в этом случае, безусловно, есть большой потенциал для этого. Я не хотел сказать, что объектно-ориентированный подход был полностью неуместным. - person Deltics; 17.08.2009
comment
Аргумент скорости компиляции имел смысл еще 15 лет назад. Сегодня у меня никогда не было проблем со скоростью компиляции на любом языке. Кажется, что только разработчики Delphi готовы отказаться от гибкости, чтобы выиграть несколько миллисекунд на компиляцию. Судя по тому, что я чаще всего вижу в реальном мире, это ограничение вообще не способствует чистоте объектно-ориентированного проектирования. Это просто заставляет людей создавать гигантские файлы .pas с 2934938 классами, которые могут видеть друг друга и даже иметь доступ к закрытым членам друг друга. - person Wouter van Nifterick; 19.08.2009
comment
Вы правы. Файлы модулей Delphi не повреждены, но ваше утверждение о том, что один класс на модуль/файл не имеет технических или организационных преимуществ, ошибочно. Его достоинство заключается в скорости понимания. Как разработчики, мы тратим 80-90% нашего времени на чтение и понимание исходного кода и только 10-20% на его модификацию. Просеивание 30 незначительно связанных классов в файле для изменения только одного из них — пустая трата времени. Библиотеки Delphi были разработаны с несколькими классами в файле, но компилятор, конечно же, никому этого не навязывает. Как указывает @Wouter, это только способствует тесной связи. - person Kenneth Cochran; 31.08.2010
comment
@codeelegance: 30 классов в одном модуле, конечно, заходят слишком далеко. Но равное разделение этих 30 классов на 30 отдельных блоков также заходит слишком далеко, особенно когда классы связаны между собой. Например, классы, которые имеют связанные коллекции (класс коллекции и соответствующий класс элементов коллекции), часто будут иметь тесную связь — необходимость прыгать между разными единицами может стать таким же препятствием для понимания, как необходимость прыгать между внутри< /i> единица. Это один из тех случаев, когда абсолютное правило — так или иначе — в некоторых случаях будет неверным. - person Deltics; 01.09.2010
comment
Я не имел в виду, что следует слепо придерживаться 1 класса на единицу. Только то, что у него есть очень реальные и значительные преимущества, которые вы упускаете из виду. В дополнение к удобочитаемости это также снижает вероятность конфликтов слияния и необходимость перекомпиляции при изменении класса. Также большинство сообщений компилятора сообщают только имя файла и номер строки. С 1 классом на единицу сразу видно, в каком классе есть ошибка, еще до того, как вы откроете файл. Да, всегда есть исключения из правил. Я просто думаю, что 1 класс на единицу - это правило, а не исключение. - person Kenneth Cochran; 02.09.2010
comment
@codeelegance: есть только одно правило, которое имеет смысл: руководствуйтесь здравым смыслом. Все остальное — это приглашение вербовать людей в Культ Карго. Мне не повезло работать над проектами, в которых точно такие же рационализации применялись к (первоклассным) функциям. Каждая функция в своем собственном модуле, где имя модуля было [FunctionName]Unit. Конечно, отслеживать изменения очень просто, но повседневная работа — это адская боль, со списками использования длиной в вашу руку и непростым способом включить функции, связанные с областью действия. - person Deltics; 02.09.2010
comment
..продолж. Точно так же при использовании класса диаграмм я не хочу также помнить об использовании дюжины других модулей, чтобы включить классы формы, инструментов и рендеринга в область видимости. Я уже сказал, что использую диаграммы, черт возьми! :) - person Deltics; 02.09.2010

Одним из решений может быть введение третьего модуля, который содержит объявления интерфейсов (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;

(Модификатор const в GetMoveTargets позволяет избежать ненужного подсчета ссылок)

person mjn    schedule 16.08.2009
comment
@ user928177263 может быть. но однажды изученное решение может быть применено к большим вариациям одной и той же проблемы. - person mjn; 22.10.2017

Было бы лучше переместить класс ChessPiece в модуль ChessBoard.
Если по какой-то причине вы не можете, попробуйте поместить одно предложение uses в часть реализации в одном модуле, а другое оставить в часть интерфейса.

person Nick Dandoulakis    schedule 16.08.2009
comment
Что касается вашего второго пункта: как я уже упоминал, это не работает, потому что мне нужно определение из другого модуля в разделе интерфейса! - person jpfollenius; 16.08.2009
comment
О, я пропустил это :-) Действительно ли ChessPiece необходим, чтобы знать ChessBoard, или наоборот? - person Nick Dandoulakis; 16.08.2009
comment
Ну, шахматная фигура должна решить, куда она может двигаться (будут подклассы для разных фигур), и ей нужно знать доску, чтобы определить, куда она может пойти. Другое направление довольно ясно. - person jpfollenius; 16.08.2009
comment
Может быть, у вас должен быть TChessController для перемещения фигур? - person Harriv; 17.08.2009

С помощью Delphi Prism вы можете распределить свои пространства имен по отдельным файлам, так что там вы сможете решить проблему простым способом.

То, как работают модули, просто принципиально не соответствует их текущей реализации Delphi. Только взгляните, как «db.pas» должен иметь TField, TDataset, TParam и т. д. в одном чудовищном .pas-файле, потому что их интерфейсы ссылаются друг на друга.

В любом случае, вы всегда можете переместить код в отдельный файл и включить его, например, в {$include ChessBoard_impl.inc}. Таким образом, вы можете разделить материал по файлам и иметь отдельные версии через ваш vcs. Тем не менее, просто немного неудобно редактировать файлы таким образом.

Лучшим долгосрочным решением было бы убедить Embarcadero отказаться от некоторых идей, которые имели смысл в 1970 году, когда родился Паскаль, но которые в наши дни не более чем заноза в заднице для разработчиков. Одним из них является однопроходный компилятор.

person Wouter van Nifterick    schedule 16.08.2009
comment
Облом, что люди просто массово голосуют против, не комментируя, с какой частью они не согласны. - person Wouter van Nifterick; 20.08.2009
comment
+1 за предложение компилятора с одним проходом стать историей. Я предполагаю, что среднее увеличение времени компиляции будет не более 5% для двухпроходного компилятора. Может кто знает точные цифры :) ? - person mjn; 20.08.2009
comment
Файлы модулей не повреждены. Единственное, что препятствует разделению этих огромных файлов .pas, — это обратная совместимость. Многого из этой связи можно было бы избежать, свободно используя интерфейсы. Я согласен с однопроходным компилятором. Это налагает ненужные требования на разработчика и предотвращает некоторые очень полезные оптимизации во время выполнения, для выполнения которых требуется несколько проходов. - person Kenneth Cochran; 02.09.2010

Не похоже, что TChessBoard.FBoard должен быть массивом TChessPiece, он также может состоять из TObject и быть пониженным в ChessPiece.pas.

person Ozan    schedule 16.08.2009
comment
-1 Нет, не может. В TChessBoard есть методы, которые вызывают, например, FBoards[I, J].GetMoveTargets(...). Вдобавок к этому понижение не является чистым решением, которое я искал. - person jpfollenius; 16.08.2009
comment
тогда почему вы не сказали об этом в своем вопросе? Я просто дал вам один простой совет, не нужно минусовать меня, потому что я не знал о ваших классах. - person Ozan; 17.08.2009
comment
Это не причина, по которой я проголосовал против. Причина, по которой я проголосовал против, заключается в том, что я не считаю решение с отрицательным значением чистым решением. Но поскольку я явно не просил чистого объектно-ориентированного решения, я беру свой отрицательный голос. Извини за это. - person jpfollenius; 17.08.2009

Другой подход:

Сделайте свою доску из tBaseChessPiece. Он абстрактный, но содержит определения, на которые вам нужно ссылаться.

Внутренняя работа находится в tChessPiece, который происходит от tBaseChessPiece.

Я согласен с тем, что Delphi плохо справляется с вещами, которые ссылаются друг на друга, — пожалуй, худшая особенность языка. Я давно призывал к опережающим объявлениям, которые работают между подразделениями. Компилятор получил бы необходимую информацию, он не нарушил бы однопроходную природу, которая делает его таким быстрым.

person Loren Pechtel    schedule 16.08.2009

как насчет этого подхода:

блок шахматной доски:

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;

...

В этом подходе модуль шахматной доски будет включать ссылку на модуль фигур только в разделе реализации (из-за метода, который фактически будет создавать объекты), а модуль фигур будет иметь ссылку на модуль шахматной доски в интерфейсе. Если я не ошибаюсь, это легко справится с вашей проблемой...

person Guy    schedule 18.08.2009
comment
да, это почти то же самое решение, которое уже было предложено, только с обменом классами. Кстати, почему бы вам не использовать отступы, чтобы сделать код более читабельным? Просто используйте отступ в 4 пробела - person jpfollenius; 18.08.2009

Получить TChessBoard от TObject

TChessBoard = класс (TObject)

затем вы можете объявить процедуру GetMoveTargets (BoardPos : TPoint; Board : TObject; MoveTargetList : TList );

когда вы вызываете процесс, используйте SELF в качестве объекта Board (если вы вызываете его оттуда), тогда вы можете ссылаться на него с помощью

(Доска как TChessBoard). и получить доступ к свойствам и т. д. из этого.

person hikari    schedule 03.05.2012