Jaki jest właściwy sposób modelowania tej aktywności w świecie rzeczywistym, która wydaje się wymagać cyklicznych odniesień w OOP?

24

Walczyłem z problemem w projekcie Java dotyczącym referencji cyklicznych. Próbuję modelować rzeczywistą sytuację, w której wydaje się, że przedmiotowe obiekty są od siebie zależne i muszą się o sobie wiedzieć.

Projekt jest ogólnym modelem gry planszowej. Podstawowe klasy są niespecyficzne, ale zostały rozszerzone o specyfikę szachów, backgammon i innych gier. Kodowałem to jako aplet 11 lat temu za pomocą kilku różnych gier, ale problem polega na tym, że jest pełen okrągłych odnośników. Zaimplementowałem go wtedy, upychając wszystkie powiązane klasy w jednym pliku źródłowym, ale mam pomysł, że w Javie jest to zła forma. Teraz chcę zaimplementować coś podobnego do aplikacji na Androida i chcę robić wszystko poprawnie.

Zajęcia to:

  • RuleBook: obiekt, który może zostać zapytany o takie rzeczy, jak początkowy układ planszy, inne początkowe informacje o stanie gry, takie jak kto się porusza pierwszy, dostępne ruchy, co dzieje się ze stanem gry po proponowanym ruchu i ocena aktualne lub proponowane stanowisko zarządu.

  • Plansza: prosta reprezentacja planszy, którą można poinstruować, aby odzwierciedlała ruch.

  • MoveList: lista ruchów. Jest to dwojaki cel: wybór ruchów dostępnych w danym punkcie lub lista ruchów wykonanych w grze. Można go podzielić na dwie niemal identyczne klasy, ale nie ma to związku z pytaniem, które zadaję, i może to jeszcze bardziej skomplikować.

  • Ruch: pojedynczy ruch. Zawiera wszystko o ruchu jako listę atomów: podnieś kawałek stąd, połóż go tam, usuń stamtąd schwytany kawałek.

  • Stan: pełna informacja o stanie gry w toku. Nie tylko stanowisko zarządu, ale MoveList i inne informacje o stanie, takie jak kto ma się teraz przenieść. W szachach zapisuje się, czy król i wieże każdego gracza zostały przeniesione.

Istnieje wiele odnośników cyklicznych, na przykład: RuleBook musi wiedzieć o stanie gry, aby określić, jakie ruchy są dostępne w danym czasie, ale stan gry musi zapytać RuleBook o początkowy układ początkowy i jakie efekty uboczne towarzyszą ruchowi raz jest tworzony (np. kto porusza się dalej).

Próbowałem uporządkować nowy zestaw klas w sposób hierarchiczny, z RuleBook na górze, ponieważ powinien wiedzieć o wszystkim. Powoduje to jednak konieczność przeniesienia wielu metod do klasy RuleBook (takich jak wykonanie ruchu), co czyni ją monolityczną i nie jest szczególnie reprezentatywna dla tego, czym powinien być RuleBook.

Więc jaki jest właściwy sposób na zorganizowanie tego? Czy powinienem zmienić RuleBook w BigClassThatDoesAlmostEverythingInTheGame, aby uniknąć okrągłych odniesień, rezygnując z dokładnego modelowania gry w świecie rzeczywistym? A może powinienem trzymać się współzależnych klas i nakłaniać kompilator do ich kompilacji, zachowując mój model rzeczywisty? A może brakuje mi oczywistej prawidłowej struktury?

Dzięki za wszelką pomoc, której możesz udzielić!

Damian Walker
źródło
7
Co jeśli RuleBookwziął np. StateJako argument i zwrócił prawidłowy MoveList, tj. „Oto gdzie jesteśmy teraz, co można zrobić dalej?”
jonrsharpe
Co powiedział @jonrsharpe. Podczas grania w prawdziwą grę planszową księga reguł również nie wie o rzeczywistych grach. Prawdopodobnie wprowadziłbym nawet inną klasę do obliczania ruchów, ale to może zależeć od tego, jak duża jest już ta klasa RuleBook.
Sebastiaan van den Broek
4
Unikanie obiektu boga (BigClassThatDoesAlmostEverythingInTheGame) jest znacznie ważniejsze niż unikanie cyklicznych odniesień.
user281377
2
@ user281377 niekoniecznie jednak wykluczają się wzajemnie!
jonrsharpe
1
Czy potrafisz pokazać próby modelowania? Na przykład schemat?
Użytkownik

