Czy potrafisz mieć „puste” streszczenie / klasy?


10

Oczywiście, że tak, po prostu zastanawiam się, czy racjonalne jest projektowanie w taki sposób.

Robię klona Breakouta i pracuję nad klasą. Chciałem zastosować dziedziczenie, mimo że nie muszę, zastosować to, czego się nauczyłem w C ++. Myślałem o projektowaniu klas i wymyśliłem coś takiego:

GameObject -> klasa podstawowa (składa się z elementów danych, takich jak przesunięcia xiy oraz wektor

SDL_Surface * MovableObject: GameObject -> klasa abstrakcyjna + klasa pochodna GameObject (jedna metoda void move () = 0;)

NonMovableObject: GameObject -> pusta klasa ... żadnych metod ani elementów danych innych niż konstruktor i destruktor (przynajmniej na razie?).

Później planowałem wyprowadzić klasę z NonMovableObject, na przykład Tileset: NonMovableObject. Zastanawiałem się tylko, czy często używane są „puste” klasy abstrakcyjne, czy tylko puste klasy ... Zauważyłem, że w ten sposób po prostu tworzę klasę NonMovableObject tylko ze względu na kategoryzację.

Wiem, że zastanawiam się nad zrobieniem klonu, ale mniej skupiam się na grze, a bardziej na używaniu dziedziczenia i projektowaniu jakiegoś frameworka.

Odpowiedzi:


2

W C ++ masz Multiple Inheritance, więc dosłownie nie ma korzyści z posiadania tych pustych klas. Jeśli chcesz, aby piłka dziedziczyła później po GameObject i MovableObject (powiedzmy na przykład, że chcesz zatrzymać tablicę GameObjects i wywoływać metodę Tick co n sekundę, aby wszystkie się poruszyły), to wystarczy.

Ale w tej sytuacji osobiście zasugerowałbym, że pamiętasz aksjomat „ wolę kompozycję (enkapsulację) niż dziedziczenie ” zamiast tego i spojrzeć na wzorzec stanu . Jeśli chcesz, aby każdy obiekt miał później swój stan ruchu, przekaż go do GameObject. Na przykład: DiagonalBouncingMovementState dla piłki, HoriazontalControlledMovementState dla wiosła, NoMovementState dla cegły.

1) Ułatwi ci to pisanie testów jednostkowych Ułatwi , jeśli zdecydujesz się (co ponownie polecam) - ponieważ możesz przetestować GameObject i każdy ze swoich stanów niezależnie.

2) Powiedzmy, że z cegieł spadają żetony, ale potem chcesz zmienić jeden z nich, aby poruszały się po przekątnej - zamiast przesuwać go wokół złożonej hierarchii dziedziczenia klas, możesz po prostu zmienić zakodowany stan ruchu, który jest do niego przekazywany .

3) Co najważniejsze: Kiedy chcesz rozpocząć zmianę w grze jak piłka i / lub ruch wiosła na podstawie tych żetonów, po prostu ciągle się zmienia swój stan ruchu (DiagonalMovementState staje DiagonalWithGravityMovementState).

Otóż ​​nic z tego nie sugerowało, że dziedziczenie jest zawsze złe, tylko po to, aby pokazać, dlaczego wolimy enkapsulację niż dziedziczenie. Możesz nadal chcieć wyprowadzić każdy typ obiektu z GameObject i sprawić, by klasy te zrozumiały swój początkowy stan ruchu. Prawie na pewno będziesz chciał wyprowadzić każdy ze swoich stanów ruchu z abstrakcyjnej klasy MovementState, ponieważ C ++ nie ma interfejsów.

0,02 USD


8

W Javie „puste” interfejsy są używane jako znaczniki (np. Serializable), ponieważ w czasie wykonywania obiekty można sprawdzać niezależnie od tego, czy „implementują” ten interfejs; ale w C ++ wydaje mi się to zupełnie bezcelowe.

IMO, funkcje językowe należy uznać za narzędzia, które pomagają osiągnąć określone cele, a nie obowiązki. Tylko dlatego, że można korzystać z funkcji nie oznacza, że muszą . Bez znaczenia dziedziczenie nie czyni programu lepszym.


3

Nie przesadzasz z tym; myślisz i to dobrze.

Jak już wspomniano, nie używaj funkcji języka tylko dlatego, że on tam jest. Kompromisy związane z nauką języka są kluczem do dobrego projektowania.

Nie należy używać klas podstawowych do kategoryzacji. Może to oznaczać typ sprawdzania. To zły pomysł.

Zastanów się nad pisaniem czystych abstrakcji, które definiują kontrakt twoich obiektów. Kontrakt twoich obiektów to interfejs skierowany na zewnątrz. Jego interfejs publiczny. Co to robi.

