Correction de l'héritage?

Tout d'abord, il y a eu une longue introduction sur la façon dont j'ai eu une idée brillante (blague, ce sont des mixins en TS / JS et Policy en C ++ ), à laquelle l'article est consacré. Je ne vais pas perdre votre temps, voici le héros de la fête d'aujourd'hui (attentivement, 5 lignes en JS):



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


Laissez-moi vous expliquer comment cela fonctionne. Au lieu de l'héritage régulier, nous utilisons le mécanisme ci-dessus. Ensuite, nous spécifions la classe de base uniquement lors de la création d'un objet:



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


J'essaierai de vous convaincre que c'est le fils de l'ami de ma mère pour l'héritage de classe et un moyen de rendre l'héritage au titre d'un véritable outil POO (juste après l'héritage prototypique, bien sûr).



Presque pas hors sujet
, , , pet project , pet project'. , .





Mettons-nous d'accord sur les noms: j'appellerai cette technique un mixin, bien que cela signifie encore un peu différent . Avant que l'on me dise qu'il s'agit de mixins de TS / JS, j'ai utilisé le nom LBC (classes à liaison tardive).







Les "problèmes" de l'héritage de classe



Nous savons tous comment «tout le monde» «déteste» l'héritage de classe. Quels sont ses problèmes? Découvrons-le et comprenons en même temps comment les mixins les résolvent.



L'héritage d'implémentation interrompt l'encapsulation



La tâche principale de la POO est de lier les données et les opérations sur celle-ci (encapsulation). Lorsqu'une classe hérite d'une autre, cette relation est rompue: les données sont à un endroit (parent), les opérations - dans un autre (héritier). De plus, l'héritier peut surcharger l'interface publique de la classe, de sorte que ni le code de la classe de base, ni le code de la classe héritée séparément ne peuvent dire ce qu'il adviendra de l'état de l'objet. Autrement dit, les classes sont couplées.



Les mixins, à leur tour, réduisent considérablement le couplage: du comportement de quelle classe de base doit dépendre l'héritier, s'il n'y a simplement pas de classe de base au moment de la déclaration de la classe héritière? Cependant, grâce à cette liaison tardive et à la surcharge de méthode, le "problème Yo-yo"restes. Si vous utilisez l'héritage dans votre conception, il ne peut pas y échapper, mais, par exemple, dans des mots open- clés Kotlin et overridedevrait grandement faciliter la situation (je ne sais pas, je ne connais pas trop Kotlin).



Hériter de méthodes inutiles



Un exemple classique avec une liste et une pile: si vous héritez de la pile d'une liste, les méthodes de l'interface de liste entreront dans l'interface de la pile, ce qui peut violer l'invariant de pile. Je ne dirais pas que c'est un problème d'héritage, car, par exemple, en C ++, il y a un héritage privé pour cela (et des méthodes individuelles peuvent être rendues publiques en utilisant using), donc c'est plutôt un problème de langues individuelles.



Manque de flexibilité



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




Si une classe hérite d'une implémentation d'une autre classe, la modification de cette implémentation peut interrompre la classe héritière. Dans cet article, il y a une très bonne illustration des problèmes Stacket MonitorableStack.



Avec les mixins, le programmeur doit tenir compte du fait que la classe héritière qu'il écrit doit fonctionner non seulement avec une classe de base spécifique, mais aussi avec d'autres classes qui correspondent à l'interface de la classe de base.



Banane, gorille et jungle



La POO promet la composabilité, c'est-à-dire la possibilité de réutiliser des classes individuelles dans différentes situations et même dans différents projets. Cependant, si une classe hérite d'une autre classe, afin de réutiliser l'héritier, vous devez copier toutes les dépendances, la classe de base et toutes ses dépendances, ainsi que sa classe de base…. Ceux. voulait une banane, et a sorti un gorille, puis la jungle. Si l'objet a été créé avec le principe d'inversion de dépendance à l'esprit, les dépendances ne sont pas si mauvaises - copiez simplement leurs interfaces. Cependant, cela ne peut pas être fait avec la chaîne d'héritage.



Les mixins, à leur tour, permettent (et obligatoire) d'utiliser les DIP en relation avec l'héritage.



Autres équipements des Mixins



Les avantages des mixins ne s'arrêtent pas là. Voyons ce que vous pouvez faire d'autre avec eux.



Mort de la hiérarchie d'héritage



Les classes ne dépendent plus les unes des autres: elles ne dépendent que des interfaces. Ceux. l'implémentation devient les feuilles du graphe de dépendances. Cela devrait faciliter le refactoring - maintenant le modèle de domaine n'est plus lié à son implémentation.



Mort des classes abstraites



Les classes abstraites ne sont plus nécessaires. Regardons un exemple du modèle de méthode d'usine en Java emprunté au gourou du refactoring :



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

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

    abstract Button createButton();
}