Odpowiedzi:

47

Walczyłem z problemem w projekcie Java dotyczącym referencji cyklicznych.

Moduł śmieciowy Javy nie polega na technikach liczenia referencji. Odnośniki cykliczne nie powodują żadnego problemu w Javie. Czas spędzony na eliminowaniu idealnie naturalnych okrągłych odniesień w Javie to strata czasu.

Zakodowałem to [...], ale problem polega na tym, że jest pełen okólników. Zaimplementowałem go wtedy, upychając wszystkie splecione klasy w jednym pliku źródłowym , [...]

Niekoniecznie. Jeśli po prostu skompilujesz wszystkie pliki źródłowe naraz (np. javac *.java), Kompilator bez problemu rozwiąże wszystkie odniesienia do przodu.

A może powinienem trzymać się klas współzależnych i nakłaniać kompilator do ich kompilacji, [...]

Tak. Oczekuje się, że klasy aplikacji będą współzależne. Kompilowanie wszystkich plików źródłowych Java, które należą do tego samego pakietu naraz, nie jest sprytnym włamaniem, jest to dokładnie sposób, w jaki Java powinna działać.

Atsby
źródło
24
„Odnośniki cykliczne nie powodują żadnych problemów w Javie”. Pod względem kompilacji jest to prawda. Odnośniki okrągłe są jednak uważane za zły projekt .
Chop
22
Referencje kołowe są w wielu sytuacjach całkowicie naturalne, dlatego Java i inne współczesne języki używają wyrafinowanego śmieciarza zamiast prostego licznika referencji.
user281377
3
Umiejętność rozpoznawania referencji cyklicznych w Javie jest świetna i zdecydowanie prawdą jest, że są one naturalne w wielu sytuacjach. Ale OP przedstawił konkretną sytuację, którą należy wziąć pod uwagę. Splątany kod spaghetti nie jest prawdopodobnie najlepszym sposobem na poradzenie sobie z tym problemem.
Mateusz
3
Nie rozpowszechniaj bezpodstawnego FUD na temat niepowiązanych języków programowania. Python wspiera GC cykli referencyjnych od wieków ( dokumenty , również na SO: tutaj i tutaj ).
Christian Aichinger
2
IMHO ta odpowiedź jest tylko przeciętna, ponieważ nie ma ani jednego słowa o okólnikach, które są przydatne na przykład w PO.
Doc Brown
22

To prawda, że ​​zależności cykliczne są wątpliwą praktyką z punktu widzenia projektowania, ale nie są zabronione, a z czysto technicznego punktu widzenia nie są nawet koniecznie problematyczne , jak się wydaje, że są: są całkowicie legalne w większości przypadków są one nieuniknione w niektórych sytuacjach, a w niektórych rzadkich przypadkach można je nawet uznać za przydatne.

W rzeczywistości jest bardzo niewiele scenariuszy, w których kompilator Java zaprzecza cyklicznej zależności. (Uwaga: może być ich więcej, teraz mogę tylko pomyśleć o następujących).

  1. Dziedziczenie: Nie możesz mieć klasy A rozszerzającej klasę B, która z kolei rozszerza klasę A, i jest całkowicie uzasadnione, że nie możesz tego mieć, ponieważ alternatywa nie miałaby absolutnie żadnego sensu z logicznego punktu widzenia.

  2. Wśród klas lokalnych: klasy zadeklarowane w ramach metody nie mogą się wzajemnie odwoływać. Jest to prawdopodobnie nic innego jak ograniczenie kompilatora Java, być może dlatego, że możliwość zrobienia czegoś takiego nie jest wystarczająco użyteczna, aby uzasadnić dodatkową złożoność, która musiałaby przejść do kompilatora, aby go obsługiwać. (Większość programistów Java nie zdaje sobie nawet sprawy z faktu, że można zadeklarować klasę w ramach metody, nie mówiąc już o zadeklarowaniu wielu klas, a następnie o tym, aby klasy te nawzajem się odwoływały).

