Wolę funkcje OOP podczas tworzenia gier w Unity. Zwykle tworzę klasę podstawową (głównie abstrakcyjną) i używam dziedziczenia obiektów, aby udostępniać tę samą funkcjonalność różnym innym obiektom.
Jednak niedawno usłyszałem od kogoś, że należy unikać dziedziczenia i zamiast tego powinniśmy używać interfejsów. Zapytałem więc dlaczego, a on powiedział, że „Dziedziczenie obiektów jest oczywiście ważne, ale jeśli istnieje wiele rozszerzonych obiektów, relacje między klasami są głęboko powiązane.
Używam abstrakcyjną klasę bazową, coś podobnego WeaponBase
i tworzenie konkretnych klas broni jak Shotgun
, AssaultRifle
, Machinegun
coś takiego. Jest tak wiele zalet, a jednym wzorem, który naprawdę lubię, jest polimorfizm. Mogę traktować obiekt utworzony przez podklasy tak, jakby były klasą podstawową, dzięki czemu logikę można drastycznie zredukować i uczynić bardziej użyteczną. Jeśli muszę uzyskać dostęp do pól lub metod podklasy, rzutuję z powrotem do podklasy.
Nie chcę, aby zdefiniować same pola, powszechnie stosowane w różnych klasach, jak AudioSource
/ Rigidbody
/ Animator
i wiele pól członkowskich określonych jak ja fireRate
, damage
, range
, spread
i tak dalej. Również w niektórych przypadkach niektóre metody mogą zostać zastąpione. Dlatego używam w tym przypadku metod wirtualnych, więc w zasadzie mogę wywołać tę metodę przy użyciu metody z klasy nadrzędnej, ale jeśli logika powinna być inna w przypadku dziecka, ręcznie je przesłoniłem.
Czy zatem wszystkie te rzeczy należy porzucić jako złe praktyki? Czy zamiast tego powinienem używać interfejsów?
źródło
Odpowiedzi:
Preferuj kompozycję zamiast dziedziczenia w twojej jednostce i systemach zapasów / przedmiotów. Ta rada ma zwykle zastosowanie do struktur logiki gry, gdy sposób, w jaki można składać złożone rzeczy (w czasie wykonywania), może prowadzić do wielu różnych kombinacji; wtedy wolimy kompozycję.
Preferuj dziedziczenie nad kompozycją w logice na poziomie aplikacji, od konstrukcji interfejsu użytkownika po usługi. Na przykład,
Widget->Button->SpinnerButton
lubConnectionManager->TCPConnectionManager
vs->UDPConnectionManager
.... istnieje tutaj jasno określona hierarchia wyprowadzania, a nie mnogość potencjalnych wyprowadzeń, więc po prostu łatwiej jest zastosować dziedziczenie.
Konkluzja : użyj dziedziczenia tam, gdzie możesz, ale użyj kompozycji tam, gdzie musisz. PS Innym powodem, dla którego możemy preferować kompozycję w systemach encji, jest to, że zwykle istnieje wiele encji, a dziedziczenie może ponieść koszty wydajności w celu uzyskania dostępu do elementów na każdym obiekcie; patrz vtables .
źródło
P.S. The other reason we may favour composition in entity systems is ... performance
: Czy to naprawdę prawda? Patrząc na stronę WIkipedia połączoną z @Linaith, widać, że trzeba skomponować obiekt interfejsów. W związku z tym masz (nadal lub nawet więcej) wywołań funkcji wirtualnych i więcej braków pamięci podręcznej, ponieważ wprowadziłeś kolejny poziom pośredni.Masz już kilka fajnych odpowiedzi, ale ogromny słoń w pokoju w twoim pytaniu to:
Zasadą jest, że gdy ktoś daje ci praktyczną regułę, zignoruj ją. Dotyczy to nie tylko „ktoś ci coś mówi”, ale także czytania rzeczy w Internecie. O ile nie wiesz, dlaczego (i naprawdę możesz za tym stać), takie porady są bezwartościowe i często bardzo szkodliwe.
Z mojego doświadczenia, najważniejszymi i pomocnymi pojęciami w OOP są „niskie sprzężenie” i „wysoka kohezja” (klasy / obiekty wiedzą o sobie jak najmniej, a każda jednostka jest odpowiedzialna za tak mało rzeczy, jak to możliwe).
Niskie sprzężenie
Oznacza to, że każdy „pakiet rzeczy” w twoim kodzie powinien jak najmniej zależeć od jego otoczenia. Dotyczy to klas (projektowanie klas), ale także obiektów (faktyczna implementacja), ogólnie „plików” (tj. Liczby
#include
s na pojedynczy.cpp
plik, liczbyimport
na pojedynczy).java
plik i tak dalej).Znakiem, że dwa byty są sprzężone, jest to, że jeden z nich się zepsuje (lub będzie musiał zostać zmieniony), gdy drugi zostanie zmieniony w jakikolwiek sposób.
Dziedziczenie oczywiście zwiększa sprzężenie; zmiana klasy podstawowej powoduje zmianę wszystkich podklas.
Interfejsy zmniejszają sprzężenie: definiując przejrzystą umowę opartą na metodach, możesz dowolnie zmieniać dowolne ustawienia obu stron interfejsu, o ile nie zmienisz umowy. (Uwaga: „interfejs” to ogólna koncepcja, Java
interface
klasy abstrakcyjne lub C ++ to tylko szczegóły implementacji).Wysoka spójność
Oznacza to, że każda klasa, obiekt, plik itp. Musi zajmować się możliwie najmniejszą odpowiedzialnością. Tj. Unikaj dużych klas, które robią wiele rzeczy. W twoim przykładzie, jeśli twoja broń ma całkowicie odrębne aspekty (amunicja, zachowanie podczas strzelania, przedstawienie graficzne, przedstawienie zapasów itp.), Możesz mieć różne klasy, które reprezentują dokładnie jedną z tych rzeczy. Główna klasa broni następnie przekształca się w „posiadacza” tych szczegółów; obiekt broni jest więc niewiele więcej niż kilkoma wskazówkami do tych szczegółów.
W tym przykładzie upewnisz się, że twoja klasa reprezentująca „Zachowanie podczas strzelania” wie jak najmniej po ludzku o głównej klasie broni. Optymalnie nic. Oznaczałoby to na przykład, że możesz dać „Zachowanie podczas strzelania” każdemu obiektowi w twoim świecie (wieżyczki, wulkany, postacie ...) jednym dotknięciem palca. Jeśli w pewnym momencie chcesz zmienić sposób reprezentowania broni w ekwipunku, możesz to po prostu zrobić - tylko twoja klasa ekwipunku w ogóle o tym wie.
Znakiem, że istota nie jest spójna, jest to, że rośnie i rośnie, rozgałęzia się w kilku kierunkach jednocześnie.
Dziedziczenie, jak to opisujesz, zmniejsza spójność - twoje klasy broni to w końcu duże kawałki, które zajmują się różnymi rodzajami niezwiązanymi z twoją bronią.
Interfejsy pośrednio zwiększają spójność, wyraźnie rozdzielając obowiązki między dwie strony interfejsu.
Co zrobić teraz
Nadal nie ma twardych i szybkich zasad, wszystko to tylko wytyczne. Ogólnie rzecz biorąc, jak wspomniał użytkownik TKK w swojej odpowiedzi, dziedziczenie uczy się dużo w szkole i książkach; to fantazyjne rzeczy o OOP. Interfejsy są prawdopodobnie bardziej nudne w nauczaniu, a także (jeśli pominąć trywialne przykłady) nieco trudniejsze, otwierając pole wstrzykiwania zależności, które nie jest tak jednoznaczne jak dziedziczenie.
Pod koniec dnia schemat oparty na dziedziczeniu jest nadal lepszy niż brak wyraźnego projektu OOP. Więc nie krępuj się. Jeśli chcesz, możesz trochę przemyśleć / google o niskim sprzężeniu, wysokiej kohezji i sprawdzić, czy chcesz dodać tego rodzaju myślenie do swojego arsenału. Zawsze możesz ponownie to sprawdzić, jeśli chcesz, później; lub wypróbuj podejścia oparte na interfejsie w swoim następnym większym nowym module kodu.
źródło
Pomysł, że należy unikać dziedziczenia, jest po prostu błędny.
Istnieje zasada kodowania o nazwie Kompozycja nad dziedziczeniem . Mówi, że możesz osiągnąć te same rzeczy za pomocą kompozycji i jest to preferowane, ponieważ możesz ponownie wykorzystać część kodu. Zobacz, dlaczego wolę kompozycję niż dziedziczenie?
Muszę powiedzieć, że podoba mi się twoja klasa broni i czy zrobiłbym to samo. Ale do tej pory nie stworzyłem gry ...Jak zauważył James Trotter, kompozycja może mieć pewne zalety, zwłaszcza jeśli chodzi o elastyczność w czasie wykonywania, aby zmienić sposób działania broni. Byłoby to możliwe dzięki dziedziczeniu, ale jest trudniejsze.
źródło
Problem polega na tym, że dziedziczenie prowadzi do sprzężenia - twoje obiekty muszą wiedzieć o sobie więcej. Dlatego reguła brzmi: „Zawsze preferuj kompozycję nad dziedziczeniem”. To nie znaczy, NIGDY nie używaj dziedziczenia, oznacza to, że używaj go tam, gdzie jest to całkowicie właściwe, ale jeśli kiedykolwiek tam siedzisz i myślisz: „Mogę to zrobić w obie strony i oba mają sens”, po prostu przejdź do kompozycji.
Dziedziczenie może być również swego rodzaju ograniczeniem. Masz „WeaponBase”, który może być AssultRifle - niesamowity. Co się dzieje, gdy masz strzelbę z podwójną lufą i chcesz pozwolić lufom strzelać niezależnie - trochę trudniej, ale wykonalnie, masz klasę jednej lufy i podwójnej lufy, ale nie możesz po prostu zamontować 2 pojedynczych luf na tym samym pistolecie, prawda? Czy możesz zmodyfikować jedną, aby mieć 3 baryłki, czy potrzebujesz do tego nowej klasy?
Co z AssultRifle z GrenadeLauncher pod spodem - hmm, trochę trudniej. Czy możesz zastąpić GrenadeLauncher latarką do nocnych polowań?
Wreszcie, w jaki sposób pozwalasz użytkownikowi tworzyć poprzednie pistolety, edytując plik konfiguracyjny? Jest to trudne, ponieważ masz zakodowane relacje, które mogą być lepiej skomponowane i skonfigurowane z danymi.
Wielokrotne dziedziczenie może nieco naprawić niektóre z tych trywialnych przykładów, ale dodaje własny zestaw problemów.
Zanim pojawiły się popularne powiedzenia, takie jak „Wolę kompozycję niż dziedziczenie”, dowiedziałem się o tym, pisząc zbyt skomplikowane drzewo dziedziczenia (co było niesamowitą zabawą i działało idealnie), a następnie stwierdziłem, że naprawdę trudno było je później modyfikować i utrzymywać. To powiedzenie jest po prostu, abyś miał alternatywny i zwarty sposób na naukę i zapamiętywanie takiej koncepcji bez konieczności przechodzenia przez całe doświadczenie - ale jeśli chcesz intensywnie korzystać z dziedziczenia, zalecam zachować otwarty umysł i ocenić, jak dobrze działa. dla ciebie - nic strasznego się nie wydarzy, jeśli będziesz intensywnie korzystał z dziedziczenia, a w najgorszym przypadku może to być wspaniałe doświadczenie edukacyjne (w najlepszym przypadku może być dla ciebie dobre)
źródło
Monster
podklasęWalkingEntity
. Potem dodali szlam. Jak myślisz, jak dobrze to zadziałało?W przeciwieństwie do innych odpowiedzi, nie ma to nic wspólnego z dziedziczeniem a kompozycją. Dziedziczenie a kompozycja to decyzja, którą podejmujesz odnośnie tego, jak klasa zostanie zaimplementowana. Interfejsy kontra klasy to decyzja, która pojawia się wcześniej.
Interfejsy są pierwszorzędnymi obywatelami dowolnego języka OOP. Klasy są dodatkowymi szczegółami implementacji. Umysły nowych programistów są poważnie wypaczone przez nauczycieli i książki, które wprowadzają klasy i dziedziczenie przed interfejsami.
Kluczową zasadą jest to, że tam, gdzie to możliwe, typami wszystkich parametrów metody i zwracanych wartości powinny być interfejsy, a nie klasy. Dzięki temu interfejsy API są znacznie bardziej elastyczne i wydajne. Zdecydowana większość czasu, twoje metody i osoby wywołujące nie powinny mieć powodu, aby wiedzieć lub dbać o rzeczywiste klasy obiektów, z którymi mają do czynienia. Jeśli Twój interfejs API nie zgadza się ze szczegółami implementacji, możesz swobodnie przełączać się między kompozycją a dziedziczeniem, nie niszcząc niczego.
źródło
interface
(słowa kluczowego języka).Ilekroć ktoś mówi ci, że jedno konkretne podejście jest najlepsze we wszystkich przypadkach, jest to to samo, co mówienie, że ten sam lek leczy wszystkie choroby.
Dziedziczenie a kompozycja jest pytaniem typu „czy przeciw”. Interfejsy to kolejny (trzeci) sposób, odpowiedni w niektórych sytuacjach.
Dziedziczenie, czyli logika: używasz go, gdy nowa klasa ma się zachowywać, i używasz go zupełnie jak (na zewnątrz) stara klasa, z której ją wywodzisz, jeśli nowa klasa ma być publicznie dostępna wszystkie funkcje, które posiadała stara klasa ... wtedy dziedziczysz.
Skład lub logika: jeśli nowa klasa musi tylko wewnętrznie używać starej klasy, bez ujawniania jej cech publicznych, wówczas używasz kompozycji - to znaczy, masz instancję starej klasy jako właściwość członka lub zmienna nowego. (Może to być własność prywatna, chroniona lub cokolwiek, w zależności od przypadku użycia). Kluczową kwestią jest to, że jest to przypadek użycia, w którym nie chcesz ujawniać oryginalnych funkcji klasowych i używać ich publicznie, wystarczy użyć ich wewnętrznie, podczas gdy w przypadku dziedziczenia udostępniasz je publicznie, poprzez nowa klasa. Czasami potrzebujesz jednego podejścia, a czasem drugiego.
Interfejsy: interfejsy mają jeszcze jeden przypadek użycia - gdy chcesz, aby nowa klasa częściowo, a nie całkowicie, wdrożyła i ujawniła publicznie funkcjonalność starej. Dzięki temu masz nową klasę, klasę z zupełnie innej hierarchii niż stara, zachowującą się jak stara tylko w niektórych aspektach.
Powiedzmy, że masz różne stworzenia reprezentowane przez klasy i mają one funkcje reprezentowane przez funkcje. Na przykład ptak miałby Talk (), Fly () i Poop (). Klasa Kaczka odziedziczyłaby po klasie Ptak, wdrażając wszystkie funkcje.
Class Fox oczywiście nie może latać. Jeśli więc zdefiniujesz przodka, który ma wszystkie funkcje, nie możesz poprawnie wyprowadzić potomka.
Jeśli jednak podzielisz funkcje na grupy reprezentujące każdą grupę wywołań za pomocą interfejsu, powiedzmy IFly, zawierającego Takeoff (), FlapWings () i Land (), możesz dla klasy Fox zaimplementować funkcje z ITalk i IPoop ale nie IFly. Następnie zdefiniowałbyś zmienne i parametry, aby zaakceptować obiekty, które implementują określony interfejs, a następnie kod współpracujący z nimi wiedziałby, co może wywołać ... i zawsze może zapytać o inne interfejsy, jeśli musi sprawdzić, czy inne funkcjonalności są również zaimplementowane dla bieżącego obiektu.
Każde z tych podejść ma przypadki użycia, gdy jest najlepsze, żadne podejście nie jest absolutnie najlepszym rozwiązaniem dla wszystkich przypadków.
źródło
W przypadku gry, zwłaszcza z Unity, która współpracuje z architekturą elementów składowych bytu, powinieneś preferować kompozycję zamiast dziedziczenia składników gry. Jest o wiele bardziej elastyczny i pozwala uniknąć sytuacji, w której chciałbyś, aby istota gry była „dwiema” rzeczami, które znajdują się na różnych liściach drzewa spadkowego.
Powiedzmy na przykład, że masz TalkingAI na jednej gałęzi drzewa spadkowego, a VolumetricCloud na innej osobnej gałęzi, a chcesz chmurę mówiącą. Jest to trudne w przypadku drzewa głębokiego dziedzictwa. Dzięki encji-komponentowi po prostu tworzysz encję, która ma komponenty TalkingAI i Cloud, i możesz zacząć.
Ale to nie znaczy, że powiedzmy w implementacji chmury wolumetrycznej, nie powinieneś używać dziedziczenia. Kod tego może być znaczny i składać się z kilku klas, i możesz użyć OOP w razie potrzeby. Ale wszystko będzie sprowadzało się do jednego komponentu gry.
Na marginesie, mam z tym problem:
Dziedziczenie nie służy do ponownego użycia kodu. To dla ustanowienia is-a związek. Wiele razy idzie to w parze, ale musisz być ostrożny. Tylko dlatego, że dwa moduły mogą wymagać użycia tego samego kodu, nie oznacza to, że jeden jest tego samego typu co drugi.
Możesz użyć interfejsów, jeśli chcesz, powiedzmy, mieć w sobie
List<IWeapon>
różne rodzaje broni. W ten sposób możesz dziedziczyć zarówno interfejs IWeapon, jak i podklasę MonoBehaviour dla wszystkich rodzajów broni i uniknąć problemów z brakiem wielokrotnego dziedziczenia.źródło
Wygląda na to, że nie rozumiesz, czym jest interfejs. Zajęło mi ponad 10 lat myślenia o OO i zadawanie naprawdę mądrym ludziom pytań, czy mogłem mieć chwilę ah-ha z interfejsami. Mam nadzieję że to pomoże.
Najlepiej jest użyć ich kombinacji. W moim umyśle OO modelowanie świata i ludzi, powiedzmy, głęboki przykład. Dusza jest połączona z ciałem poprzez umysł. Umysł JEST interfejsem między duszą (niektórzy uważają intelekt) a poleceniami, jakie wydaje ciału. Funkcjonalny interfejs umysłu do ciała jest taki sam dla wszystkich ludzi.
Tę samą analogię można zastosować między osobą (ciałem i duszą) łączącą się ze światem. Ciało jest interfejsem między umysłem a światem. Każdy łączy się ze światem w ten sam sposób, jedyną wyjątkowością jest to, w jaki sposób wchodzimy w interakcję ze światem, czyli w jaki sposób używamy naszego UMYSŁU, aby DECYZOWAĆ, w jaki sposób łączymy się / współdziałamy w świecie.
Interfejs jest wtedy po prostu mechanizmem do powiązania twojej struktury OO z inną inną strukturą OO, przy czym każda strona ma różne atrybuty, które muszą negocjować / mapować względem siebie, aby wytworzyć funkcjonalny wynik w innym medium / środowisku.
Tak więc, aby umysł mógł wywołać funkcję otwartej ręki, musi zaimplementować interfejs nerwowy_komenda_system, KTÓRY ma swój własny interfejs API z instrukcjami, jak zaimplementować wszystkie wymagania niezbędne przed wywołaniem otwartej ręki.
źródło