Vererbung in Programmiersprachen weglassen

10

Ich entwickle meine eigene Programmiersprache. Es ist eine Allzwecksprache (denken Sie an statisch typisiertes Python für den Desktop, dh int x = 1;), die nicht für die Cloud gedacht ist.

Denken Sie, dass es in Ordnung ist, Vererbung oder Mixins nicht zuzulassen? (vorausgesetzt, der Benutzer hätte zumindest Schnittstellen)

Beispiel: Google Go, eine Systemsprache, die die Programmiergemeinschaft schockierte, indem sie keine Vererbung zuließ.

Christopher
quelle

Antworten:

4

Tatsächlich stellen wir immer mehr fest, dass die Verwendung der Vererbung nicht optimal ist, da sie (unter anderem) zu einer engen Kopplung führt.

Die Vererbung von Implementierungen kann immer durch Vererbung von Schnittstellen plus Komposition ersetzt werden, und modernere Software-Designs tendieren dazu, die Vererbung immer weniger zugunsten der Komposition zu verwenden.

Also ja , nicht Vererbung insbesondere die Bereitstellung ist eine vollständig gültig, und sehr in Mode Design - Entscheidung.

Mixins, auf der anderen Seite, sind nicht einmal (noch) nicht zu einem Mainstream - Sprache - Funktion, und Sprachen , die Sie über eine Funktion „mixin“ oft verstehen sehr verschiedene Dinge , indem sie es genannt bieten. Fühlen Sie sich frei, es bereitzustellen oder nicht. Persönlich finde ich es sehr nützlich, aber es richtig umzusetzen kann sehr schwierig sein.

Konrad Rudolph
quelle
13

Es ist sinnlos, darüber nachzudenken, ob Vererbung (oder wirklich ein einzelnes Merkmal) notwendig ist oder nicht, ohne auch den Rest der Semantik der Sprache zu berücksichtigen. Sie streiten in einem Vakuum.

Was Sie brauchen, ist eine konsequente Philosophie des Sprachdesigns. Die Sprache muss in der Lage sein, die Probleme, für die sie entwickelt wurde, elegant zu lösen. Das Modell, um dies zu erreichen, erfordert möglicherweise eine Vererbung oder nicht, aber es ist schwierig, dies ohne das Gesamtbild zu beurteilen.

Wenn Ihre Sprache beispielsweise über erstklassige Funktionen, Teilfunktionsanwendungen, polymorphe Datentypen, Typvariablen und generische Typen verfügt, haben Sie nahezu dieselben Grundlagen wie bei der klassischen OOP-Vererbung, jedoch unter Verwendung eines anderen Paradigmas.

Wenn Sie über eine späte Bindung, dynamische Typisierung, Methoden als Eigenschaften, flexible Funktionsargumente und erstklassige Funktionen verfügen, decken Sie dieselben Gründe ebenfalls ab, verwenden jedoch wieder ein anderes Paradigma.

(Das Finden von Beispielen für die beiden beschriebenen Paradigmen bleibt dem Leser als Übung.)

Denken Sie also über die Art der Semantik nach, die Sie möchten, spielen Sie mit ihnen herum und prüfen Sie, ob sie ohne Vererbung ausreichen. Wenn dies nicht der Fall ist, können Sie entweder entscheiden, die Vererbung in die Mischung zu werfen, oder Sie können entscheiden, dass etwas anderes fehlt.

tdammers
quelle
9

Ja.

Ich denke, es ist in Ordnung, keine Vererbung zuzulassen, insbesondere wenn Ihre Sprache dynamisch eingegeben wird. Sie können eine ähnliche Wiederverwendung von Code beispielsweise durch Delegierung oder Komposition erreichen. Zum Beispiel habe ich einige mäßig komplizierte Programme - okay, nicht so kompliziert :) - in JavaScript ohne Vererbung geschrieben. Grundsätzlich habe ich Objekte als algebraische Datenstrukturen verwendet, wobei einige Funktionen als Methoden angehängt wurden. Ich hatte auch eine Reihe von Funktionen, die keine Methoden waren, die auf diese Objekte angewendet wurden.

Wenn Sie dynamisch tippen - und ich gehe davon aus, dass Sie dies tun -, können Sie Polymorphismus auch ohne Vererbung haben. Wenn Sie zur Laufzeit beliebige Methoden zu Objekten hinzufügen möchten, benötigen Sie Dinge wie Mixins nicht wirklich.

