Zależność klas kołowych

12

Czy źle jest mieć 2 klasy, które się potrzebują?

Piszę małą grę, w której mam GameEngineklasę, która ma kilka GameStateprzedmiotów. Aby uzyskać dostęp do kilku metod renderowania, GameStateobiekty te muszą również znać GameEngineklasę - więc jest to zależność cykliczna.

Czy nazwałbyś to złym projektem? Po prostu pytam, ponieważ nie jestem do końca pewien i w tej chwili jestem w stanie refaktoryzować te rzeczy.

shad0w
źródło
2
Byłbym bardzo zaskoczony, gdyby odpowiedź brzmiała „tak”.
doppelgreener
Ponieważ są dwa pytania, które są logicznie rozłączne ... odpowiedź na jedno z nich brzmi „tak”. Dlaczego chcesz zaprojektować stan, który faktycznie coś robi? powinieneś mieć menedżera / kontrolera, który sprawdzi stan i wykona akcję. Trochę mylące jest pozwalanie obiektowi typu stan przejąć odpowiedzialność za coś innego. Jeśli chcesz to zrobić, C ++ jest twoim przyjacielem .
teodron
Moje klasy GameState to takie jak wprowadzenie, gra, menu i tak dalej. GameEngine przechowuje te stany na stosie, więc jestem w stanie zatrzymać stan i otworzyć menu lub odtworzyć scenkę przerywnikową ... Klasy GameState muszą znać GameEngine, ponieważ silnik tworzy okno i ma kontekst renderowania .
shad0w
5
Pamiętaj, że wszystkie te zasady mają na celu szybkie. Szybki w uruchomieniu, Szybki w tworzeniu, Szybki w utrzymaniu. Czasami te aspekty są ze sobą sprzeczne i musisz przeprowadzić analizę kosztów i korzyści, aby zdecydować, jak postępować. Jeśli nawet o tym pomyślisz i odważysz się zadzwonić, nadal radzisz sobie lepiej niż 90% programistów. Nie ma ukrytego potwora, który zabiłby cię, jeśli zrobisz to źle, jest bardziej ukrytym operatorem punktu poboru opłat.
DampeS8N

Odpowiedzi:

0

Z natury nie jest to zły projekt, ale łatwo można wymknąć się spod kontroli. W rzeczywistości używasz tego projektu w swoich codziennych kodach. Na przykład wektor wie, że jest to pierwszy iterator, a iteratory mają wskaźnik do kontenera.

W twoim przypadku znacznie lepiej jest mieć dwie osobne klasy dla GameEnigne i GameState. Ponieważ w zasadzie ci dwaj robią różne rzeczy, a później możesz zdefiniować wiele klas, które dziedziczą GameState (jak GameState dla każdej sceny w grze). I nikt nie może zaprzeczyć ich potrzebie dostępu do siebie nawzajem. Zasadniczo GameEngine obsługuje gamestaty, więc powinien mieć do nich wskaźnik. GameState korzysta z zasobów zdefiniowanych w GameEngine (takich jak renderowany, menedżer fizyki itp.).

Nie możesz i nie powinieneś łączyć ze sobą tych dwóch klas, ponieważ z natury robią różne rzeczy, a łączenie spowoduje bardzo dużą klasę, której nikt nie lubi.

Jak dotąd wiemy, że potrzebujemy okrągłej zależności w naszym projekcie. Istnieje wiele sposobów bezpiecznego tworzenia:

  1. Jak zasugerowałeś, możemy umieścić wskaźnik każdego z nich w innym, jest to najprostsze rozwiązanie, ale może łatwo wymknąć się spod kontroli.
  2. Możesz także użyć projektu singleton / multiton, dzięki tej metodzie powinieneś zdefiniować swoją klasę GameEngine jako singleton, a każdy z tych GameStates jako multiton. Chociaż wielu programistów uważa zarówno singleton, jak i multiton za AntiPattern, wolę ten projekt.
  3. Możesz używać zmiennych globalnych, hej, są w zasadzie takie same jak Singleton / multiton, ale mają niewielką różnicę w tym, że nie ograniczają programisty do tworzenia instancji do woli.