Dlatego ważne jest, aby uświadomić sobie i usunąć z drogi, że dążenie do zminimalizowania zależności cyklicznych jest dążeniem do czystości projektu, a nie dążeniem do technicznej poprawności.

O ile mi wiadomo, nie istnieje redukcjonistyczne podejście do eliminowania zależności cyklicznych, co oznacza, że ​​nie ma przepisu składającego się wyłącznie z prostych, uprzednio określonych kroków „bezmyślnych”, polegających na przyjęciu systemu z cyklicznymi odniesieniami, zastosowaniu ich jeden po drugim i zakończeniu z systemem wolnym od okrągłych odniesień. Musisz skupić się na pracy i wykonać czynności refaktoryzacyjne, które zależą od charakteru twojego projektu.

W konkretnej sytuacji, którą masz pod ręką, wydaje mi się, że potrzebujesz nowego bytu, być może zwanego „Game” lub „GameLogic”, który zna wszystkie inne byty (bez znajomości innych podmiotów, ), aby pozostałe podmioty nie musiały się znać.

Na przykład wydaje mi się nieuzasadnione, że twoja jednostka RuleBook musi wiedzieć cokolwiek na temat jednostki GameState, ponieważ książka reguł jest czymś, z czym się konsultujemy, aby zagrać, to nie jest coś, co bierze aktywny udział w grze. Tak więc to nowa jednostka „Gry” musi zapoznać się zarówno z książką reguł, jak i stanem gry, aby ustalić, jakie ruchy są dostępne, a to eliminuje okrągłe zależności.

Myślę, że teraz mogę zgadnąć, jaki będzie twój problem z tym podejściem: kodowanie bytu „Game” w sposób niezależny od gry będzie bardzo trudne, więc najprawdopodobniej skończysz nie z jednym, ale z dwoma podmioty, które będą musiały mieć niestandardowe implementacje dla każdego rodzaju gry: „RuleBook” i „Gra”. Które z kolei przeczy celowi posiadania encji „RuleBook”. Cóż, wszystko, co mogę o tym powiedzieć, to to, że być może twoje początkowe dążenie do napisania systemu, który może grać w wiele różnych rodzajów gier, mogło być szlachetne, ale może źle pomyślane. Gdybym był w twoich butach, skoncentrowałbym się na użyciu wspólnego mechanizmu wyświetlania stanu wszystkich różnych gier oraz wspólnego mechanizmu otrzymywania informacji od użytkowników dla wszystkich tych gier,

Mike Nakis
źródło
1
Dzięki Mike. Masz rację co do wad istoty Gry; przy pomocy starego kodu apletu mogłem tworzyć nowe gry z niewiele więcej niż nową podklasą RuleBook i odpowiednim projektem graficznym.
Damian Walker
10

Teoria gier traktuje gry jako listę poprzednich ruchów (typy wartości, w tym kto je grał) oraz funkcję ValidMoves (previousMoves)

Spróbowałbym postępować zgodnie z tym wzorem dla części gry bez interfejsu użytkownika i traktować takie rzeczy, jak przygotowanie planszy jako ruchy.

interfejs użytkownika może wtedy być standardowym narzędziem OO z jednym kierunkiem do logiki


Zaktualizuj, aby skondensować komentarze

Rozważ szachy. Gry w szachy są zwykle rejestrowane jako listy ruchów. http://en.wikipedia.org/wiki/Portable_Game_Notation

lista ruchów znacznie lepiej określa pełny stan gry niż zdjęcie planszy.

Powiedzmy na przykład, że zaczynamy tworzyć obiekty dla Board, Piece, Move itp. I Metody takie jak Piece.GetValidMoves ()

najpierw widzimy, że musimy mieć odniesienie do planszy, ale potem rozważamy castling. co możesz zrobić tylko wtedy, gdy nie przeniosłeś już króla lub wieży. Potrzebujemy flagi MovedAlready na królu i wieżach. Podobnie pionki mogą poruszać się o 2 pola w swoim pierwszym ruchu.

Następnie widzimy, że w roszowaniu ważny ruch króla zależy od istnienia i stanu wieży, więc plansza musi mieć na sobie pionki i odwoływać się do nich. mamy problem z okólnikiem.