Eine andere Option - die ich für gut halte - ist die Emulation von JavaScript und die Verwendung von Prototypen. Diese sind einfacher als Klassen, aber auch sehr flexibel. Nur etwas zu beachten.

Alles in allem würde ich es also versuchen.

Tikhon Jelvis
quelle
2
Solange es einen vernünftigen Weg gibt, die Aufgaben zu erledigen, die normalerweise mit Vererbung erledigt werden, fahren Sie fort. Sie könnten ein paar Anwendungsfälle ausprobieren, um sicherzugehen (möglicherweise sogar, um Beispielprobleme von anderen zu erbitten, um Selbstbias zu vermeiden ...)
Comingstorm
Nein, es ist statisch und stark getippt, das erwähne ich oben.
Christopher
Da es statisch typisiert ist, können Sie Objekten zur Laufzeit nicht einfach zufällige Methoden hinzufügen. Sie können jedoch weiterhin Enten tippen: Ein Beispiel finden Sie in den Gosu-Protokollen . Ich habe im Sommer ein bisschen Protokolle verwendet und sie waren wirklich nützlich. Gosu hat auch "Verbesserungen", mit denen Klassen nachträglich Methoden hinzugefügt werden können. Sie könnten auch in Betracht ziehen, so etwas hinzuzufügen.
Tikhon Jelvis
2
Bitte, bitte, bitte, hör auf, Vererbung und Wiederverwendung von Code in Beziehung zu setzen. Es ist seit langem bekannt, dass die Vererbung ein wirklich schlechtes Werkzeug für die Wiederverwendung von Code ist.
Jan Hudec
3
Vererbung ist nur ein Werkzeug für die Wiederverwendung von Code. Oft ist es ein sehr gutes Werkzeug, und manchmal ist es schrecklich. Wenn Sie die Komposition der Vererbung vorziehen, bedeutet dies nicht, dass Sie die Vererbung niemals verwenden.
Michael K
8

Ja, es ist eine durchaus vernünftige Entwurfsentscheidung, die Vererbung wegzulassen.

Es gibt in der Tat sehr gute Gründe, die Implementierungsvererbung zu entfernen, da dies zu äußerst komplexem und schwer zu wartendem Code führen kann. Ich würde sogar so weit gehen, Vererbung (wie sie normalerweise in den meisten OOP-Sprachen implementiert ist) als Fehlfunktion zu betrachten.

Clojure bietet beispielsweise keine Implementierungsvererbung und bevorzugt eine Reihe von orthogonalen Merkmalen (Protokolle, Daten, Funktionen, Makros), mit denen dieselben Ergebnisse erzielt werden können, jedoch viel sauberer.

Hier ist ein Video, das ich zu diesem allgemeinen Thema sehr aufschlussreich fand. Rich Hickey identifiziert grundlegende Komplexitätsquellen in Programmiersprachen (einschließlich Vererbung) und präsentiert Alternativen für jede: Einfach gemacht leicht

mikera
quelle
In vielen Fällen ist die Schnittstellenvererbung besser geeignet. Bei Verwendung mit Moderation kann die Vererbung von Implantaten jedoch das Entfernen von viel Code auf dem Boilerplate ermöglichen und ist die eleganteste Lösung. Es erfordert nur Programmierer, die intelligenter und vorsichtiger sind als der durchschnittliche Python / JavaScript-Affe.
Erik Alapää
4

Als ich zum ersten Mal auf die Tatsache stieß, dass VB6-Klassen keine Vererbung unterstützen (nur Schnittstellen), hat mich das wirklich geärgert (und tut es immer noch).

Der Grund dafür ist jedoch, dass es auch keine Konstruktorparameter gab, sodass Sie keine normale Abhängigkeitsinjektion (DI) durchführen konnten. Wenn Sie DI haben, ist dies wichtiger als die Vererbung, da Sie dem Prinzip folgen können, die Komposition der Vererbung vorzuziehen. Das ist sowieso eine bessere Möglichkeit, Code wiederzuverwenden.

