Vererbung reparieren?

Zuerst gab es eine lange Einführung darüber, wie ich auf eine brillante Idee gekommen bin (Witz, das sind Mixins in TS / JS und Policy in C ++ ), um die es in diesem Artikel geht. Ich werde Ihre Zeit nicht verschwenden, hier ist der Held der heutigen Feier (sorgfältig 5 Zeilen in JS):



function Extends(clazz) {
    return class extends clazz {
        // ...
    }
}


Lassen Sie mich erklären, wie es funktioniert. Anstelle der regulären Vererbung verwenden wir den obigen Mechanismus. Dann geben wir die Basisklasse nur beim Erstellen eines Objekts an:



const Class = Extends(Base)
const object = new Class(...args)


Ich werde versuchen, Sie davon zu überzeugen, dass dies der Sohn des Freundes meiner Mutter für die Klassenvererbung ist und eine Möglichkeit, die Vererbung auf den Titel des True-OOP-Tools zurückzuführen (natürlich direkt nach der prototypischen Vererbung).



Fast nicht offtopisch
, , , pet project , pet project'. , .





Lassen Sie uns die Namen vereinbaren: Ich werde diese Technik als Mixin bezeichnen, obwohl dies immer noch etwas anders bedeutet . Bevor mir gesagt wurde, dass dies Mixins von TS / JS sind, habe ich den Namen LBC (Late-Bound Classes) verwendet.







Die "Probleme" der Klassenvererbung



Wir alle wissen, wie "jeder" die Klassenvererbung nicht mag. Was sind seine Probleme? Lassen Sie es uns herausfinden und gleichzeitig verstehen, wie Mixins sie lösen.



Durch die Vererbung der Implementierung wird die Kapselung unterbrochen



Die Hauptaufgabe von OOP besteht darin, Daten und Operationen darauf zusammenzubinden (Kapselung). Wenn eine Klasse von einer anderen erbt, wird diese Beziehung unterbrochen: Daten befinden sich an einer Stelle (übergeordnet), Operationen an einer anderen (erben). Darüber hinaus kann der Erben die öffentliche Schnittstelle der Klasse überlasten, sodass weder der Code der Basisklasse noch der Code der ererbenden Klasse separat angeben können, was mit dem Status des Objekts geschehen wird. Das heißt, die Klassen sind gekoppelt.



Mixins wiederum reduzieren die Kopplung erheblich: Von welchem ​​Verhalten sollte der Erben abhängig sein, wenn zum Zeitpunkt der Deklaration der ererbenden Klasse einfach keine Basisklasse vorhanden ist? Dank der verspäteten Überlastung und der Methodenüberladung ist das "Yo-Yo-Problem" jedochÜberreste. Wenn Sie Vererbung in Ihrem Design verwenden, kann es nicht entkommen, aber zum Beispiel in Kotlin-Schlüsselwörtern openund overridesollte die Situation erheblich erleichtern (ich weiß nicht, nicht zu eng mit Kotlin vertraut).



Unnötige Methoden erben



Ein klassisches Beispiel mit einer Liste und einem Stapel: Wenn Sie den Stapel von einer Liste erben, gelangen Methoden von der Listenschnittstelle in die Stapelschnittstelle, wodurch die Stapelinvariante verletzt werden kann. Ich würde nicht sagen, dass dies ein Vererbungsproblem ist, da es beispielsweise in C ++ eine private Vererbung dafür gibt (und einzelne Methoden mit veröffentlicht werden können using), so dass dies eher ein Problem einzelner Sprachen ist.



Mangel an Flexibilität



  1. , : . , , : , , cohesion . , .
  2. ( ), . , : , .
  3. . - , . , , ? - — , .. .
  4. . - , - . , , .




Wenn eine Klasse von der Implementierung einer anderen Klasse erbt, kann das Ändern dieser Implementierung die erbende Klasse beschädigen. In diesem Artikel gibt es eine sehr gute Illustration der Probleme Stackund MonitorableStack.



Bei Mixins muss der Programmierer berücksichtigen, dass die von ihm geschriebene erbende Klasse nicht nur mit einer bestimmten Basisklasse, sondern auch mit anderen Klassen funktionieren muss, die der Schnittstelle der Basisklasse entsprechen.



Banane, Gorilla und Dschungel



OOP verspricht Zusammensetzbarkeit, d.h. die Fähigkeit, einzelne Klassen in verschiedenen Situationen und sogar in verschiedenen Projekten wiederzuverwenden. Wenn eine Klasse jedoch von einer anderen Klasse erbt, müssen Sie alle Abhängigkeiten, die Basisklasse und alle ihre Abhängigkeiten sowie ihre Basisklasse kopieren, um den Erben wiederzuverwenden. Jene. wollte eine Banane und holte einen Gorilla und dann einen Dschungel heraus. Wenn das Objekt unter Berücksichtigung des Abhängigkeitsinversionsprinzips erstellt wurde, sind die Abhängigkeiten nicht so schlecht - kopieren Sie einfach ihre Schnittstellen. Dies ist jedoch mit der Vererbungskette nicht möglich.



Mixins wiederum ermöglichen (und sind obligatorisch) die Verwendung von DIPs in Bezug auf die Vererbung.



Weitere Annehmlichkeiten der Mixins



Die Vorteile von Mixins enden hier nicht. Mal sehen, was Sie sonst noch mit ihnen machen können.



Tod der Vererbungshierarchie



Klassen hängen nicht mehr voneinander ab: Sie hängen nur noch von Schnittstellen ab. Jene. Die Implementierung wird zum Blatt des Abhängigkeitsgraphen. Dies sollte das Refactoring vereinfachen - jetzt ist das Domänenmodell nicht mehr an seine Implementierung gebunden.