Jeśli jednak zdefiniujemy Move jako niezmienną strukturę i stan gry jako listę poprzednich ruchów, to problemy te znikną. Aby sprawdzić, czy castling jest ważny, możemy sprawdzić listę ruchów istnienia ruchów zamku i króla. Aby sprawdzić, czy pionek może przyjąć en-passent, możemy sprawdzić, czy drugi pionek wykonał wcześniej dwukrotnie ruch w ruchu. Nie są potrzebne żadne odniesienia oprócz Reguł -> Przenieś

Teraz szachy mają statyczną planszę, a wycinki zawsze są ustawione w ten sam sposób. Ale powiedzmy, że mamy wariant, w którym pozwalamy na alternatywną konfigurację. być może pomijając niektóre elementy jako utrudnienie.

Jeśli dodamy ruchy przygotowawcze jako ruchy, „z pola do kwadratu X” i dostosujemy obiekt Rules do zrozumienia tego ruchu, nadal będziemy mogli przedstawić grę jako sekwencję ruchów.

Podobnie, jeśli w grze sama plansza jest niestatyczna, powiedzmy, że możemy dodawać kwadraty do szachów lub usuwać kwadraty z planszy, aby nie można było ich przenosić. Zmiany te można również przedstawić jako ruchy bez zmiany ogólnej struktury silnika reguł lub konieczności odwoływania się do obiektu BoardSetup o podobnym charakterze

Ewan
źródło
Będzie to komplikować implementację ValidMoves, co spowolni Twoją logikę.
Taemyr
nie bardzo, zakładam, że konfiguracja płytki jest zmienna, więc musisz ją jakoś zdefiniować. Jeśli przekształcisz ruchy instalacyjne w inną strukturę lub obiekt, aby ułatwić obliczenia, możesz buforować wynik, jeśli to konieczne. Niektóre gry mają plansze, które zmieniają się wraz z grą, a niektóre prawidłowe ruchy mogą zależeć od wcześniejszych ruchów niż bieżącej pozycji (np. Roszowanie w szachach)
Ewan
1
Dodawanie flag i innych rzeczy to złożoność, której unikasz, mając po prostu historię ruchów. powiedzenie 100 ruchów szachowych, aby uzyskać bieżącą konfigurację planszy, nie jest drogie, a wynik między ruchami można zapisać w pamięci podręcznej
Ewan
1
unikniesz również zmiany modelu obiektu, aby odzwierciedlał reguły. tzn. w szachach, jeśli wykonasz prawidłowe ruchy -> Kawałek + plansza, nie zaliczysz roszowania, en-passent, pierwszego ruchu pionków i promocji pionu i musisz dodać dodatkowe informacje do obiektów lub odwołać się do trzeciego obiektu. Tracisz także pojęcie, kto to jest i pojęcia, takie jak odkryty czek
Ewan
1
@Gabe boardLayoutJest to funkcja wszystkich priorMoves(tzn. Jeśli utrzymalibyśmy ją jako stan, nic by się do tego nie przyczyniło oprócz siebie thisMove). Stąd sugestia Ewana jest zasadniczo „obciąć środkowego człowieka” - ważne ruchy zastępują bezpośrednią funkcję wszystkich wcześniejszych validMoves( boardLayout( priorMoves ) ).
OJFord
8

Standardowym sposobem usuwania odwołania cyklicznego między dwiema klasami w programowaniu obiektowym jest wprowadzenie interfejsu, który może być następnie zaimplementowany przez jedną z nich. Więc w twoim przypadku możesz mieć RuleBookodniesienie do, Statektóre następnie odnosi się do InitialPositionProvider(który byłby interfejsem realizowanym przez RuleBook). Ułatwia to również testowanie, ponieważ można następnie utworzyć Statedla celów testowych inną (przypuszczalnie prostszą) pozycję początkową.

Jules
źródło
6

Wierzę, że okrągłe odniesienia i obiekt boga w twoim przypadku można łatwo usunąć, oddzielając kontrolę nad przebiegiem gry od modeli stanu i reguł gry. W ten sposób zapewne zyskasz dużą elastyczność i pozbędziesz się niepotrzebnej złożoności.