Keine Mixins? Wenn Sie eine Schnittstelle implementieren und die gesamte Arbeit dieser Schnittstelle an ein Objekt delegieren möchten, das durch Abhängigkeitsinjektion festgelegt wurde, sind Mixins ideal. Andernfalls müssen Sie den gesamten Boilerplate-Code schreiben, um jede Methode und / oder Eigenschaft an das untergeordnete Objekt zu delegieren. Ich mache es viel (dank C #) und es ist eine Sache, die ich mir wünschte, ich müsste es nicht tun.

Scott Whitlock
quelle
Ja, was diese Frage ausgelöst hat, ist die Implementierung des SVG-DOM, bei dem die Wiederverwendung von Code sehr vorteilhaft ist.
Christopher
2

Um einer anderen Antwort nicht zuzustimmen: Nein, Sie werfen Funktionen aus, wenn sie mit etwas nicht kompatibel sind, das Sie mehr wollen. Java (und andere GC-Sprachen) haben die explizite Speicherverwaltung verworfen, weil es mehr Typensicherheit wollte. Haskell warf eine Mutation aus, weil er mehr Gleichungsdenken und ausgefallene Typen wollte. Sogar C hat bestimmte Arten von Aliasing und anderem Verhalten verworfen (oder für illegal erklärt), weil es mehr Compiler-Optimierungen wollte.

Die Frage ist also: Was wollen Sie mehr als Vererbung?

Ryan Culpepper
quelle
1
Nur dass explizite Speicherverwaltung und Typensicherheit völlig unabhängig und definitiv nicht inkompatibel sind. Gleiches gilt für Mutationen und „Phantasietypen“. Ich stimme der allgemeinen Argumentation zu, aber Ihre Beispiele sind ziemlich schlecht ausgewählt.
Konrad Rudolph
@KonradRudolph, Speicherverwaltung und Typensicherheit sind eng miteinander verbunden. Mit A free/ deleteoperation können Sie Referenzen ungültig machen. Wenn Ihr Typsystem nicht alle betroffenen Referenzen verfolgen kann, ist die Sprache dadurch unsicher. Insbesondere ist weder C noch C ++ typsicher. Es ist wahr, dass Sie auf die eine oder andere Weise Kompromisse eingehen können (z. B. lineare Typen oder Zuordnungsbeschränkungen), um diese zu vereinbaren. Um genau zu sein, hätte ich sagen sollen, dass Java die Typensicherheit mit einem bestimmten, einfachen Typsystem und einer uneingeschränkten Zuordnung mehr wollte.
Ryan Culpepper
Nun, das hängt eher davon ab, wie Sie die Typensicherheit definieren. Wenn Sie beispielsweise die (völlig vernünftige) Definition der Typensicherheit zur Kompilierungszeit verwenden und möchten , dass die Referenzintegrität Teil Ihres Typsystems ist, ist Java auch nicht typsicher (da es nullReferenzen zulässt ). Das Verbieten freeist eine willkürliche zusätzliche Einschränkung. Zusammenfassend hängt die Frage, ob eine Sprache typsicher ist, mehr von Ihrer Definition der Typensicherheit als von der Sprache ab.
Konrad Rudolph
1
@ Ryan: Zeiger sind absolut typsicher. Sie verhalten sich immer nach ihrer Definition. Es mag nicht so sein, wie Sie es möchten, aber es hängt immer davon ab, wie sie definiert sind. Du versuchst sie zu dehnen, um etwas zu sein, was sie nicht sind. Intelligente Zeiger können die Speichersicherheit in C ++ ziemlich trivial gewährleisten.
DeadMG
@KonradRudolph: In Java Tbezieht sich jede Referenz auf nulleines oder ein Objekt einer erweiterten Klasse T. nullist hässlich, aber Operationen zum nullAuslösen einer genau definierten Ausnahme, anstatt die oben beschriebene Typinvariante zu beschädigen. Im Gegensatz zu C ++: Nach einem Aufruf von deletekann ein T*Zeiger auf einen Speicher verweisen, der kein TObjekt mehr enthält . Schlimmer noch, wenn Sie eine Feldzuweisung mit diesem Zeiger in einer Zuweisung durchführen, können Sie ein Feld eines Objekts einer anderen Klasse vollständig aktualisieren, wenn es gerade an einer nahe gelegenen Adresse platziert wurde. Das ist keine Typensicherheit nach einer nützlichen Definition des Begriffs.
Ryan Culpepper
1

Nein.

Wenn Sie ein Basissprachenfeature entfernen möchten, erklären Sie allgemein, dass es niemals notwendig ist (oder ungerechtfertigt schwierig zu implementieren ist, was hier nicht gilt). Und "nie" ist ein starkes Wort in der Softwareentwicklung. Sie würden eine sehr starke Begründung benötigen, um eine solche Erklärung abzugeben.

DeadMG
quelle
8
Das stimmt kaum: Bei Javas gesamtem Design ging es darum, Funktionen wie Überlastung des Bedieners, Mehrfachvererbung und manuelle Speicherverwaltung zu entfernen. Außerdem halte ich es für falsch, Sprachmerkmale als "heilig" zu behandeln. Anstatt das Entfernen eines Features zu rechtfertigen , sollten Sie das Hinzufügen rechtfertigen - ich kann mir keine Features vorstellen, die jede Sprache haben muss.
Tikhon Jelvis
6
@TikhonJelvis: Deshalb Java ist eine schreckliche Sprache, und es Mangel an irgendetwas außer „USE Garbagekollektor INHERITANCE“ ist die Nummer eins Grund , warum. Ich stimme zu, dass Sprachfunktionen gerechtfertigt sein müssen, aber dies ist eine Basissprachenfunktion - sie enthält viele nützliche Anwendungen und kann vom Programmierer nicht repliziert werden, ohne DRY zu verletzen.
DeadMG
1
@DeadMG wow! Ziemlich starke Sprache.
Christopher
@DeadMG: Ich habe angefangen, eine Gegenargumentation als Kommentar zu schreiben, aber dann habe ich daraus eine Antwort gemacht (siehe auch).
Ryan Culpepper
1
"Perfektion wird nicht erreicht, wenn nichts mehr hinzuzufügen ist, sondern wenn nichts mehr zu entfernen ist" (q)
9000
1

Aus rein C ++ - Sicht ist Vererbung für zwei Hauptzwecke nützlich:

  1. Es ermöglicht die Wiederverwendbarkeit von Code
  2. In Kombination mit dem Überschreiben in einer untergeordneten Klasse können Sie einen Basisklassenzeiger verwenden, um ein untergeordnetes Klassenobjekt zu verarbeiten, ohne dessen Typ zu kennen.

Für Punkt 1 können Sie die Vererbung beseitigen, solange Sie eine Möglichkeit haben, Code zu teilen, ohne sich zu viel Akrobatik hingeben zu müssen. Für Punkt 2. Sie können den Java-Weg gehen und auf einer Schnittstelle bestehen, um diese Funktion zu implementieren.

Die Vorteile der Entfernung der Vererbung sind

  1. Verhindern Sie lange Hierarchien und die damit verbundenen Probleme. Ihr Code-Sharing-Mechanismus sollte dafür gut genug sein.
  2. Vermeiden Sie, dass Änderungen in übergeordneten Klassen untergeordnete Klassen unterbrechen.

Der Kompromiss besteht hauptsächlich zwischen "Nicht wiederholen" und Flexibilität auf der einen Seite und Problemvermeidung auf der anderen Seite. Persönlich würde ich es hassen, wenn die Vererbung von C ++ gehen würde, nur weil ein anderer Entwickler möglicherweise nicht klug genug ist, um die Probleme vorherzusehen.

DPD
quelle
1

Denken Sie, dass es in Ordnung ist, Vererbung oder Mixins nicht zuzulassen? (vorausgesetzt, der Benutzer hätte zumindest Schnittstellen)

Sie können eine Menge sehr nützlicher Arbeit ohne Implementierungsvererbung oder Mixins leisten , aber ich frage mich, ob Sie eine Art Schnittstellenvererbung haben sollten, dh eine Deklaration, die besagt, wenn ein Objekt die Schnittstelle A implementiert, muss es auch die Schnittstelle B (dh) A ist eine Spezialisierung von B und daher gibt es eine Typbeziehung. Auf der anderen Seite muss Ihr resultierendes Objekt nur aufzeichnen, dass es beide Schnittstellen implementiert, sodass es dort nicht zu komplex ist. Alles perfekt machbar.

Das Fehlen einer Vererbung der Implementierung hat jedoch einen klaren Nachteil: Sie können keine numerisch indizierten vtables für Ihre Klassen erstellen und müssen daher für jeden Methodenaufruf Hash-Lookups durchführen (oder einen cleveren Weg finden, um sie zu vermeiden). Dies kann schmerzhaft sein, wenn Sie selbst grundlegende Werte (z. B. Zahlen) durch diesen Mechanismus leiten. Selbst eine sehr gute Hash-Implementierung kann teuer sein, wenn Sie sie in jeder inneren Schleife mehrmals treffen!

Donal Fellows
quelle