Tod abstrakter Klassen



Abstrakte Klassen werden nicht mehr benötigt. Schauen wir uns ein Beispiel für das Factory-Methodenmuster in Java an, das vom Refactoring-Guru entlehnt wurde :



interface Button {
    void render();
    void onClick();
}

abstract class Dialog {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }

    abstract Button createButton();
}


Ja, natürlich entwickeln sich Factory-Methoden zu Builder- und Strategiemustern. Aber Sie können dies mit Mixins tun (stellen wir uns für eine Sekunde vor, dass Java erstklassige Mixins hat):



interface Button {
    void render();
    void onClick();
}

interface ButtonFactory {
    Button createButton();
}

class Dialog extends ButtonFactory {
    void renderWindow() {
        Button okButton = createButton();
        okButton.render();
    }
}


Sie können diesen Trick mit fast jeder abstrakten Klasse ausführen. Ein Beispiel, wo es nicht funktioniert:



abstract class Abstract {
    void method() {
        abstractMethod();
    }

    abstract void abstractMethod();
}

class Concrete extends Abstract {
    private encapsulated = new Encapsulated();

    @Override
    void method() {
        encapsulated.method();
        super.method();
    }

    void abstractMethod() {
        encapsulated.otherMethod();
    }
}


Hier wird das Feld encapsulatedsowohl bei Überlastung methodals auch bei Implementierung benötigt abstractMethod. Das heißt, ohne die Kapselung zu unterbrechen, kann die Klasse Concretenicht in ein Kind Abstractund eine "Oberklasse" unterteilt werden Abstract. Ich bin mir aber nicht sicher, ob dies ein Beispiel für gutes Design ist.



Flexibilität vergleichbar mit Typen



Der aufmerksame Leser wird feststellen, dass diese Merkmale den Smalltalk / Rust-Merkmalen sehr ähnlich sind. Es gibt zwei Unterschiede:



  1. Mixin-Instanzen können Daten enthalten, die nicht zur Basisklasse gehören.
  2. Mixins ändern nicht die Klasse, von der sie erben: Um die Funktionalität des Mixins nutzen zu können, müssen Sie das Mixin-Objekt explizit erstellen, nicht die Basisklasse.


Der zweite Unterschied führt dazu, dass Mixins beispielsweise lokal wirken, im Gegensatz zu Merkmalen, die auf alle Instanzen der Basisklasse wirken. Wie bequem es ist, hängt vom Programmierer und vom Projekt ab. Ich werde nicht sagen, dass meine Lösung definitiv besser ist.



Diese Unterschiede bringen Mixins näher an die normale Vererbung heran, daher scheint mir diese Sache ein lustiger Kompromiss zwischen Vererbung und Merkmalen zu sein.



Nachteile von Mixins



Oh, wenn es nur so einfach wäre. Mixins haben definitiv ein kleines Problem und ein Fettminus.



Explodierende Schnittstellen



Wenn Sie nur von der Schnittstelle erben können, gibt es natürlich mehr Schnittstellen im Projekt. Wenn das DIP im Projekt eingehalten wird, sind natürlich einige weitere Schnittstellen nicht wetterbeständig, aber nicht alle folgen SOLID. Dieses Problem kann gelöst werden, wenn basierend auf jeder Klasse eine Schnittstelle generiert wird, die alle öffentlichen Methoden enthält, und wenn der Klassenname erwähnt wird, unterschieden wird, ob die Klasse als Objektfabrik oder als Schnittstelle gedacht ist. Ähnliches geschieht in TypeScript, aber aus irgendeinem Grund werden private Felder und Methoden in der generierten Schnittstelle erwähnt.



Komplexe Konstruktoren



Mit Mixins ist es am schwierigsten, ein Objekt zu erstellen. Berücksichtigen Sie zwei Optionen, je nachdem, ob der Konstruktor in der Basisklassenschnittstelle enthalten ist:



  1. , , . , - . , .
  2. , . :



    interface Base {
        new(values: Array<int>)
    }
    
    class Subclass extends Base {
        // ...
    }
    
    class DoesntFit {
        new(values: Array<int>, mode: Mode) {
            // ...
        }
    }
    


    DoesntFit Subclass, - . Subclass DoesntFit, Base .
  3. Tatsächlich gibt es eine andere Möglichkeit - dem Konstruktor keine Liste von Argumenten, sondern ein Wörterbuch zu übergeben. Dies löst das obige Problem, da es { values: Array<int>, mode: Mode }offensichtlich mit dem Muster übereinstimmt { values: Array<int> }, aber zu einer unvorhersehbaren Kollision von Namen in einem solchen Wörterbuch führt: Beispielsweise verwenden sowohl die Oberklasse Aals auch der Erben Bdieselben Parameter, aber dieser Name wird in der Schnittstelle der Basisklasse für nicht angegeben B.


Anstelle einer Schlussfolgerung



Ich bin sicher, ich habe einige Aspekte dieser Idee übersehen. Oder die Tatsache, dass dies bereits ein wildes Knopfakkordeon ist und es vor zwanzig Jahren eine Sprache gab, die diese Idee verwendete. Auf jeden Fall warte ich in den Kommentaren auf dich!



Liste der Quellen



neethack.com/2017/04/Why-inheritance-is-bad

www.infoworld.com/article/2073649/why-extends-is-evil.html

www.yegor256.com/2016/09/13/inheritance-is- procedural.html

refactoring.guru/ru/design-patterns/factory-method/java/example

scg.unibe.ch/archive/papers/Scha03aTraits.pdf



All Articles