Kończąc jego odpowiedź, możesz użyć jednej z tych trzech metod, ale musisz uważać, aby nie użyć zbytnio żadnej z nich, ponieważ, jak powiedziałem, wszystkie te projekty są naprawdę niebezpieczne i mogą łatwo doprowadzić do niemożliwego do utrzymania kodu.

Ali1S232
źródło
6

Czy źle jest mieć 2 klasy, które się potrzebują?

To trochę Zapach Kodowy , ale można z tym wyjść. Jeśli jest to łatwiejszy i szybszy sposób na uruchomienie gry, wybierz ją. Ale pamiętaj o tym, ponieważ istnieje duża szansa, że ​​w pewnym momencie będziesz musiał to zmienić.

C ++ polega na tym, że zależności cykliczne nie będą się tak łatwo kompilować , więc lepszym pomysłem może się ich pozbyć zamiast spędzać czas na naprawianiu kompilacji.

Zobacz to pytanie na SO, aby uzyskać więcej opinii.

Czy nazwałbyś [mój projekt] złym projektem?

Nie, to wciąż lepsze niż umieszczanie wszystkiego w jednej klasie.

Nie jest tak świetny, ale w rzeczywistości jest bardzo zbliżony do większości wdrożeń, jakie widziałem. Zwykle masz klasę menedżera stanów gry ( strzeż się! ) I klasę renderera i dość często są to singletony. Tak więc zależność cykliczna jest „ukryta”, ale potencjalnie istnieje.

Ponadto, jak powiedziano w komentarzach, to trochę dziwne, że klasy stanu gry wykonują jakiś rendering. Powinny one po prostu przechowywać informacje o stanie, a rendering powinien być obsługiwany przez renderer lub jakiś element graficzny samych obiektów gry.

Teraz może być ostateczny projekt. Jestem ciekawy, czy inne odpowiedzi przynoszą jeden fajny pomysł. Mimo to prawdopodobnie jesteś w stanie znaleźć najlepszy projekt dla Twojej gry.

Laurent Couvidou
źródło
Dlaczego miałby to być zapach kodu? Większość obiektów z relacją rodzic-dziecko musi się widzieć.
Kikaimaru
4
Ponieważ jest to najprostszy sposób, aby nie zdefiniować, która klasa jest odpowiedzialna za co. Łatwo kończy się na dwóch klasach, które są tak głęboko powiązane, że dodawanie nowego kodu może być wykonane w każdej z nich, więc nie są już rozdzielone koncepcyjnie. Jest to również otwarte drzwi do kodu spaghetti: klasa A wywołuje w tym celu klasę B, ale B potrzebuje tych informacji od A, itp. Więc nie, nie powiedziałbym, że obiekt potomny powinien wiedzieć o swoim rodzicu. Jeśli to możliwe, lepiej jeśli nie.
Laurent Couvidou
To śliski błąd na zboczu. Klasy statyczne również mogą prowadzić do złych rzeczy, ale klasy util nie pachną kodem. I naprawdę wątpię, aby istniał jakiś „dobry” sposób, aby Scena zawierała SceneNodes, a SceneNode ma odniesienie do Scene, więc nie można dodawać go do dwóch różnych scen ... I gdzie byś przestał? Czy A wymaga B, B wymaga C, a C wymaga A zapach kodu?
Kikaimaru
Rzeczywiście, jest to tak jak klasy statyczne. Obchodź się ostrożnie, używaj go tylko wtedy, gdy musisz. Teraz mówię z doświadczenia i nie jestem jedynym, który myśli w ten sposób , ale tak naprawdę to kwestia opinii. Moim zdaniem w kontekście podanym przez PO jest to rzeczywiście śmierdzące.
Laurent Couvidou
W tym artykule na Wikipedii nie ma wzmianki o tej sprawie. I nie możesz powiedzieć, że jest to przypadek „niewłaściwej intymności”, dopóki nie zobaczysz tych klas.
Kikaimaru
6

