it-swarm-eu.dev

Code-Geruch: Vererbungsmissbrauch

In der OO Community) ist allgemein anerkannt, dass man "Komposition gegenüber Vererbung bevorzugen" sollte. Andererseits bietet Vererbung sowohl Polymorphismus als auch eine einfache, knappe Möglichkeit, alles an eine Basisklasse zu delegieren Es sei denn, dies wird ausdrücklich überschrieben und ist daher äußerst praktisch und nützlich. Die Delegierung kann häufig (wenn auch nicht immer) ausführlich und spröde sein.

Das offensichtlichste und meiner Meinung nach sicherste Anzeichen für Erbschaftsmissbrauch ist die Verletzung des Liskov-Substitutionsprinzips . Was sind einige andere Anzeichen dafür, dass Vererbung das falsche Werkzeug für den Job ist, auch wenn es zweckmäßig erscheint?

52
dsimcha

Wenn Sie nur erben, um Funktionalität zu erhalten, missbrauchen Sie wahrscheinlich die Vererbung. Dies führt zur Schaffung eines Gottobjekts .

Die Vererbung selbst ist kein Problem, solange Sie eine echte Beziehung zwischen den Klassen sehen (wie die klassischen Beispiele, wie z. B. Dog erweitert Animal) und Sie der übergeordneten Klasse keine Methoden zuweisen, die in einigen Fällen keinen Sinn ergeben Kinder (zum Beispiel würde das Hinzufügen einer Bark () -Methode in der Animal-Klasse in einer Fish-Klasse, die Animal erweitert, keinen Sinn ergeben).

Wenn eine Klasse Funktionalität benötigt, verwenden Sie Komposition (möglicherweise wird der Funktionsanbieter in den Konstruktor eingefügt?). Wenn eine Klasse wie eine andere sein muss, verwenden Sie die Vererbung.

48
Fernando

Ich würde sagen, dass Vererbung ein Code-Geruch an sich ist, wenn man das Risiko eingeht, abgeschossen zu werden :)

Das Problem bei der Vererbung ist, dass sie für zwei orthogonale Zwecke verwendet werden kann:

  • schnittstelle (für Polymorphismus)
  • implementierung (zur Wiederverwendung von Code)

Ein einziger Mechanismus, um beides zu erhalten, führt in erster Linie zu "Vererbungsmissbrauch", da die meisten Leute erwarten, dass es bei der Vererbung um die Schnittstelle geht, aber es kann verwendet werden, um die Standardimplementierung auch von ansonsten vorsichtigen Programmierern zu erhalten (das ist einfach so einfach) übersehen dies ...)

Tatsächlich haben moderne Sprachen wie Haskell oder Go die Vererbung aufgegeben, um beide Anliegen zu trennen.

Wieder auf Kurs:

Ein Verstoß gegen das Liskov-Prinzip ist daher das sicherste Zeichen, da dies bedeutet, dass der "Schnittstellen" -Teil des Vertrags nicht eingehalten wird.

Selbst wenn die Schnittstelle respektiert wird, haben Sie möglicherweise Objekte, die von "fetten" Basisklassen erben, nur weil eine der Methoden als nützlich erachtet wurde.

Daher reicht das Liskov-Prinzip an sich nicht aus. Sie müssen wissen, ob der Polymorphismus verwendet ist oder nicht. Wenn nicht, dann hatte es nicht viel Sinn, überhaupt zu erben, das ist ein Missbrauch.

Zusammenfassung:

  • erzwinge das Liskov Principle
  • überprüfen Sie, ob es tatsächlich verwendet wird

Ein Weg, um es zu umgehen:

Eine klare Trennung der Bedenken auferlegen:

  • nur von Schnittstellen erben
  • verwenden Sie die Komposition, um die Implementierung zu delegieren

bedeutet, dass für jede Methode mindestens ein paar Tastenanschläge erforderlich sind, und plötzlich beginnen die Leute darüber nachzudenken, ob es eine so gute Idee ist, diese "fette" Klasse für nur ein kleines Stück davon wiederzuverwenden.

39
Matthieu M.

Ist es wirklich "allgemein anerkannt"? Es ist eine Idee, die ich einige Male gehört habe, aber es ist kaum ein allgemein anerkanntes Prinzip. Wie Sie gerade betont haben, sind Vererbung und Polymorphismus die Kennzeichen von OOP. Ohne sie haben Sie nur prozedurale Programmierung mit doof Syntax.