Myślę, że powinieneś mieć kontrolera („mistrza gry”, jeśli chcesz), który kontroluje przebieg gry i obsługuje rzeczywiste zmiany stanu, zamiast dawać tę odpowiedzialność książce reguł lub stanowi gry.

Obiekt stanu gry nie musi się zmieniać ani być świadomym zasad. Klasa musi jedynie dostarczyć model łatwych w obsłudze (utworzonych, sprawdzonych, zmienionych, utrwalonych, zalogowanych, skopiowanych, buforowanych itp.) I wydajnych obiektów stanu gry dla pozostałej części aplikacji.

Księga reguł nie powinna zawierać żadnych informacji na temat trwającej gry. Powinien potrzebować jedynie widoku stanu gry, aby móc stwierdzić, które ruchy są legalne, i musi tylko odpowiedzieć na wynikowy stan gry, gdy zostanie zapytany, co się stanie, gdy ruch zostanie zastosowany do stanu gry. Może również zapewnić początkowy stan gry, gdy zostanie poproszony o wstępny układ.

Kontroler musi być świadomy stanów gry i księgi reguł i być może innych obiektów modelu gry, ale nie powinien bałaganić szczegółów.

POCHODZIĆ Z
źródło
4
DOKŁADNIE moje myślenie. OP miesza za dużo danych i procedur w tych samych klasach. Lepiej je bardziej podzielić. To dobra rozmowa na ten temat. Btw, kiedy czytam „widok do stanu gry”, myślę „argument do funkcji”. +100 gdybym mógł.
jpmc26
5

Myślę, że problem polega na tym, że nie podałeś jasnego opisu zadań, które mają być obsługiwane przez poszczególne klasy. Opiszę, co uważam za dobry opis tego, co każda klasa powinna zrobić, a następnie podam przykład ogólnego kodu ilustrującego pomysły. Przekonamy się, że kod jest mniej sprzężony, a więc tak naprawdę nie ma odwołań cyklicznych.

Zacznijmy od opisania tego, co robi każda klasa.

GameStateKlasa powinna zawierać tylko informacje na temat aktualnego stanu gry. Nie powinien zawierać żadnych informacji o tym, jakie są wcześniejsze stany gry ani jakie ruchy będą możliwe w przyszłości. Powinien zawierać tylko informacje o tym, jakie elementy znajdują się na jakich kwadratach w szachach, lub ile i jakie są rodzaje szachów w jakich punktach w backgammon. GameStateBędą musiały zawierać dodatkowe informacje, takie jak informacje o roszady w szachy lub o podwojenie sześcianu w backgammon.

MoveKlasa jest trochę trudne. Powiedziałbym, że mogę określić ruch, który chcesz zagrać, określając GameStatewynik tego ruchu. Więc możesz sobie wyobrazić, że ruch można po prostu zaimplementować jako GameState. Jednak w go (na przykład) można sobie wyobrazić, że o wiele łatwiej jest określić ruch, określając pojedynczy punkt na planszy. Chcemy, aby nasza Moveklasa była wystarczająco elastyczna, aby poradzić sobie z jedną z tych spraw. Dlatego Moveklasa będzie interfejsem z metodą, która pobiera ruch przed GameStatei zwraca nowy ruch po GameState.

Teraz RuleBookklasa odpowiada za znajomość zasad. Można to podzielić na trzy rzeczy. Musi wiedzieć, co to GameStatejest inicjał , musi wiedzieć, jakie ruchy są legalne, i musi wiedzieć, czy któryś z graczy wygrał.

Możesz także stworzyć GameHistoryklasę, która będzie śledzić wszystkie ruchy, które zostały wykonane i wszystkie GameStates, które się wydarzyły. Nowa klasa jest konieczna, ponieważ zdecydowaliśmy, że jeden GameStatenie powinien być odpowiedzialny za znajomość wszystkich GameStatepoprzedniej.

To kończy klasy / interfejsy, które omówię. Ty też masz Boardklasę. Ale myślę, że plansze w różnych grach są na tyle różne, że trudno jest zobaczyć, co można ogólnie zrobić z planszami. Teraz przejdę do tworzenia ogólnych interfejsów i implementowania klas ogólnych.