Często uważa się, że zły projekt musi mieć 2 klasy, które bezpośrednio się do siebie odnoszą, tak. W praktyce śledzenie przepływu sterowania przez kod może być trudniejsze, własność obiektów i ich czas życia mogą być skomplikowane, co oznacza, że ​​żadna klasa nie nadaje się do ponownego użycia bez drugiej, może to oznaczać, że przepływ sterowania powinien naprawdę żyć poza oboma z tych klas w trzeciej klasie „mediatora” i tak dalej.

Jednak bardzo często dwa obiekty odnoszą się do siebie, a różnica polega na tym, że zwykle relacja w jednym kierunku jest bardziej abstrakcyjna. Na przykład w systemie Model-View-Controller obiekt View może zawierać odwołanie do obiektu Model i będzie wiedział o nim wszystko, będąc w stanie uzyskać dostęp do wszystkich swoich metod i właściwości, aby widok mógł wypełnić się odpowiednimi danymi . Obiekt Model może przechowywać odniesienie z powrotem do Widoku, aby mógł dokonać aktualizacji Widoku po zmianie własnych danych. Ale zamiast modelu z odniesieniem do widoku - co uzależniałoby model od widoku - zwykle widok implementuje interfejs obserwowalny, często z tylko 1Update()funkcja, a model zawiera odniesienie do obserwowalnego obiektu, którym może być widok. Kiedy Model się zmienia, wywołuje Update()wszystkie swoje Obserwowalne, a widok implementuje Update(), oddzwaniając do Modelu i pobierając wszelkie zaktualizowane informacje. Zaletą jest to, że Model w ogóle nie wie nic o Widoku (i dlaczego miałby to robić?) I może być ponownie użyty w innych sytuacjach, nawet tych bez Widoku.

W swojej grze masz podobną sytuację. GameEngine zwykle będzie wiedział o GameStates. Ale GameState nie musi wiedzieć wszystkiego o GameEngine - potrzebuje jedynie dostępu do niektórych metod renderowania w GameEngine. To powinno wywołać mały alarm w twojej głowie, który mówi, że albo (a) GameEngine próbuje zrobić zbyt wiele rzeczy w obrębie jednej klasy i / lub (b) GameState nie potrzebuje całego silnika gry, tylko część do renderowania.

Trzy podejścia do rozwiązania tego problemu obejmują:

  • Spraw, aby GameEngine pochodziło z interfejsu do renderowania i przekaż to odniesienie do GameState. Jeśli kiedykolwiek zreformujesz część renderującą, musisz tylko upewnić się, że zachowuje ten sam interfejs.
  • Rozłóż część renderującą na nowy obiekt Renderer i przekaż ją do GameStates.
  • Zostaw to tak, jak jest. Może w końcu będziesz chciał uzyskać dostęp do wszystkich funkcji GameEngine z GameState. Należy jednak pamiętać, że oprogramowanie, które jest łatwe w utrzymaniu i łatwe do rozszerzenia, zwykle wymaga, aby każda klasa odnosiła się tak mało na zewnątrz, jak to możliwe, dlatego dzielenie rzeczy na podobiekty i interfejsy, które wykonują jeden i dobrze lepiej zdefiniowane zadanie .
Kylotan
źródło
0

Powszechnie uważa się, że dobrą praktyką jest wysoka kohezja przy niskim sprzężeniu. Oto kilka linków dotyczących sprzężenia i spójności.

sprzęganie wikipedia

spójność wikipedia

niskie sprzęgło, wysoka kohezja

przepływ stosów zgodnie z najlepszymi praktykami

Posiadanie dwóch klas do siebie nawzajem oznacza wysokie sprzężenie. W google ramowe Guice ma na celu uzyskanie wysokiej spójności z niską sprzęgania przez środki wtryskiwania zależności. Sugerowałbym, abyś przeczytał nieco więcej na ten temat, a następnie wykonał własną rozmowę, biorąc pod uwagę kontekst.

avanderw
źródło