Komposition ist ein Werkzeug, um Objekte auf bestimmte Weise zu bauen. Vererbung ist ein Werkzeug, um Objekte auf andere Weise zu erstellen. An beiden ist nichts auszusetzen. Sie müssen nur wissen, wie beide funktionieren und wann es angemessen ist, jeden Stil zu verwenden.

14
Mason Wheeler

Die Stärke der Vererbung ist die Wiederverwendbarkeit. Von Schnittstelle, Daten, Verhalten, Kollaborationen. Es ist ein nützliches Muster, um Attribute an neue Klassen zu übertragen.

Der Nachteil bei der Verwendung der Vererbung ist die Wiederverwendbarkeit. Die Schnittstelle, Daten und das Verhalten werden eingekapselt und massenhaft übertragen, was es zu einem Alles-oder-Nichts-Angebot macht. Die Probleme werden besonders deutlich, wenn Hierarchien tiefer werden, da Eigenschaften, die abstrakt nützlich sein könnten, weniger werden und Eigenschaften auf lokaler Ebene zu verschleiern beginnen.

Jede Klasse, die das Kompositionsobjekt verwendet, muss denselben Code mehr oder weniger erneut implementieren, um mit ihm zu interagieren. Diese Vervielfältigung erfordert zwar eine Verletzung von DRY, bietet Designern jedoch mehr Freiheit bei der Auswahl der Eigenschaften einer Klasse, die für ihre Domäne am aussagekräftigsten sind. Dies ist besonders vorteilhaft, wenn der Unterricht spezialisierter wird.

Im Allgemeinen sollte es bevorzugt werden, Klassen über Komposition zu erstellen und diese Klassen dann mit dem Großteil ihrer gemeinsamen Eigenschaften in Vererbung umzuwandeln, wobei die Unterschiede als Kompositionen verbleiben.

IOW, YAGNI (Du wirst keine Vererbung brauchen).

5
Huperniketes

Wenn Sie A der Klasse B unterordnen, es aber niemanden interessiert, ob B von A erbt oder nicht, ist dies das Zeichen dafür, dass Sie es möglicherweise falsch machen .

3
tia

"Komposition gegenüber Vererbung bevorzugen" ist die albernste Behauptung. Sie sind beide wesentliche Elemente von OOP. Es ist wie zu sagen "Bevorzugen Sie Hämmer gegenüber Sägen".

Natürlich kann die Vererbung missbraucht werden, jedes Sprachmerkmal kann missbraucht werden, bedingte Anweisungen können missbraucht werden.

2

Am offensichtlichsten ist es vielleicht, wenn die erbende Klasse einen Stapel von Methoden/Eigenschaften enthält, die in der exponierten Schnittstelle niemals verwendet werden. Im schlimmsten Fall, wenn die Basisklasse abstrakte Elemente hat, finden Sie leere (oder bedeutungslose) Implementierungen der abstrakten Elemente, um sozusagen die Lücken zu füllen.

Subtiler sieht man manchmal, wo in dem Bestreben, die Wiederverwendung von Code zu erhöhen, zwei Dinge mit ähnlichen Implementierungen in einer Implementierung zusammengefasst wurden - obwohl diese beiden Dinge nicht miteinander zusammenhängen. Dies erhöht tendenziell die Codekopplung und entwirrt sie, wenn sich herausstellt, dass die Ähnlichkeit nur ein Zufall war. Dies kann sehr schmerzhaft sein, wenn sie nicht frühzeitig erkannt wird.

Aktualisiert mit ein paar Beispielen: Obwohl dies nicht direkt mit der Programmierung als solcher zu tun hat, denke ich, dass das beste Beispiel für solche Dinge die Lokalisierung/Internationalisierung ist. Auf Englisch gibt es ein Wort für "Ja". In anderen Sprachen kann es viele, viele mehr geben, um der Frage grammatikalisch zuzustimmen. Jemand könnte sagen, ah, lass uns eine Zeichenfolge für "Ja" haben und das ist in Ordnung für viele Sprachen. Dann stellen sie fest, dass die Zeichenfolge "Ja" nicht wirklich "Ja" entspricht, sondern dem Konzept der "positiven Antwort auf diese Frage" - die Tatsache, dass es immer "Ja" ist, ist ein Zufall und nur die Zeitersparnis Ein "Ja" zu haben, geht völlig verloren, wenn das resultierende Chaos entwirrt wird.

