Я боролся с проблемой в проекте Java о циклических ссылках. Я пытаюсь смоделировать реальную ситуацию, в которой кажется, что рассматриваемые объекты взаимозависимы и должны знать друг о друге.
Проект представляет собой общую модель игры в настольную игру. Основные классы неспецифичны, но расширены, чтобы иметь дело со спецификой шахмат, нардов и других игр. Я кодировал это как апплет 11 лет назад с полдюжины разных игр, но проблема в том, что он полон циклических ссылок. Я реализовал это тогда, поместив все переплетенные классы в один исходный файл, но я понял, что это плохая форма в Java. Теперь я хочу реализовать то же самое, что и приложение для Android, и хочу делать все правильно.
Классы:
RuleBook: объект, который может быть опрошен для таких вещей, как начальная раскладка доски, другая начальная информация о состоянии игры, например, кто двигается первым, ходы, которые доступны, что происходит с состоянием игры после предложенного хода, и оценка текущая или предлагаемая позиция совета.
Доска: простое представление игровой доски, на которой можно указать движение.
MoveList: список ходов. Это двойная цель: выбор доступных ходов в данный момент или список ходов, которые были сделаны в игре. Его можно разбить на два почти идентичных класса, но это не относится к вопросу, который я задаю, и может усложнить его.
Ход: один ход. Он включает в себя все, что касается движения, в виде списка атомов: возьмите отсюда кусок, положите его туда, удалите отснятый кусок.
Состояние: полная информация о состоянии игры. Не только позиция Правления, но и MoveList, и другая информация о состоянии, например, кто должен двигаться сейчас. В шахматах можно было бы записать, были ли перемещены король и ладьи каждого игрока.
Например, имеется множество циклических ссылок: RuleBook необходимо знать о состоянии игры, чтобы определить, какие ходы доступны в данный момент, но Game State должен запросить RuleBook для начального начального макета и каких побочных эффектов сопровождают ход один раз. это сделано (например, кто движется дальше).
Я попытался организовать новый набор классов иерархически, с RuleBook наверху, так как он должен знать обо всем. Но это приводит к тому, что необходимо переместить множество методов в класс RuleBook (например, сделать ход), что делает его монолитным и не особенно отражает, каким должен быть RuleBook.
Так как правильно организовать это? Должен ли я превратить RuleBook в BigClassThatDoesAlmostEverythingInTheGame, чтобы избежать циклических ссылок и отказаться от попытки смоделировать игру в реальном мире? Или я должен придерживаться взаимозависимых классов и заставить компилятор каким-то образом их компилировать, сохраняя мою модель реального мира? Или есть какая-то очевидная действительная структура, которую мне не хватает?
Спасибо за любую помощь, вы можете дать!
источник
RuleBook
например , взятьState
в качестве аргумента аргумент и вернуть действительное значениеMoveList
, то есть «вот где мы сейчас находимся, что можно сделать дальше?»Ответы:
Сборщик мусора в Java не использует методы подсчета ссылок. Циркулярные ссылки не вызывают каких-либо проблем в Java. Время, потраченное на устранение совершенно естественных циклических ссылок в Java, - пустая трата времени.
Не обязательно. Если вы просто скомпилируете все исходные файлы одновременно (например,
javac *.java
), компилятор разрешит все прямые ссылки без проблем.Да. Ожидается, что классы приложений будут взаимозависимыми. Компиляция всех исходных файлов Java, которые принадлежат одному и тому же пакету одновременно, не является умным взломом, это именно то, как Java должна работать.
источник
Конечно, круговые зависимости являются сомнительной практикой с точки зрения проектирования, но они не запрещены, и с чисто технической точки зрения они даже не обязательно проблематичны , как вы, по-видимому, считаете их: они совершенно законны в В большинстве случаев они неизбежны в некоторых ситуациях, а в редких случаях их можно даже рассматривать как полезную вещь.
На самом деле, существует очень немного сценариев, в которых java-компилятор будет отрицать циклическую зависимость. (Примечание: может быть больше, я могу думать только о следующем прямо сейчас.)
В наследовании: у вас не может быть класса A, расширяющего класс B, который, в свою очередь, расширяет класс A, и совершенно разумно, что вы не можете иметь этого, поскольку альтернатива не имеет абсолютно никакого смысла с логической точки зрения.
Среди локальных классов метода: классы, объявленные в методе, могут не ссылаться друг на друга. Вероятно, это не что иное, как ограничение java-компилятора, возможно, потому, что способность делать такие вещи недостаточно полезна, чтобы оправдать дополнительную сложность, которая должна идти в компилятор для ее поддержки. (Большинство Java-программистов даже не знают о том, что вы можете объявить класс в методе, не говоря уже о том, чтобы объявить несколько классов, и затем эти классы циклически ссылаются друг на друга.)
Поэтому важно осознать, что стремление минимизировать циклические зависимости - это стремление к чистоте проектирования, а не техническое исправление.
Насколько я знаю, не существует редукционистского подхода к устранению циклических зависимостей, а это означает, что не существует рецепта, состоящего из простых предопределенных «простых задач» для взятия системы с циклическими ссылками, применения их одна за другой и завершения с системой, свободной от циркулярных ссылок. Вы должны сосредоточиться на работе и выполнить шаги по рефакторингу, которые зависят от характера вашего дизайна.
В конкретной ситуации, которая у вас под рукой, мне кажется, что вам нужна новая сущность, возможно, называемая «Game» или «GameLogic», которая знает все другие сущности (без того, чтобы другие сущности знали об этом, ) чтобы другие лица не знали друг друга.
Например, мне кажется необоснованным, что ваша сущность RuleBook должна что-либо знать о сущности GameState, потому что книга правил - это то, с чем мы консультируемся, чтобы играть, а не то, что принимает активное участие в игре. Таким образом, именно эта новая сущность «Игра» должна обратиться к книге правил и состоянию игры, чтобы определить, какие ходы доступны, и это устраняет круговые зависимости.
Теперь, я думаю, я могу догадаться, в чем заключается ваша проблема с этим подходом: кодирование сущности «Игра» в не зависящей от игры форме будет очень трудным, поэтому вы, скорее всего, в конечном итоге получите не один, а два сущности, которые должны будут иметь индивидуальные реализации для каждого типа игры: сущность «Книга правил» и сущность «Игра». Что, в свою очередь, противоречит цели создания сущности RuleBook. Что ж, все, что я могу сказать по этому поводу, это то, что, может быть, просто, может быть, ваше первоначальное стремление написать систему, которая может играть в различные типы игр, может быть благородным, но, возможно, плохо продуманным. Если бы я был на вашем месте, я бы сосредоточился на использовании общего механизма для отображения состояния всех различных игр и общего механизма для получения пользовательского ввода для всех этих игр,
источник
Теория игр рассматривает игры как список предыдущих ходов (типы значений, включая тех, кто в них играл) и функцию ValidMoves (previousMoves)
Я попытался бы следовать этому шаблону для части игры, не связанной с пользовательским интерфейсом, и относиться к таким вещам, как установка доски, как к ходам.
пользовательский интерфейс может быть стандартным ООП с одним способом ссылки на логику
Обновление для сокращения комментариев
Рассмотрим шахматы. Игры в шахматы обычно записываются в виде списков ходов. http://en.wikipedia.org/wiki/Portable_Game_Notation
Список ходов определяет полное состояние игры гораздо лучше, чем рисунок доски.
Скажем, например, мы начинаем создавать объекты для Board, Piece, Move и т. Д. И таких методов, как Piece.GetValidMoves ()
сначала мы видим, что у нас должна быть часть, указывающая на доску, но затем мы рассмотрим рокировку. что вы можете сделать, только если вы еще не передвинули своего короля или ладью. Поэтому нам нужен флаг MovedAlready на короле и ладьях. Точно так же пешки могут передвигаться на 2 клетки с первого хода.
Затем мы видим, что при рокировке действительный ход короля зависит от существования и состояния ладьи, поэтому на доске должны быть фигуры и ссылки на них. мы входим в вашу круговую проблему реферирования.
Однако, если мы определим Move как неизменную структуру, а состояние игры как список предыдущих ходов, мы обнаружим, что эти проблемы исчезают. Чтобы убедиться, что рокировка действительна, мы можем проверить список ходов существования ходов замка и короля. Чтобы узнать, может ли пешка пройти мимо, мы можем проверить, не сделала ли другая пешка двойной ход ранее. Ссылки не нужны, кроме правил -> Переместить
Теперь в шахматах есть статическая доска, и peices всегда настроены одинаково. Но допустим, у нас есть вариант, где мы разрешаем альтернативную настройку. возможно, опуская некоторые фигуры в качестве гандикапа.
Если мы добавим установочные ходы как ходы «из прямоугольника в квадрат X» и адаптируем объект «Правила», чтобы понять этот ход, то мы все равно сможем представить игру как последовательность ходов.
Точно так же, если в вашей игре сама доска не является статичной, скажем, мы можем добавить квадраты в шахматы или удалить квадраты с доски, чтобы их нельзя было перемещать. Эти изменения также могут быть представлены как Move без изменения общей структуры вашего движка правил или необходимости ссылаться на объект BoardSetup аналогичного
источник
boardLayout
является функцией всехpriorMoves
(то есть, если бы мы поддерживали его как состояние, ничто не было бы добавлено, кроме каждогоthisMove
). Следовательно, предложение Эвана, по сути, «разрезает посредника» - действительные движет прямую функцию всех предшествующих, а неvalidMoves( boardLayout( priorMoves ) )
.Стандартный способ удаления циклической ссылки между двумя классами в объектно-ориентированном программировании состоит в том, чтобы ввести интерфейс, который затем может быть реализован одним из них. Таким образом, в вашем случае вы могли бы
RuleBook
сослаться наState
который затем ссылается наInitialPositionProvider
(который будет интерфейсом, реализованнымRuleBook
). Это также облегчает тестирование, потому что вы можете создать объект,State
который использует другую (предположительно более простую) исходную позицию для целей тестирования.источник
Я считаю, что круговые ссылки и объект бога в вашем случае можно легко удалить, отделив управление игровым потоком от моделей состояния и правил игры. Делая это, вы, вероятно, получите большую гибкость и избавитесь от ненужной сложности.
Я думаю, что у вас должен быть контроллер («мастер игры», если хотите), который контролирует ход игры и обрабатывает фактические изменения состояния, а не возлагает ответственность на книгу правил или состояние игры.
Объект игрового состояния не должен изменять себя или знать правила. Классу просто нужно предоставить модель легко обрабатываемых (созданных, проверенных, измененных, сохраненных, зарегистрированных, скопированных, кэшированных и т. Д.) И эффективных объектов состояния игры для остальной части приложения.
Книга правил не должна знать или играть в какую-либо игру. Для того, чтобы определить, какие ходы являются законными, нужно только иметь представление о состоянии игры, и ему нужно ответить только с помощью итогового игрового состояния, когда его спросят, что происходит, когда ход применяется к игровому состоянию. Это может также обеспечить начальное игровое состояние, когда запрашивается начальная раскладка.
Контроллер должен знать о состоянии игры и книге правил и, возможно, о некоторых других объектах игровой модели, но он не должен вмешиваться в детали.
источник
Я думаю, что проблема здесь в том, что вы не дали четкого описания того, какие задачи должны решаться какими классами. Я опишу то, что я считаю хорошим описанием того, что должен делать каждый класс, а затем приведу пример общего кода, который иллюстрирует идеи. Мы увидим, что код менее связан, и поэтому он не имеет циклических ссылок.
Давайте начнем с описания того, что делает каждый класс.
GameState
Класс должен содержать только информацию о текущем состоянии игры. Он не должен содержать какую-либо информацию о том, какие состояния игры в прошлом или какие ходы возможны в будущем. Он должен содержать только информацию о том, какие фигуры находятся на каких клетках в шахматах, и сколько и какие типы шашек на каких точках в нардах. ОниGameState
должны будут содержать некоторую дополнительную информацию, например, информацию о рокировке в шахматах или о кубе удвоения в нардах.Move
Класс немного сложнее. Я бы сказал, что могу указать ход для игры, указавGameState
результат, полученный в результате хода. Таким образом, вы можете представить, что движение может быть реализовано какGameState
. Тем не менее, в go (например) вы можете представить, что намного проще указать ход, указав одну точку на доске. Мы хотим, чтобы нашMove
класс был достаточно гибким, чтобы справиться с любым из этих случаев. Следовательно,Move
класс на самом деле будет интерфейсом с методом, который выполняет предварительное перемещениеGameState
и возвращает новое после перемещенияGameState
.Теперь
RuleBook
класс отвечает за знание всего о правилах. Это можно разбить на три вещи. Он должен знать, что такое начальныйGameState
, он должен знать, какие ходы законны, и он должен уметь определить, выиграл ли один из игроков.Вы также можете создать
GameHistory
класс, чтобы отслеживать все шаги, которые были сделаны, и все,GameStates
что произошло. Новый класс необходим, потому что мы решили, что одинGameState
не должен нести ответственность за знание всего,GameState
что было до него.Это завершает классы / интерфейсы, которые я буду обсуждать. У вас также есть
Board
класс. Но я думаю, что доски в разных играх достаточно разные, поэтому трудно понять, что в общем можно сделать с досками. Теперь я продолжу давать универсальные интерфейсы и реализовывать универсальные классы.Первый есть
GameState
. Поскольку этот класс полностью зависит от конкретной игры, нет универсальногоGamestate
интерфейса или класса.Дальше есть
Move
. Как я уже сказал, это может быть представлено интерфейсом, который имеет единственный метод, который принимает состояние перед перемещением и создает состояние после перемещения. Вот код для этого интерфейса:Обратите внимание, что есть параметр типа. Это связано с тем, что, например,
ChessMove
необходимо знать подробности предварительного ходаChessGameState
. Так, например, объявление классаChessMove
будетclass ChessMove extends Move<ChessGameState>
,где вы уже определили
ChessGameState
класс.Далее я буду обсуждать общий
RuleBook
класс. Вот код:Опять же, есть параметр типа для
GameState
класса. ПосколькуRuleBook
предполагается, что оно знает начальное состояние, мы создали метод для определения начального состояния. Так какRuleBook
предполагается, что ходы являются законными, у нас есть методы, чтобы проверить, является ли ход законным в данном состоянии, и дать список законных ходов для данного состояния. Наконец, есть метод оценкиGameState
. Обратите внимание, чтоRuleBook
ответственность должна быть только за описание того, выиграл ли тот или иной игрок, но не за то, кто находится в лучшем положении в середине игры. Решение, кто находится в лучшем положении, является сложной вещью, которую следует перевести в свой класс. Следовательно,StateEvaluation
класс на самом деле представляет собой простое перечисление, заданное следующим образом:Наконец, давайте опишем
GameHistory
класс. Этот класс отвечает за запоминание всех позиций, которые были достигнуты в игре, а также ходов, которые были сыграны. Главное, что он должен уметь делать - это записыватьMove
как проигранные. Вы также можете добавить функциональность для удаленияMove
s. У меня есть реализация ниже.Наконец, мы могли бы представить себе
Game
класс, чтобы связать все вместе.Game
Предполагается, что этот класс предоставляет методы, позволяющие людям увидеть, что это за токGameState
, увидеть, кто, если есть, посмотреть, какие ходы можно сыграть, и сыграть ход. У меня есть реализация нижеОбратите внимание на этот класс, что
RuleBook
не несет ответственности за знание того, что токGameState
. ЭтоGameHistory
работа. Таким образом, онGame
спрашивает,GameHistory
каково текущее состояние, и дает эту информацию,RuleBook
когдаGame
нужно сказать, каковы законные шаги или если кто-то победил.В любом случае, смысл этого ответа заключается в том, что, как только вы сделали разумное определение того, за что отвечает каждый класс, и вы сделали каждый класс сосредоточенным на небольшом числе обязанностей, и вы назначаете каждую ответственность уникальному классу, то классы имеют тенденцию быть отделенными, и все становится легко закодировать. Надеюсь, это видно из приведенных мною примеров кода.
источник
По моему опыту, циклические ссылки обычно указывают на то, что ваш дизайн не продуман.
В вашем дизайне я не понимаю, почему RuleBook нужно «знать» о государстве. Конечно, он может получать состояние в качестве параметра некоторого метода, но зачем ему знать (т.е. хранить в качестве переменной экземпляра) ссылку на состояние? Это не имеет смысла для меня. RuleBook не нужно «знать» о состоянии какой-либо конкретной игры, чтобы выполнять свою работу; правила игры не меняются в зависимости от текущего состояния игры. Так что либо вы разработали это неправильно, либо вы разработали это правильно, но объясняете это неправильно.
источник
Циркулярная зависимость не обязательно является технической проблемой, но ее следует рассматривать как запах кода, который обычно является нарушением принципа единой ответственности .
Ваша круговая зависимость проистекает из того факта, что вы пытаетесь сделать слишком много из своего
State
объекта.Любой объект с состоянием должен предоставлять только те методы, которые напрямую связаны с управлением этим локальным состоянием. Если для этого требуется нечто большее, чем самая простая логика, то, вероятно, его следует разбить на более крупную модель. Некоторые люди имеют разные мнения по этому поводу, но, как правило, если вы делаете что-то большее, чем сборщики и установщики данных, вы делаете слишком много.
В этом случае вам лучше иметь
StateFactory
, который может знать оRulebook
. Возможно, у вас есть другой класс контроллера, который использует вашStateFactory
для создания новой игры.State
определенно не должен знать оRulebook
.Rulebook
может знать оState
зависимости от реализации ваших правил.источник
Есть ли необходимость в привязке объекта книги правил к определенному игровому состоянию, или было бы более разумно иметь объект книги правил с методом, который, учитывая игровое состояние, сообщит, какие ходы доступны из этого состояния (и, сообщив об этом, ничего не помните о данном государстве)? Если нет ничего, что можно было бы получить, если объект, о котором спрашивают о доступных ходах, сохраняет память о состоянии игры, ему не нужно сохранять ссылку.
Возможно, в некоторых случаях было бы полезно поддерживать состояние объекта оценки правил. Если вы думаете, что такая ситуация может возникнуть, я бы предложил добавить класс "рефери", а в книге правил должен быть метод "createReferee". В отличие от книги правил, которая не заботится о том, спрашивается ли она об одной игре или пятидесяти, объект рефери должен был бы исполнять одну игру. Не следует ожидать, что он будет инкапсулировать все состояния, связанные с игрой, которую он исполняет, но может кэшировать любую информацию об игре, которую он сочтет полезной. Если игра поддерживает функциональность «отменить», может быть полезно, чтобы судья включил средство для создания объекта «снимка», который может быть сохранен вместе с более ранними состояниями игры; этот объект должен,
Если может потребоваться некоторая связь между аспектами кода, касающимися обработки правил и состояния игры, использование объекта-рефери позволит сохранить такую связь вне основной книги правил и классов состояния игры. Это также может позволить новым правилам учитывать аспекты игрового состояния, которые класс игрового состояния не счел бы релевантными (например, если было добавлено правило, которое гласило: «Объект X не может делать Y, если он когда-либо находился в местоположении Z». msgstr "судья может быть изменен, чтобы отслеживать, какие объекты находились в точке Z, без необходимости изменять класс игрового состояния).
источник
Надлежащий способ справиться с этим - использовать интерфейсы. Вместо того, чтобы два класса знали друг о друге, пусть каждый класс реализует интерфейс и ссылается на другой класс. Допустим, у вас есть класс A и класс B, которые должны ссылаться друг на друга. Пусть класс A реализует интерфейс A, а класс B реализует интерфейс B, тогда вы можете ссылаться на интерфейс B из класса A и интерфейс A из класса B. Класс A может быть в своем собственном проекте, как и класс B. Интерфейсы находятся в отдельном проекте. на которые ссылаются оба других проекта.
источник