Po pierwsze jest GameState. Ponieważ ta klasa jest całkowicie zależna od konkretnej gry, nie ma ogólnego Gamestateinterfejsu ani klasy.

Dalej jest Move. Jak powiedziałem, można to przedstawić za pomocą interfejsu, który ma jedną metodę, która przyjmuje stan przed ruchem i wytwarza stan po ruchu. Oto kod tego interfejsu:

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Zauważ, że istnieje parametr typu. Jest tak, ponieważ na przykład ChessMovetrzeba będzie wiedzieć o szczegółach wstępnego ruchu ChessGameState. Na przykład deklaracja klasy ChessMovebędzie

class ChessMove extends Move<ChessGameState>,

gdzie już zdefiniowałbyś ChessGameStateklasę.

Następnie omówię RuleBookklasę ogólną . Oto kod:

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

Ponownie istnieje parametr typu dla GameStateklasy. Ponieważ RuleBookma wiedzieć, jaki jest stan początkowy, umieściliśmy metodę nadania stanu początkowego. Ponieważ RuleBookma wiedzieć, które ruchy są legalne, dysponujemy metodami sprawdzania, czy ruch jest legalny w danym stanie, i podajemy listę legalnych ruchów dla danego stanu. Wreszcie istnieje metoda oceny GameState. Zauważ, że RuleBookpowinien być odpowiedzialny tylko za opisanie, czy jeden lub inni gracze już wygrywają, ale nie kto jest na lepszej pozycji w środku gry. Decyzja o tym, kto ma lepszą pozycję, jest skomplikowaną sprawą, którą należy przenieść do własnej klasy. Dlatego StateEvaluationklasa jest w rzeczywistości zwykłym wyliczeniem podanym w następujący sposób:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Na koniec opiszmy GameHistoryklasę. Ta klasa jest odpowiedzialna za zapamiętywanie wszystkich pozycji, które zostały osiągnięte w grze, a także wykonanych ruchów. Najważniejsze, co powinien być w stanie zrobić, to nagrać Movejako odtworzony. Możesz także dodać funkcję cofania Move. Mam implementację poniżej.

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

Wreszcie możemy sobie wyobrazić tworzenie Gameklasy, która łączy wszystko razem. Ta Gameklasa ma na celu ujawnienie metod, które pozwalają ludziom zobaczyć, co GameStatejest prądem , zobaczyć, kto, jeśli ktoś ma, zobaczyć, jakie ruchy mogą być odtwarzane, i wykonać ruch. Mam implementację poniżej

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

Zauważ w tej klasie, że RuleBooknie jest odpowiedzialny za wiedzieć, co to GameStatejest prąd . To jest GameHistorypraca. Więc Gameprosi GameHistoryJaki jest obecny stan i daje te informacje RuleBook, gdy Gamepotrzeby, aby powiedzieć, co porusza prawne lub jeśli ktoś wygrał.

W każdym razie, celem tej odpowiedzi jest to, że po dokonaniu rozsądnego ustalenia, za co każda klasa jest odpowiedzialna, i skupieniu każdej klasy na niewielkiej liczbie obowiązków, i przypisujesz każdą odpowiedzialność wyjątkowej klasie, a następnie klasom mają tendencję do rozłączania się i wszystko staje się łatwe do kodowania. Mam nadzieję, że wynika to z przykładów kodu, które podałem.

Brian Moths
źródło
3

Z mojego doświadczenia wynika, że ​​okólniki wskazują ogólnie, że twój projekt nie jest dobrze przemyślany.

W twoim projekcie nie rozumiem, dlaczego RuleBook musi „wiedzieć” o państwie. Oczywiście może otrzymać stan jako parametr jakiejś metody, ale dlaczego miałby znać (tzn. Trzymać jako zmienną instancji) odwołanie do stanu? To nie ma dla mnie sensu. RuleBook nie musi „wiedzieć” o stanie żadnej konkretnej gry, aby wykonać swoje zadanie; zasady gry nie zmieniają się w zależności od aktualnego stanu gry. Więc albo zaprojektowałeś go niepoprawnie, albo zaprojektowałeś go poprawnie, ale niepoprawnie wyjaśniam.

Mehrdad
źródło
+1. Kupujesz fizyczną grę planszową, dostajesz zestaw reguł, który potrafi opisać reguły bez stanu.
unperson325680
1

