Niskie sprzęgło i ścisła kohezja


11

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 worldklasę, która ma zmienną składową vector<monster> monsters. Kiedy monsterklasa komunikuje się z world, czy wolę używać funkcji zwrotnej, czy powinienem mieć wskaźnik do worldklasy wewnątrz monsterklasy?


Oprócz pisowni w tytule pytania, tak naprawdę nie jest sformułowane w formie pytania. Myślę, że przeredagowanie go może pomóc w utrwaleniu tego, o co tutaj pytasz, ponieważ nie sądzę, że możesz uzyskać użyteczną odpowiedź na to pytanie w jego obecnej formie. I nie jest to również pytanie dotyczące projektowania gry, to pytanie dotyczące struktury programowania (nie jestem pewien, czy to oznacza, że ​​znacznik projektu jest, czy nie jest odpowiedni, nie pamiętam, skąd wzięliśmy się na temat „projektowania oprogramowania” i znaczników)
MrCranky

Odpowiedzi:


10

Istnieją trzy główne sposoby, w jakie jedna klasa może rozmawiać z drugą bez ścisłego powiązania z nią:

  1. Poprzez funkcję oddzwaniania.
  2. Poprzez system zdarzeń.
  3. Poprzez interfejs.

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:

  1. C ++ nie ma dobrego wsparcia dla wywołań zwrotnych, które zachowują swój thiswskaźnik, więc trudno jest używać wywołań zwrotnych w kodzie obiektowym.

  2. 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 monstertak naprawdę trzeba się komunikować world. Zgadując, zrobiłbym coś takiego:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

Chodzi o to, abyś wprowadził IWorld(co jest kiepską nazwą) absolutne minimum, które Monstermusi uzyskać dostęp. Jego widok na świat powinien być możliwie wąski.


1
+1 Delegaci (oddzwanianie) zwykle stają się liczni w miarę upływu czasu. Dawanie potworom interfejsu, aby mogli dostać się do rzeczy, jest moim zdaniem dobrym sposobem.
Michael Coleman,

12

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.


+1 Danie potworowi ograniczonego sposobu na wezwanie rodzica jest moim zdaniem miłym środkowym sposobem.
Michael Coleman

7

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.


3

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ć.


2

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:

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

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.


1

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:

  • Utrzymuje moje obiekty luźniej sprzężone. Moje obiekty mogą zawierać inne obiekty, ale nigdy nie będą zależeć od kontekstu obiektu wywołującego, a kontekst nigdy nie będzie zależał od obiektów otoczonych.
  • To sprawia, że ​​moja kontrola jest łatwa do uzasadnienia. Miło jest móc założyć, że jedynym kodem, który może zmienić stan wewnętrzny selfpodczas 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.
  • Chroni niezmienniki w enkapsulowanych danych moich obiektów. Metody publiczne mogą zależeć od niezmienników, a niezmienniki te mogą zostać naruszone, jeśli jedną metodę można wywołać zewnętrznie, podczas gdy inna już działa.

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ą.


0

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ć.


2
Prawdopodobnie powiedziałbym to bardziej zwięźle: „Nie bój się refaktoryzować”.
Tetrad

Singletony są złe! Globale są znacznie lepsze. Wszystkie wymienione cnoty singleton są takie same dla globalnego. Używam wskaźników globalnych (w rzeczywistości funkcji globalnych zwracających referencje) do mojego różnych podsystemów i inicjuję / niszczę je w mojej głównej funkcji. Wskaźniki globalne unikają problemów z kolejnością inicjalizacji, zawieszania singletonów podczas burzenia, nietrywialnej konstrukcji singletonów itp. Powtarzam, że singletony są złe.
deft_code

@Tetrad, bardzo się zgadzam. To jedna z najlepszych umiejętności, jakie możesz mieć.
ZorbaTHut
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.