Ich habe ein besonders unangenehmes Beispiel dafür in unserer Codebasis gesehen, in der eine Beziehung zwischen Eltern und Kindern, bei der Eltern und Kind Eigenschaften mit ähnlichen Namen hatten, mit Vererbung modelliert wurde. Niemand bemerkte dies, da alles zu funktionieren schien, dann musste das Verhalten des Kindes (eigentlich Basis) und des Elternteils (Unterklasse) in verschiedene Richtungen gehen. Wie es das Glück wollte, geschah dies genau zum falschen Zeitpunkt, als sich eine Frist abzeichnete - beim Versuch, das Modell hier und da zu optimieren, ging die Komplexität (sowohl in Bezug auf Logik als auch in Bezug auf Codeduplizierung) sofort über das Dach. Es war auch sehr schwierig, darüber nachzudenken/damit zu arbeiten, da der Code einfach nicht damit zusammenhängt, wie das Unternehmen über die Konzepte dieser Klassen dachte oder darüber sprach. Am Ende mussten wir in die Kugel beißen und einige größere Umgestaltungen vornehmen, die völlig hätten vermieden werden können, wenn der ursprüngliche Typ nicht entschieden hätte, dass ein paar ähnlich klingende Eigenschaften mit zufällig gleichen Werten dasselbe Konzept repräsentierten.

0
FinnNk

Ich denke, Schlachten wie Vererbung gegen Komposition sind wirklich nur Aspekte eines tieferen Kernkampfes.

Dieser Kampf lautet: Was führt zu der geringsten Menge an Code für die größte Erweiterbarkeit bei zukünftigen Verbesserungen?

Deshalb ist die Frage so schwer zu beantworten. In einer Sprache kann es sein, dass die Vererbung schneller erfolgt als die Komposition als in einer anderen Sprache oder umgekehrt.

Oder es könnte das spezifische Problem sein, das Sie im Code lösen, das eher von Zweckmäßigkeit als von einer langfristigen Vision des Wachstums profitiert. Das ist genauso ein Geschäftsproblem wie ein Programmierproblem.

Um auf die Frage zurückzukommen: Vererbung kann falsch sein, wenn Sie irgendwann in der Zukunft in eine Zwangsjacke gesteckt werden - oder wenn Code erstellt wird, der nicht vorhanden sein muss (Klassen, die ohne Grund von anderen erben, oder Schlimmer noch, Basisklassen, die anfangen, viele bedingte Tests für Kinder durchzuführen.

Es gibt Situationen, in denen die Vererbung der Komposition vorgezogen werden sollte, und die Unterscheidung ist viel klarer als eine Frage des Stils. Ich paraphrasiere hier von Sutter und Alexandrescus C++ Coding Standards, da sich meine Kopie gerade in meinem Bücherregal befindet. Übrigens sehr zu empfehlen zu lesen.

Zuallererst wird von Vererbung abgeraten, wenn dies nicht erforderlich ist, da dadurch eine sehr enge, eng gekoppelte Beziehung zwischen Basis- und abgeleiteten Klassen hergestellt wird, die später zu Wartungsproblemen führen kann. Es ist nicht nur die Meinung eines Mannes, dass die Syntax für die Vererbung das Lesen erschwert oder so.

Die Vererbung wird jedoch empfohlen, wenn die Klasse abgeleitet selbst in Kontexten wiederverwendet werden soll, in denen die Basisklasse derzeit verwendet wird, ohne dass der Code in diesem Kontext geändert werden muss. Wenn Sie beispielsweise Code haben, der allmählich wie if (type1) type1.foo(); else if (type2) type2.foo(); aussieht, haben Sie wahrscheinlich einen sehr guten Grund, die Vererbung zu betrachten.

Wenn Sie nur Basisklassenmethoden wiederverwenden möchten, verwenden Sie zusammenfassend die Komposition. Wenn Sie eine abgeleitete Klasse in Code verwenden möchten, der derzeit eine Basisklasse verwendet, verwenden Sie die Vererbung.

0
Karl Bielefeldt