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ć!
źródło
RuleBook
wziął np.State
Jako argument i zwrócił prawidłowyMoveList
, tj. „Oto gdzie jesteśmy teraz, co można zrobić dalej?”Odpowiedzi:
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.
Niekoniecznie. Jeśli po prostu skompilujesz wszystkie pliki źródłowe naraz (np.
javac *.java
), Kompilator bez problemu rozwiąże wszystkie odniesienia do przodu.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ć.
źródło
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).
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.
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,
źródło
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
źródło
boardLayout
Jest to funkcja wszystkichpriorMoves
(tzn. Jeśli utrzymalibyśmy ją jako stan, nic by się do tego nie przyczyniło oprócz siebiethisMove
). Stąd sugestia Ewana jest zasadniczo „obciąć środkowego człowieka” - ważne ruchy zastępują bezpośrednią funkcję wszystkich wcześniejszychvalidMoves( boardLayout( priorMoves ) )
.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ć
RuleBook
odniesienie do,State
które następnie odnosi się doInitialPositionProvider
(który byłby interfejsem realizowanym przezRuleBook
). Ułatwia to również testowanie, ponieważ można następnie utworzyćState
dla celów testowych inną (przypuszczalnie prostszą) pozycję początkową.źródło
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.
źródło
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.
GameState
Klasa 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.GameState
Będą musiały zawierać dodatkowe informacje, takie jak informacje o roszady w szachy lub o podwojenie sześcianu w backgammon.Move
Klasa jest trochę trudne. Powiedziałbym, że mogę określić ruch, który chcesz zagrać, określającGameState
wynik tego ruchu. Więc możesz sobie wyobrazić, że ruch można po prostu zaimplementować jakoGameState
. 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 naszaMove
klasa była wystarczająco elastyczna, aby poradzić sobie z jedną z tych spraw. DlategoMove
klasa będzie interfejsem z metodą, która pobiera ruch przedGameState
i zwraca nowy ruch poGameState
.Teraz
RuleBook
klasa odpowiada za znajomość zasad. Można to podzielić na trzy rzeczy. Musi wiedzieć, co toGameState
jest inicjał , musi wiedzieć, jakie ruchy są legalne, i musi wiedzieć, czy któryś z graczy wygrał.Możesz także stworzyć
GameHistory
klasę, która będzie śledzić wszystkie ruchy, które zostały wykonane i wszystkieGameStates
, które się wydarzyły. Nowa klasa jest konieczna, ponieważ zdecydowaliśmy, że jedenGameState
nie powinien być odpowiedzialny za znajomość wszystkichGameState
poprzedniej.To kończy klasy / interfejsy, które omówię. Ty też masz
Board
klasę. 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ólnegoGamestate
interfejsu 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:Zauważ, że istnieje parametr typu. Jest tak, ponieważ na przykład
ChessMove
trzeba będzie wiedzieć o szczegółach wstępnego ruchuChessGameState
. Na przykład deklaracja klasyChessMove
będzieclass ChessMove extends Move<ChessGameState>
,gdzie już zdefiniowałbyś
ChessGameState
klasę.Następnie omówię
RuleBook
klasę ogólną . Oto kod:Ponownie istnieje parametr typu dla
GameState
klasy. PonieważRuleBook
ma wiedzieć, jaki jest stan początkowy, umieściliśmy metodę nadania stanu początkowego. PonieważRuleBook
ma 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 ocenyGameState
. Zauważ, żeRuleBook
powinien 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. DlategoStateEvaluation
klasa jest w rzeczywistości zwykłym wyliczeniem podanym w następujący sposób:Na koniec opiszmy
GameHistory
klasę. 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ćMove
jako odtworzony. Możesz także dodać funkcję cofaniaMove
. Mam implementację poniżej.Wreszcie możemy sobie wyobrazić tworzenie
Game
klasy, która łączy wszystko razem. TaGame
klasa ma na celu ujawnienie metod, które pozwalają ludziom zobaczyć, coGameState
jest prądem , zobaczyć, kto, jeśli ktoś ma, zobaczyć, jakie ruchy mogą być odtwarzane, i wykonać ruch. Mam implementację poniżejZauważ w tej klasie, że
RuleBook
nie jest odpowiedzialny za wiedzieć, co toGameState
jest prąd . To jestGameHistory
praca. WięcGame
prosiGameHistory
Jaki jest obecny stan i daje te informacjeRuleBook
, gdyGame
potrzeby, 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.
źródło
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.
źródło
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
State
obiektu.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ć oRulebook
. Prawdopodobnie miałbyś inną klasę kontrolera, która użyje twojegoStateFactory
do stworzenia nowej gry.State
zdecydowanie nie powinienem wiedziećRulebook
.Rulebook
może wiedzieć oState
zależności od implementacji twoich reguł.źródło
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).
źródło
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.
źródło