Oui, bien sûr, les méthodes d'usine évoluent en modèles de constructeur et de stratégie. Mais vous pouvez le faire avec des mixins (imaginons un instant que Java a des mixins de première classe):



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

interface ButtonFactory {
    Button createButton();
}

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


Vous pouvez faire cette astuce avec presque toutes les classes abstraites. Un exemple où cela ne fonctionne pas:



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();
    }
}


Ici, le champ est encapsulatednécessaire à la fois en surcharge methodet en implémentation abstractMethod. Autrement dit, sans casser l'encapsulation, la classe Concretene peut pas être divisée en un enfant Abstractet une "superclasse" Abstract. Mais je ne suis pas sûr que ce soit un exemple de bonne conception.



Flexibilité comparable aux types



Le lecteur attentif remarquera que ceux-ci sont tous très similaires aux traits Smalltalk / Rust. Il existe deux différences:



  1. Les instances Mixin peuvent contenir des données qui n'étaient pas dans la classe de base;
  2. Les mixins ne modifient pas la classe dont ils héritent: pour utiliser la fonctionnalité du mixin, vous devez créer explicitement l'objet mixin, pas la classe de base.


La deuxième différence conduit au fait que, disons, les mixins agissent localement, contrairement aux traits qui agissent sur toutes les instances de la classe de base. La commodité dépend du programmeur et du projet, je ne dirai pas que ma solution est définitivement meilleure.



Ces différences rapprochent les mixins de l'héritage normal, donc cette chose me semble être un drôle de compromis entre l'héritage et les traits.



Inconvénients des mixins



Oh, si seulement c'était aussi simple que ça. Les mixins ont définitivement un petit problème et un gros moins.



Interfaces explosives



Si vous ne pouvez hériter que de l'interface, il y aura évidemment plus d'interfaces dans le projet. Bien sûr, si le DIP est respecté dans le projet, quelques interfaces supplémentaires ne feront pas la météo, mais toutes ne suivront pas SOLID. Ce problème peut être résolu si, sur la base de chaque classe, une interface contenant toutes les méthodes publiques est générée, et en mentionnant le nom de la classe, distinguer si la classe est conçue comme une fabrique d'objets ou comme une interface. Quelque chose de similaire est fait dans TypeScript, mais pour une raison quelconque, les champs et méthodes privés sont mentionnés dans l'interface générée.



Constructeurs complexes



Si vous utilisez des mixins, la tâche la plus difficile est de créer un objet. Considérez deux options selon que le constructeur est inclus dans l'interface de classe de base:



  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. En fait, il existe une autre option: passer au constructeur non pas une liste d'arguments, mais un dictionnaire. Cela résout le problème ci-dessus, car il { values: Array<int>, mode: Mode }correspond évidemment au modèle { values: Array<int> }, mais cela conduit à une collision imprévisible de noms dans un tel dictionnaire: par exemple, la superclasse Aet l'héritier Butilisent les mêmes paramètres, mais ce nom n'est pas spécifié dans l'interface de la classe de base pour B.


Au lieu d'une conclusion



Je suis sûr que j'ai raté certains aspects de cette idée. Ou le fait qu'il s'agit déjà d'un accordéon à boutons sauvages et qu'il y a vingt ans, il y avait un langage utilisant cette idée. En tout cas, je vous attends dans les commentaires!



Liste des sources



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