Zależność cykliczna niekoniecznie stanowi problem techniczny, ale należy ją uznać za zapach kodu, który zwykle stanowi naruszenie zasady pojedynczej odpowiedzialności .

Twoja cykliczna zależność wynika z faktu, że próbujesz zrobić zbyt wiele ze swojego Stateobiektu.

Każdy obiekt stanowy powinien udostępniać tylko metody bezpośrednio związane z zarządzaniem tym stanem lokalnym. Jeśli wymaga czegoś więcej niż najbardziej podstawowej logiki, prawdopodobnie powinien zostać rozbity na większy wzór. Niektóre osoby mają różne opinie na ten temat, ale ogólną zasadą jest, że jeśli robisz coś więcej niż operacje pobierania i ustawiania danych, robisz za dużo.

W takim przypadku lepiej byłoby mieć taki StateFactory, który mógłby wiedzieć o Rulebook. Prawdopodobnie miałbyś inną klasę kontrolera, która użyje twojego StateFactorydo stworzenia nowej gry. Statezdecydowanie nie powinienem wiedzieć Rulebook. Rulebookmoże wiedzieć o Statezależności od implementacji twoich reguł.

00500005
źródło
0

Czy istnieje potrzeba powiązania obiektu zbioru reguł z określonym stanem gry, czy może bardziej sensowne byłoby posiadanie obiektu zbioru reguł z metodą, która w danym stanie gry będzie raportować, jakie ruchy są dostępne z tego stanu (i, zgłaszając to, nie pamiętasz nic o danym stanie)? Jeśli obiekt, który zostanie zapytany o dostępne ruchy, nie zapisze się w pamięci stanu gry, nie ma potrzeby, aby zachowywał odniesienie.

W niektórych przypadkach możliwe jest utrzymanie stanu obiektu oceniającego reguły. Jeśli uważasz, że taka sytuacja może się zdarzyć, sugerowałbym dodanie klasy „sędzia” i zalecenie, aby instrukcja zawierała metodę „createReferee”. W przeciwieństwie do instrukcji, która nie obchodzi niczego, czy jest pytana o jedną grę, czy o pięćdziesiąt, obiekt sędziowski spodziewałby się, że poprowadzi jedną grę. Nie należy oczekiwać, aby zawierał cały stan związany z oficjalną grą, ale może buforować wszelkie informacje o grze, które uzna za przydatne. Jeśli gra obsługuje funkcję „cofnij”, pomocne może być włączenie przez sędziego środka do wytworzenia obiektu „migawki”, który mógłby być przechowywany wraz z wcześniejszymi stanami gry; ten obiekt powinien

Jeśli konieczne może być sprzężenie między przetwarzaniem reguł a aspektami przetwarzania stanu gry w kodzie, użycie obiektu arbitra umożliwi utrzymanie takiego sprzężenia poza główną książką reguł i klasami stanu gry. Może to również pozwolić, aby nowe reguły uwzględniały aspekty stanu gry, których klasa stanu gry nie uznałaby za stosowne (np. Gdyby dodano regułę, która mówi: „Obiekt X nie może wykonać Y, jeśli kiedykolwiek byłby w lokalizacji Z ", sędzia może zostać zmieniony, aby śledzić, które obiekty były w lokalizacji Z bez konieczności zmiany klasy stanu gry).

supercat
źródło
-2

Właściwym sposobem radzenia sobie z tym jest użycie interfejsów. Zamiast wiedzieć o sobie dwie klasy, poproś każdą klasę o zaimplementowanie interfejsu i odwołanie do niej w drugiej klasie. Załóżmy, że masz klasy A i B, które muszą się nawzajem odnosić. Mając interfejs implementacji klasy A i interfejs implementacji klasy B, możesz odwoływać się do interfejsu B z klasy A i interfejsu A z klasy B. Klasa A może być w swoim własnym projekcie, podobnie jak klasa B. Interfejsy są w osobnym projekcie do których odnoszą się oba pozostałe projekty.

Piotr
źródło
2
wydaje się, że to tylko powtórzenie punktów poczynionych i wyjaśnionych we wcześniejszej odpowiedzi opublikowanej kilka godzin przed tą
komara