Oczywiście zależy to od sytuacji. Ale kiedy obiekt lub system o niższej dźwigni komunikuje się z systemem wyższego poziomu, czy należy preferować wywołania zwrotne lub zdarzenia zamiast utrzymywania wskaźnika na obiekcie wyższego poziomu?
Na przykład mamy world
klasę, która ma zmienną składową vector<monster> monsters
. Kiedy monster
klasa komunikuje się z world
, czy wolę używać funkcji zwrotnej, czy powinienem mieć wskaźnik do world
klasy wewnątrz monster
klasy?
architecture
software-engineering
hidayat
źródło
źródło
Odpowiedzi:
Istnieją trzy główne sposoby, w jakie jedna klasa może rozmawiać z drugą bez ścisłego powiązania z nią:
Te trzy są ściśle ze sobą powiązane. System zdarzeń na wiele sposobów jest tylko listą wywołań zwrotnych. Oddzwonienie jest mniej więcej interfejsem z jedną metodą.
W C ++ rzadko używam wywołań zwrotnych:
C ++ nie ma dobrego wsparcia dla wywołań zwrotnych, które zachowują swój
this
wskaźnik, więc trudno jest używać wywołań zwrotnych w kodzie obiektowym.Oddzwonienie jest w zasadzie nierozszerzalnym interfejsem jednej metody. Z czasem okazało się, że prawie zawsze potrzebuję więcej niż jednej metody definiowania tego interfejsu, a pojedyncze wywołanie zwrotne rzadko wystarcza.
W takim przypadku prawdopodobnie zrobiłbym interfejs. W swoim pytaniu nie określasz, z czym
monster
tak naprawdę trzeba się komunikowaćworld
. Zgadując, zrobiłbym coś takiego:Chodzi o to, abyś wprowadził
IWorld
(co jest kiepską nazwą) absolutne minimum, któreMonster
musi uzyskać dostęp. Jego widok na świat powinien być możliwie wąski.źródło
Nie używaj funkcji oddzwaniania, aby ukryć jaką funkcję wywołujesz. Jeśli miałbyś wprowadzić funkcję wywołania zwrotnego, a ta funkcja wywołania będzie miała przypisaną jedną i tylko jedną funkcję, to tak naprawdę wcale nie zrywasz sprzężenia. Po prostu maskujesz to kolejną warstwą abstrakcji. Nic nie zyskujesz (poza czasem kompilacji), ale tracisz jasność.
Nie nazwałbym tego dokładnie najlepszą praktyką, ale jest to powszechny wzorzec posiadania bytów zawartych w czymś, co ma wskaźnik do ich rodzica.
Biorąc to pod uwagę, warto poświęcić chwilę na użycie wzorca interfejsu, aby dać potworom ograniczony zestaw funkcji, które mogą wywołać na całym świecie.
źródło
Zasadniczo staram się unikać linków dwukierunkowych, ale jeśli muszę je mieć, absolutnie upewniam się, że istnieje jedna metoda ich wykonania i jedna, aby je złamać, aby nigdy nie występowały niespójności.
Często można całkowicie uniknąć dwukierunkowego łącza, przekazując dane, jeśli jest to konieczne. Trywialne refaktoryzacja polega na tym, aby zamiast utrzymywania przez potwora powiązania ze światem, przekazujesz świat przez odniesienie do metod potworów, które go potrzebują. Jeszcze lepiej jest przekazać interfejs tylko tym fragmentom świata, którego potwór potrzebuje, co oznacza, że potwór nie polega na konkretnej implementacji świata. Odpowiada to zasadzie segregacji interfejsu i zasadzie inwersji zależności , ale nie zaczyna wprowadzać nadmiernej abstrakcji, którą czasami można uzyskać za pomocą zdarzeń, sygnałów + gniazd itp.
W pewnym sensie można argumentować, że korzystanie z oddzwaniania jest bardzo wyspecjalizowanym minikomputerem i jest w porządku. Musisz zdecydować, czy możesz w bardziej znaczący sposób osiągnąć swoje cele za pomocą jednej kolekcji metod w obiekcie interfejsu, czy kilku innych w różnych wywołaniach zwrotnych.
źródło
Staram się unikać wywoływania przez obiekty obiektów ich kontenerów, ponieważ uważam, że prowadzi to do zamieszania, staje się zbyt łatwe do uzasadnienia, staje się nadużywane i tworzy zależności, których nie można zarządzać.
Moim zdaniem idealnym rozwiązaniem jest, aby klasy wyższego poziomu były wystarczająco inteligentne, aby zarządzać klasami niższego poziomu. Na przykład, świat wiedzący, aby ustalić, czy doszło do zderzenia potwora z rycerzem bez wiedzy o drugim, jest dla mnie lepszy niż potwór pytający świat, czy zderzył się z rycerzem.
Inna opcja w twoim przypadku, prawdopodobnie wymyślę, dlaczego klasa potworów musi wiedzieć o klasie światowej, a najprawdopodobniej odkryjesz, że jest coś w tej klasie światowej, co można podzielić na własną klasę, którą tworzy poczucie, że klasa potworów powinna wiedzieć.
źródło
Oczywiście nie zajdziesz daleko bez wydarzeń, ale zanim nawet zaczniesz pisać (i projektować) system wydarzeń, powinieneś zadać prawdziwe pytanie: dlaczego potwór miałby się komunikować ze światowej klasy? Czy to naprawdę powinno?
Weźmy „klasyczną” sytuację, potwór atakujący gracza.
Potwór atakuje: świat może bardzo dobrze zidentyfikować sytuację, w której bohater znajduje się obok potwora i powiedzieć potworowi, aby zaatakował. Tak więc funkcja w potworze byłaby:
Ale świat (który już zna potwora) nie musi być znany potworowi. W rzeczywistości potwór może zignorować samo istnienie świata klasowego, co prawdopodobnie jest lepsze.
To samo, gdy potwór się porusza (pozwalam podsystemom na zdobycie stworzenia i zajmuję się obliczeniami / zamiarami ruchu, potwór jest po prostu zbiorem danych, ale wiele osób powiedziałoby, że to nie jest OOP).
Chodzi mi o to: wydarzenia (lub oddzwanianie) są oczywiście świetne, ale nie są jedyną odpowiedzią na każdy problem, z którym się spotkasz.
źródło
Kiedy tylko mogę, staram się ograniczyć komunikację między obiektami do modelu żądania i odpowiedzi. Istnieje domniemane częściowe uporządkowanie obiektów w moim programie, tak że pomiędzy dowolnymi dwoma obiektami A i B może istnieć sposób, aby A bezpośrednio lub pośrednio wywołał metodę B lub B mógł bezpośrednio lub pośrednio wywołać metodę A , ale A i B nigdy nie mogą wzajemnie nazywać się metodami. Czasami oczywiście chcesz mieć wsteczną komunikację z dzwoniącym metody. Lubię to robić na kilka sposobów i żaden z nich nie jest wywołaniem zwrotnym.
Jednym ze sposobów jest dołączenie większej ilości informacji do wartości zwracanej wywołania metody, co oznacza, że kod klienta może zdecydować, co z nim zrobić, gdy procedura zwróci mu kontrolę.
Innym sposobem jest wywołanie wspólnego obiektu potomnego. To znaczy, jeśli A wywołuje metodę na B, a B musi przekazać pewne informacje do A, B wywołuje metodę na C, gdzie A i B mogą wywoływać C, ale C nie może wywoływać A lub B. Obiekt A byłby wtedy odpowiedzialny za uzyskanie informacji z C po tym, jak B zwraca kontrolę do A. Zauważ, że tak naprawdę nie różni się to tak naprawdę od pierwszego sposobu, jaki zaproponowałem. Obiekt A nadal może pobierać informacje tylko z wartości zwracanej; żadna z metod obiektu A nie jest wywoływana przez B lub C. Odmianą tej sztuczki jest przekazanie C jako parametru metody, ale nadal obowiązują ograniczenia dotyczące relacji C do A i B.
Ważne pytanie brzmi: dlaczego nalegam na robienie tego w ten sposób. Istnieją trzy główne powody:
self
podczas wykonywania metody, jest ta jedna metoda i żadna inna. Jest to ten sam rodzaj rozumowania, który może prowadzić do umieszczenia muteksów na współbieżnych obiektach.Nie jestem przeciwko wszelkim zastosowaniom wywołań zwrotnych. Zgodnie z moją polityką, aby nigdy nie „wywoływać obiektu wywołującego”, jeśli obiekt A wywołuje metodę na B i przekazuje do niej wywołanie zwrotne, wywołanie zwrotne może nie zmienić stanu wewnętrznego A, i obejmuje obiekty otoczone przez A i obiekt obiekty w kontekście A. Innymi słowy, wywołanie zwrotne może wywoływać metody tylko na obiektach przekazanych mu przez B. W efekcie wywołanie zwrotne podlega takim samym ograniczeniom jak B.
Ostatnim luźnym zakończeniem jest to, że pozwolę na wywołanie dowolnej czystej funkcji, niezależnie od tego częściowego uporządkowania, o którym mówiłem. Czyste funkcje różnią się nieco od metod, ponieważ nie mogą się zmienić lub zależą od stanu zmiennego lub efektów ubocznych, więc nie martw się o to, że dezorientują.
źródło
Osobiście? Po prostu używam singletona.
Tak, dobra, zły projekt, nie zorientowany obiektowo itp. Wiesz co? Nie obchodzi mnie to . Piszę grę, a nie prezentację technologii. Nikt nie oceni mnie na podstawie kodu. Celem jest stworzenie gry, która będzie fajna, a wszystko, co stanie mi na drodze, spowoduje, że gra będzie mniej przyjemna.
Czy kiedykolwiek będą istnieć dwa światy naraz? Może! Może będziesz. Ale jeśli nie możesz teraz pomyśleć o tej sytuacji, prawdopodobnie nie zrobisz tego.
Tak więc moje rozwiązanie: stwórz singielkę World. Funkcje połączeń na nim. Skończ z całym bałaganem. Państwo mogli przechodzić na dodatkowym parametrem do każdej pojedynczej funkcji - a nie pomyłka, to gdzie to prowadzi. Lub możesz po prostu napisać kod, który działa.
Wykonanie tego w ten sposób wymaga odrobiny dyscypliny, aby wyczyścić rzeczy, gdy robi się bałagan (to jest „kiedy”, a nie „jeśli”), ale nie ma sposobu, aby zapobiec zabrudzeniu kodu - albo masz problem ze spaghetti, albo tysiące problem graczy-abstrakcji. Przynajmniej w ten sposób nie piszesz dużych ilości niepotrzebnego kodu.
A jeśli zdecydujesz, że nie chcesz już singletona, zazwyczaj łatwo go się pozbyć. Zajmuje trochę pracy, wymaga przekazania miliarda parametrów, ale są to parametry, które i tak trzeba przekazać.
źródło