Ich bevorzuge OOP-Funktionen, wenn ich Spiele mit Unity entwickle. Normalerweise erstelle ich eine Basisklasse (meistens abstrahiert) und nutze die Objektvererbung, um die gleiche Funktionalität für die verschiedenen anderen Objekte zu nutzen.
Ich habe jedoch kürzlich von jemandem gehört, dass die Verwendung von Vererbung vermieden werden muss und wir stattdessen Schnittstellen verwenden sollten. Also fragte ich warum und er sagte, dass "Objektvererbung natürlich wichtig ist, aber wenn es viele erweiterte Objekte gibt, ist die Beziehung zwischen Klassen tief gekoppelt.
Ich verwende eine abstrakte Basisklasse, so etwas wie WeaponBase
und bestimmte Waffenklassen zu schaffen , wie Shotgun
, AssaultRifle
, Machinegun
so ähnlich. Es gibt so viele Vorteile und ein Muster, das ich wirklich mag, ist Polymorphismus. Ich kann das von Unterklassen erstellte Objekt so behandeln, als ob es die Basisklasse wäre, damit die Logik drastisch reduziert und wiederverwendbarer wird. Wenn ich auf die Felder oder Methoden der Unterklasse zugreifen muss, kehre ich zur Unterklasse zurück.
Ich möchte nicht , gleiche Felder definieren, die üblicherweise in verschiedenen Klassen, wie AudioSource
/ Rigidbody
/ Animator
und viele Mitgliederfelder I definiert wie fireRate
, damage
, range
, spread
und so weiter. In einigen Fällen können auch einige Methoden überschrieben werden. In diesem Fall verwende ich also virtuelle Methoden, sodass ich diese Methode im Grunde mit der Methode der übergeordneten Klasse aufrufen kann. Sollte sich die Logik bei untergeordneten Klassen jedoch unterscheiden, habe ich sie manuell überschrieben.
Sollten all diese Dinge als schlechte Praktiken aufgegeben werden? Soll ich stattdessen Schnittstellen verwenden?
quelle
Antworten:
Bevorzugen Sie die Zusammensetzung gegenüber der Vererbung in Ihrem Entitäts- und Inventar- / Objektsystem. Dieser Rat gilt in der Regel für Spielelogikstrukturen, bei denen die Art und Weise, wie Sie komplexe Dinge (zur Laufzeit) zusammenstellen können, zu vielen verschiedenen Kombinationen führen kann. Dann bevorzugen wir die Komposition.
Bevorzugen Sie die Vererbung gegenüber der Komposition in Ihrer Logik auf Anwendungsebene, von UI-Konstrukten bis hin zu Diensten. Beispielsweise,
Widget->Button->SpinnerButton
oderConnectionManager->TCPConnectionManager
vs->UDPConnectionManager
.... hier gibt es eine klar definierte Ableitungshierarchie und keine Vielzahl möglicher Ableitungen, sodass die Verwendung der Vererbung einfacher ist.
Fazit : Verwenden Sie Vererbung, wo Sie können, aber Komposition, wo Sie müssen. PS Der andere Grund, warum wir die Komposition in Entitätssystemen bevorzugen, ist, dass es normalerweise viele Entitäten gibt und die Vererbung für den Zugriff auf Mitglieder auf jedes Objekt einen Leistungsaufwand verursachen kann. siehe vtables .
quelle
P.S. The other reason we may favour composition in entity systems is ... performance
: Ist das wirklich wahr? Wenn Sie sich die durch @Linaith verknüpfte Wikipedia-Seite ansehen, werden Sie feststellen, dass Sie Ihr Objekt aus Schnittstellen zusammenstellen müssen. Daher haben Sie (noch oder noch mehr) virtuelle Funktionsaufrufe und mehr Cache-Fehler, als Sie eine andere Indirektionsebene eingeführt haben.Sie haben bereits ein paar nette Antworten erhalten, aber der riesige Elefant im Raum in Ihrer Frage ist dieser:
Als Faustregel gilt: Wenn Ihnen jemand eine Faustregel gibt, ignorieren Sie diese. Dies gilt nicht nur für "jemand, der Ihnen etwas erzählt", sondern auch für das Lesen von Inhalten im Internet. Solcher Rat ist wertlos und oft sehr schädlich, es sei denn, Sie wissen warum (und können wirklich dahinter stehen).
Nach meiner Erfahrung sind die wichtigsten und hilfreichsten Konzepte in OOP "niedrige Kopplung" und "hohe Kohäsion" (Klassen / Objekte wissen so wenig wie möglich voneinander und jede Einheit ist für so wenig wie möglich verantwortlich).
Geringe Kopplung
Dies bedeutet, dass jedes "Bündel von Dingen" in Ihrem Code so wenig wie möglich von der Umgebung abhängen sollte. Dies gilt für Klassen (Klassenentwurf), aber auch für Objekte (tatsächliche Implementierung), "Dateien" im Allgemeinen (dh Anzahl
#include
s pro einzelne.cpp
Datei, Anzahlimport
pro einzelne.java
Datei usw.).Ein Zeichen dafür, dass zwei Entitäten gekoppelt sind, ist, dass eine von ihnen unterbrochen wird (oder geändert werden muss), wenn die andere in irgendeiner Weise geändert wird.
Vererbung erhöht offensichtlich die Kopplung; Durch Ändern der Basisklasse werden alle Unterklassen geändert.
Schnittstellen reduzieren die Kopplung: Indem Sie einen klaren, methodenbasierten Vertrag definieren, können Sie alles an beiden Seiten der Schnittstelle frei ändern, solange Sie den Vertrag nicht ändern. (Beachten Sie, dass "interface" ein allgemeines Konzept ist. Die
interface
abstrakten Java- oder C ++ - Klassen sind lediglich Implementierungsdetails.)Hoher Zusammenhalt
Dies bedeutet, dass jede Klasse, jedes Objekt, jede Datei usw. so wenig wie möglich betroffen oder dafür verantwortlich ist. Das heißt, vermeiden Sie große Klassen, die viel zu tun haben. Wenn Ihre Waffen in Ihrem Beispiel völlig unterschiedliche Aspekte haben (Munition, Schussverhalten, grafische Darstellung, Inventardarstellung usw.), können Sie verschiedene Klassen haben, die genau eines dieser Dinge darstellen. Die Hauptwaffenklasse verwandelt sich dann in einen "Inhaber" dieser Details; Ein Waffenobjekt ist dann kaum mehr als ein paar Zeiger auf diese Details.
In diesem Beispiel würden Sie sicherstellen, dass Ihre Klasse, die das "Feuerverhalten" darstellt, so wenig wie möglich über die Hauptwaffenklasse weiß. Optimalerweise gar nichts. Dies würde zum Beispiel bedeuten, dass Sie jedem Objekt in Ihrer Welt (Türmen, Vulkanen, NSCs ...) mit einem Fingerschnipp "Feuerverhalten" verleihen könnten . Wenn Sie irgendwann ändern möchten, wie Waffen im Inventar dargestellt werden, können Sie dies einfach tun - nur Ihre Inventarklasse weiß darüber Bescheid.
Ein Zeichen dafür, dass eine Entität nicht zusammenhängend ist, ist, dass sie immer größer wird und sich gleichzeitig in mehrere Richtungen verzweigt.
Erbschaft, wie Sie sie beschreiben, verringert den Zusammenhalt - Ihre Waffenklassen sind letztendlich große Brocken, die alle möglichen unterschiedlichen, nicht verwandten Aspekte Ihrer Waffen behandeln.
Schnittstellen erhöhen indirekt den Zusammenhalt, indem sie Verantwortlichkeiten zwischen den beiden Seiten der Schnittstelle klar aufteilen.
Was nun
Es gibt immer noch keine festen Regeln, all dies sind nur Richtlinien. Im Allgemeinen wird, wie der Benutzer TKK in seiner Antwort erwähnte, die Vererbung viel in der Schule und in Büchern unterrichtet. es ist das schicke Zeug über OOP. Das Lehren von Interfaces ist wahrscheinlich langweiliger und erschließt (wenn Sie an trivialen Beispielen vorbeigehen) das Feld der Abhängigkeitsinjektion, das nicht so eindeutig ist wie die Vererbung.
Letztendlich ist Ihr vererbungsbasiertes Schema immer noch besser, als überhaupt kein klares OOP-Design zu haben. Fühlen Sie sich also frei, dabei zu bleiben. Wenn Sie möchten, können Sie ein wenig über niedrige Kopplung, hohe Kohäsion nachdenken und nachsehen, ob Sie Ihrem Arsenal diese Art des Denkens hinzufügen möchten. Sie können jederzeit nacharbeiten, um dies zu einem späteren Zeitpunkt auszuprobieren. Oder probieren Sie schnittstellenbasierte Ansätze in Ihrem nächsten größeren neuen Codemodul aus.
quelle
Die Idee, dass Vererbung vermieden werden muss, ist einfach falsch.
Es gibt ein Kodierungsprinzip namens Komposition über Vererbung . Darin heißt es, dass Sie mit Komposition das Gleiche erreichen können, und dies ist vorzuziehen, da Sie einen Teil des Codes wiederverwenden können. Siehe Warum sollte ich die Komposition der Vererbung vorziehen?
Ich muss sagen, ich mag deine Waffenklassen und würde es genauso machen. Aber ich habe noch kein Spiel gemacht ...Wie von James Trotter hervorgehoben, könnte die Komposition einige Vorteile haben, insbesondere in Bezug auf die Flexibilität zur Laufzeit, um die Funktionsweise der Waffe zu ändern. Dies wäre mit Vererbung möglich, aber es ist schwieriger.
quelle
Das Problem ist, dass Vererbung zu Kopplung führt - Ihre Objekte müssen mehr voneinander wissen. Deshalb lautet die Regel "Komposition immer vor Vererbung bevorzugen". Dies bedeutet nicht, NIEMALS Vererbung zu verwenden, sondern nur, wo es völlig angemessen ist. Wenn Sie jedoch jemals dagesitzen und denken, "Ich könnte dies auf beide Arten tun, und beide ergeben einen Sinn", gehen Sie direkt zur Komposition über.
Vererbung kann auch einschränkend sein. Sie haben eine "WeaponBase", die ein AssultRifle sein kann - fantastisch. Was passiert, wenn Sie eine Doppellauf-Schrotflinte haben und die Läufe unabhängig schießen lassen möchten? Etwas härter, aber machbar, Sie haben nur eine Klasse mit einem Lauf und zwei Läufen, aber Sie können nicht einfach zwei Läufe montieren auf der gleichen Waffe, können Sie? Könntest du einen so umbauen, dass er 3 Fässer hat oder brauchst du dafür eine neue Klasse?
Was ist mit einem AssultRifle mit einem GrenadeLauncher darunter - hmm, etwas härter. Können Sie den GrenadeLauncher durch eine Taschenlampe für die Nachtjagd ersetzen?
Wie können Sie Ihrem Benutzer schließlich erlauben, die vorhergehenden Waffen zu erstellen, indem Sie eine Konfigurationsdatei bearbeiten? Dies ist schwierig, da Sie Beziehungen fest programmiert haben, die möglicherweise besser mit Daten erstellt und konfiguriert sind.
Die mehrfache Vererbung kann einige dieser trivialen Beispiele in gewisser Weise beheben, fügt jedoch eine Reihe eigener Probleme hinzu.
Bevor es übliche Redewendungen wie "Komposition vor Vererbung" gab, habe ich dies herausgefunden, indem ich einen übermäßig komplexen Vererbungsbaum geschrieben habe (was super Spaß machte und perfekt funktionierte) und dann herausgefunden habe, dass es wirklich schwierig war, ihn später zu ändern und zu pflegen. Das Sprichwort ist nur so, dass Sie eine alternative und kompakte Möglichkeit haben, ein solches Konzept zu lernen und sich daran zu erinnern, ohne die gesamte Erfahrung durchlaufen zu müssen. Wenn Sie jedoch stark auf Vererbung setzen möchten, empfehle ich, nur offen zu bleiben und zu bewerten, wie gut es funktioniert für Sie - nichts Schlimmes wird passieren, wenn Sie stark vererbt werden und es im schlimmsten Fall eine großartige Lernerfahrung sein könnte (im besten Fall funktioniert es gut für Sie)
quelle
Monster
eine Unterklasse von gemachtWalkingEntity
. Dann fügten sie Schleime hinzu. Wie gut hat das wohl funktioniert?Im Gegensatz zu den anderen Antworten hat dies nichts mit Vererbung oder Komposition zu tun. Vererbung vs. Komposition ist eine Entscheidung, die Sie hinsichtlich der Implementierung einer Klasse treffen. Schnittstellen vs. Klassen ist eine Entscheidung, die davor liegt.
Interfaces sind die erstklassigen Bürger jeder OOP-Sprache. Klassen sind sekundäre Implementierungsdetails. Neue Programmierer werden von Lehrern und Büchern, die Klassen und Vererbung vor Schnittstellen einführen, in den Bann gezogen.
Das Schlüsselprinzip ist, dass die Typen aller Methodenparameter und Rückgabewerte nach Möglichkeit Schnittstellen und keine Klassen sein sollten. Dies macht Ihre APIs viel flexibler und leistungsfähiger. In den allermeisten Fällen sollten Ihre Methoden und ihre Aufrufer keinen Grund haben, die tatsächlichen Klassen der Objekte, mit denen sie sich befassen, zu kennen oder sich darum zu kümmern. Wenn Ihre API nicht mit Implementierungsdetails vertraut ist, können Sie frei zwischen Komposition und Vererbung wechseln, ohne etwas zu beschädigen.
quelle
interface
(das Sprachschlüsselwort).Wann immer Ihnen jemand sagt, dass ein spezifischer Ansatz für alle Fälle der beste ist, ist es das Gleiche, als würde er Ihnen sagen, dass ein und dasselbe Arzneimittel alle Krankheiten heilt.
Vererbung vs Komposition ist eine Is-A-vs-Has-A-Frage. Schnittstellen sind ein weiterer (dritter) Weg, der für einige Situationen geeignet ist.
Vererbung oder die is-a-Logik: Sie verwenden sie, wenn sich die neue Klasse verhält und vollständig (äußerlich) wie die alte Klasse verwendet wird, von der Sie sie ableiten, wenn die neue Klasse der Öffentlichkeit zugänglich gemacht wird all die Funktionalität, die die alte Klasse hatte ... dann erben Sie.
Komposition oder die Has-A-Logik: Wenn die neue Klasse die alte Klasse nur intern verwenden muss, ohne die Features der alten Klasse der Öffentlichkeit zugänglich zu machen, verwenden Sie Komposition, dh, Sie haben eine Instanz der alten Klasse als Mitgliedseigenschaft oder eine Variable der neuen. (Dies kann je nach Anwendungsfall ein privates oder geschütztes Eigentum sein oder was auch immer). Der entscheidende Punkt hierbei ist, dass dies der Anwendungsfall ist, in dem Sie die ursprünglichen Klassenfeatures nicht der Öffentlichkeit zugänglich machen und verwenden möchten, sondern sie nur intern verwenden möchten, während Sie sie im Fall der Vererbung der Öffentlichkeit über die neue Klasse. Manchmal braucht man einen Ansatz und manchmal den anderen.
Schnittstellen: Schnittstellen sind ein weiterer Anwendungsfall - wenn die neue Klasse die Funktionalität der alten Klasse teilweise und nicht vollständig implementieren und der Öffentlichkeit zugänglich machen soll. Auf diese Weise können Sie eine neue Klasse erstellen, eine Klasse mit einer völlig anderen Hierarchie als die alte, die sich nur in einigen Punkten wie die alte verhält.
Angenommen, Sie haben verschiedene Kreaturen, die durch Klassen dargestellt werden, und Funktionen, die durch Funktionen dargestellt werden. Zum Beispiel würde ein Vogel Talk (), Fly () und Poop () haben. Class Duck würde von Class Bird erben und alle Funktionen implementieren.
Class Fox kann offensichtlich nicht fliegen. Wenn Sie also definieren, dass der Vorfahr alle Features hat, können Sie den Nachfahren nicht richtig ableiten.
Wenn Sie jedoch die Features in Gruppen aufteilen, die jede Gruppe von Aufrufen mit einer Schnittstelle darstellen, z. B. IFly, die Takeoff (), FlapWings () und Land () enthält, können Sie für die Klasse Fox Funktionen von ITalk und IPoop implementieren aber nicht IFly. Sie würden dann Variablen und Parameter definieren, um Objekte zu akzeptieren, die eine bestimmte Schnittstelle implementieren, und dann würde der Code, der mit ihnen arbeitet, wissen, wie er aufgerufen werden kann ... und immer nach anderen Schnittstellen fragen können, wenn geprüft werden muss, ob andere Funktionen ebenfalls vorhanden sind für das aktuelle Objekt implementiert.
Jeder dieser Ansätze hat Anwendungsfälle, bei denen es sich um den besten handelt. Kein Ansatz ist eine absolut beste Lösung für alle Fälle.
quelle
Für ein Spiel, insbesondere mit Unity, das mit einer Entity-Komponenten-Architektur arbeitet, sollten Sie für Ihre Spielkomponenten die Komposition der Vererbung vorziehen. Es ist viel flexibler und verhindert, dass Sie in eine Situation geraten, in der eine Spieleinheit zwei Dinge sein soll, die sich auf verschiedenen Blättern eines Vererbungsbaums befinden.
Angenommen, Sie haben eine TalkingAI in einem Zweig eines Vererbungsbaums und VolumetricCloud in einem anderen Zweig und möchten eine sprechende Cloud. Dies ist bei einem tiefen Vererbungsbaum schwierig. Mit der Entitätskomponente erstellen Sie einfach eine Entität mit TalkingAI- und Cloud-Komponenten, und schon kann es losgehen.
Dies bedeutet jedoch nicht, dass Sie in Ihrer volumetrischen Cloud-Implementierung beispielsweise keine Vererbung verwenden sollten. Der Code dafür ist möglicherweise umfangreich und besteht aus mehreren Klassen. Sie können OOP nach Bedarf verwenden. Aber es wird alles eine einzelne Spielkomponente sein.
Als Randnotiz nehme ich ein Problem damit:
Die Vererbung ist nicht für die Wiederverwendung von Code vorgesehen. Es ist für die Einrichtung eines ist-eine Beziehung. Diese gehen oft Hand in Hand, aber Sie müssen vorsichtig sein. Nur weil zwei Module möglicherweise denselben Code verwenden müssen, bedeutet dies nicht, dass eines vom selben Typ ist wie das andere.
Sie können Schnittstellen verwenden, wenn Sie beispielsweise eine
List<IWeapon>
mit verschiedenen Waffentypen verwenden möchten . Auf diese Weise können Sie sowohl von der IWeapon-Oberfläche als auch von der MonoBehaviour-Unterklasse für alle Waffen erben und Probleme vermeiden, wenn keine Mehrfachvererbung vorliegt.quelle
Sie scheinen nicht zu verstehen, was eine Schnittstelle ist oder tut. Es dauerte mehr als 10 Jahre, bis ich über OO nachgedacht und einige wirklich kluge Leute gefragt hatte, ob ich in der Lage war, einen ah-ha-Moment mit Schnittstellen zu verbringen. Hoffe das hilft.
Das Beste ist, eine Kombination von ihnen zu verwenden. In meinem OO Mind Modeling lassen sich die Welt und die Menschen sagen, ein tiefes Beispiel. Die Seele ist durch den Geist mit dem Körper verbunden. Der Geist IST die Schnittstelle zwischen der Seele (manche betrachten den Intellekt) und den Befehlen, die sie an den Körper ausgibt. Die Schnittstelle des Geistes zum Körper ist funktionell für alle Menschen gleich.
Dieselbe Analogie kann zwischen der Person (Körper und Seele) verwendet werden, die mit der Welt in Kontakt steht. Der Körper ist die Schnittstelle zwischen Geist und Welt. Jeder Mensch verbindet sich auf die gleiche Weise mit der Welt. Die einzige Einzigartigkeit ist, wie wir mit der Welt interagieren. Auf diese Weise entscheiden wir mit unserem VERSTÄNDNIS, wie wir mit der Welt interagieren.
Eine Schnittstelle ist dann einfach ein Mechanismus, um Ihre OO-Struktur mit einer anderen unterschiedlichen OO-Struktur in Beziehung zu setzen, wobei jede Seite unterschiedliche Attribute aufweist, die miteinander verhandelt / abgebildet werden müssen, um eine funktionale Ausgabe in einem anderen Medium / einer anderen Umgebung zu erzeugen.
Damit der Verstand die Open-Hand-Funktion aufrufen kann, muss er die Schnittstelle nervous_command_system implementieren, die über eine eigene API verfügt, die Anweisungen zum Implementieren aller erforderlichen Anforderungen vor dem Aufruf von Open-Hand enthält.
quelle