Uwaga: ważne jest, aby zrozumieć, że nie chodzi o to, jakie dane ma x, yale o to, co robi move().

W swoim projekcie masz klasę GameObject. Polegasz na nim w odniesieniu do jego danych, a nie jego funkcjonalności. To czuje się skuteczny i poprawne użycie dziedziczenia do dzielenia co wydaje się wspólne dla danych, w Twoim przypadku xi y.

To nie jest prawidłowe zastosowanie dziedziczenia. Zawsze pamiętaj, że dziedziczenie jest najściślejszą formą sprzężenia, jaką możesz mieć, i zawsze powinieneś dążyć do luźnego połączenia.

Z samej swojej natury NonMovableObjectsię nie rusza. Jest xi z ypewnością powinien zostać zadeklarowany const. Z samej swojej natury MoveableObjectmusi się ruszać. Nie może zadeklarować swojego xi yjako const. Dlatego nie są to te same dane i nie można ich udostępniać.

Rozważ funkcjonalność swoich obiektów lub umowę. Co powinni zrobić ? Zrób to dobrze, a dane nadejdą. Pracuj na jednym obiekcie na raz. Martw się po kolei. Przekonasz się, że twoje dobre projekty są wielokrotnego użytku.

Być może wcale nie ma NonMovableObject. Co z czystą abstrakcją, GameObjectBasektóra definiuje move()metodę? Twój system GameObjectwdraża te, które implementują, move()a system przenosi tylko te, które muszą się przenieść.

To dopiero początek; smak. Królicza nora idzie znacznie głębiej. Nie ma łyżki.


Chociaż prawdą jest, że ani MovableObjectnie NonMovableObjectmogą się po sobie dziedziczyć, obie mają swoją lokalizację; jako takie, oba powinny być zużywalne przez kod, który np. chce skierować cel potwora w stronę obiektu bez względu na to, czy obiekt ten może się poruszać, czy nie.
supercat

2

Ma aplikacje w C # jak wspomniane ammoQ, ale w C ++ jedyna aplikacja byłaby, gdybyś chciał mieć listę wskaźników NonMovableObject.

Ale wyobrażam sobie, że niszczycie obiekty za pomocą wskaźnika NonMoveableObject, w którym to przypadku potrzebowalibyście wirtualnych destruktorów i nie byłby to już pusta klasa. :)


Myślałem też o tej sprawie, ale co możesz zrobić z taką listą obiektów, które nie ujawniają żadnego zachowania? Najprawdopodobniej jakoś znajdziesz prawdziwy typ i użyjesz obsady, aby uzyskać dostęp do dostępnych metod i pól - zdecydowanie zapach kodu.
user281377

Tak, to dobra uwaga.
tenpn

1

Jedną z sytuacji, w których możesz to zobaczyć, jest sytuacja, w której silnie powiązana kolekcja zawiera inne typy heterogeniczne. Nie mówię, że to świetny pomysł, może wskazywać na słabą abstrakcję lub zły projekt, ale się zdarza. Jak wspomniałeś, jest to sposób kategoryzacji rzeczy i może być użyteczny i całkowicie poprawny.

Np. Chcesz, aby kolekcja zawierała koty i psy. W swoim projekcie decydujesz, że mają ze sobą wiele wspólnego, więc masz podstawową klasę Mamal i używaj tego typu w swojej kolekcji (np. Lista). Wszystko dobrze. Następnie decydujesz, że chcesz dodać kilka żab do swojej kolekcji, ale nie są to ssaki, więc jednym ze sposobów może być uczynienie z nich podklasy zwierząt, ale może nie możesz myśleć o tym, ile żab i mamali wspólne, więc zwierzę pozostaje puste. Następnie wpisujesz swoją kolekcję za pomocą Animal zamiast Mamal i wszystko jest w porządku.

W prawdziwym życiu stwierdzam, że nawet jeśli wcześniej lub później utworzę pustą klasę podstawową, znajdzie się w niej jakaś funkcjonalność, ale na pewno są czasy, kiedy istnieją.


Jedno pytanie, które należy zadać, brzmi: jeśli typy nie mają ze sobą nic wspólnego, dlaczego są przechowywane w tej samej kolekcji? Nie ma operacji, którą można wykonać na wszystkich przedmiotach w kolekcji, co w pewnym sensie nie pozwala na taką kolekcję. Teraz mogą istnieć pewne sztuczne operacje, które możesz chcieć dodać do obu, aby uprościć logikę - np. Implementując wzorzec projektowy Odwiedzającego - w którym momencie może się to znów przydać, ale teraz mają coś wspólnego ...
Jules
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.