¿Arreglando la herencia?

Primero, hubo una larga introducción sobre cómo se me ocurrió una idea brillante (broma, estos son mixins en TS / JS y Policy en C ++ ), a la que está dedicado el artículo. No voy a perder su tiempo, aquí está el héroe de la celebración de hoy (con cuidado, 5 líneas en JS):



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


Déjame explicarte cómo funciona. En lugar de la herencia regular, usamos el mecanismo anterior. Luego especificamos la clase base solo al crear un objeto:



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


Trataré de convencerlos de que este es el hijo de la amiga de mi madre para la herencia de clases y una forma de devolver la herencia al título de verdadera herramienta OOP (justo después de la herencia prototípica, por supuesto).



Casi no fuera de tema
, , , pet project , pet project'. , .





Acordemos los nombres: llamaré a esta técnica un mixin, aunque esto todavía significa un poco diferente . Antes de que me dijeran que estos son mixins de TS / JS, usé el nombre LBC (clases de enlace tardío).







Los "problemas" de la herencia de clases



Todos sabemos cómo "a todos" "no les gusta" la herencia de clases. Cuales son sus problemas? Vamos a resolverlo y al mismo tiempo entender cómo los resuelven los mixins.



La herencia de implementación rompe la encapsulación



La tarea principal de OOP es unir datos y operaciones en él (encapsulación). Cuando una clase hereda de otra, esta relación se rompe: los datos están en un lugar (padre), las operaciones, en otro (heredero). Además, el heredero puede sobrecargar la interfaz pública de la clase, de modo que ni el código de la clase base ni el código de la clase heredada por separado puedan decir qué sucederá con el estado del objeto. Es decir, las clases están acopladas.



Los mixins, a su vez, reducen en gran medida el acoplamiento: ¿en el comportamiento de qué clase base debería depender el heredero, si simplemente no hay una clase base en el momento de declarar la clase heredera? Sin embargo, gracias a este enlace en tiempo-y la sobrecarga de métodos, el "yo-yo problema"permanece. Si usa la herencia en su diseño, no puede escapar de ella, pero, por ejemplo, en las palabras clave de Kotlin openy overridedebería aliviar enormemente la situación (no lo sé, no estoy demasiado familiarizado con Kotlin).



Heredar métodos innecesarios



Un ejemplo clásico con una lista y una pila: si hereda la pila de una lista, los métodos de la interfaz de la lista entrarán en la interfaz de la pila, lo que puede violar el invariante de la pila. No diría que se trata de un problema de herencia, porque, por ejemplo, en C ++ existe una herencia privada para esto (y los métodos individuales pueden hacerse públicos usando using), por lo que este es más bien un problema de lenguajes individuales.



Falta de flexibilidad



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




Si una clase hereda de una implementación de otra clase, cambiar esa implementación puede romper la clase heredada. En este artículo hay una muy buena ilustración de los problemas Stacky MonitorableStack.



Con mixins, el programador debe tener en cuenta que la clase heredera que escribe debe trabajar no solo con alguna clase base específica, sino también con otras clases que corresponden a la interfaz de la clase base.



Plátano, gorila y selva



OOP promete componibilidad, es decir la capacidad de reutilizar clases individuales en diferentes situaciones e incluso en diferentes proyectos. Sin embargo, si una clase hereda de otra clase, para poder reutilizar el heredero, necesita copiar todas las dependencias, la clase base y todas sus dependencias, y su clase base…. Aquellos. quería un plátano, y sacó un gorila, y luego una jungla. Si el objeto fue creado con el Principio de Inversión de Dependencias en mente, las dependencias no son tan malas, simplemente copie sus interfaces. Sin embargo, esto no se puede hacer con la cadena de herencia.



Los mixins, a su vez, posibilitan (y obligan) el uso de DIP en relación a la herencia.



Otras comodidades de los Mixins



Las ventajas de los mixins no terminan ahí. Veamos qué más puedes hacer con ellos.



Muerte de la jerarquía de herencia



Las clases ya no dependen unas de otras: solo dependen de interfaces. Aquellos. la implementación se convierte en las hojas del gráfico de dependencia. Esto debería facilitar la refactorización: el modelo de dominio ahora es independiente de su implementación.



Muerte de clases abstractas



Ya no se necesitan clases abstractas. Veamos un ejemplo del patrón Factory Method en Java tomado del gurú de la refactorización :



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

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

    abstract Button createButton();
}


Sí, por supuesto, los métodos de fábrica evolucionan hacia patrones de construcción y estrategia. Pero puede hacer esto con mixins (imaginemos por un segundo que Java tiene mixins de primera clase):



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

interface ButtonFactory {
    Button createButton();
}

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


Puedes hacer este truco con casi cualquier clase abstracta. Un ejemplo en el que no funciona:



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


Aquí el campo es encapsulatednecesario tanto en sobrecarga methodcomo en implementación abstractMethod. Es decir, sin romper la encapsulación, la clase Concreteno se puede dividir en un hijo Abstracty una "superclase" Abstract. Pero no estoy seguro de si este es un ejemplo de buen diseño.



Flexibilidad comparable a los tipos



El lector atento notará que todos estos son muy similares a los rasgos de Smalltalk / Rust. Hay dos diferencias:



  1. Las instancias de Mixin pueden contener datos que no estaban en la clase base;
  2. Los mixins no modifican la clase de la que heredan: para utilizar la funcionalidad del mixin, es necesario crear explícitamente el objeto mixin, no la clase base.


La segunda diferencia lleva al hecho de que, digamos, los mixins actúan localmente, a diferencia de los rasgos que actúan en todas las instancias de la clase base. Lo conveniente que sea depende del programador y del proyecto, no diré que mi solución sea definitivamente mejor.



Estas diferencias acercan a los mixins a la herencia normal, por lo que esto me parece un compromiso divertido entre herencia y rasgos.



Contras de mixins



Oh, si tan solo fuera así de simple. Los mixins definitivamente tienen un pequeño problema y una grasa menos.



Interfaces explosivas



Si puede heredar solo de la interfaz, obviamente, habrá más interfaces en el proyecto. Por supuesto, si se respeta el DIP en el proyecto, algunas interfaces más no harán el tiempo, pero no todas siguen SOLID. Este problema se puede resolver si, en función de cada clase, se genera una interfaz que contenga todos los métodos públicos y, al mencionar el nombre de la clase, se distingue si la clase se entiende como una fábrica de objetos o como una interfaz. Algo similar se hace en TypeScript, pero por alguna razón los campos y métodos privados se mencionan en la interfaz generada.



Constructores complejos



Si usa mixins, la tarea más difícil es crear un objeto. Considere dos opciones dependiendo de si el constructor está incluido en la interfaz de la clase 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. De hecho, hay otra opción: pasar al constructor no una lista de argumentos, sino un diccionario. Esto resuelve el problema anterior, porque { values: Array<int>, mode: Mode }obviamente coincide con el patrón { values: Array<int> }, pero conduce a una colisión impredecible de nombres en dicho diccionario: por ejemplo, tanto la superclase Acomo el heredero Busan los mismos parámetros, pero este nombre no se especifica en la interfaz de la clase base para B.


En lugar de una conclusión



Estoy seguro de que me perdí algunos aspectos de esta idea. O el hecho de que esto ya es un acordeón de botones salvajes y hace veinte años había un lenguaje que usaba esta idea. En cualquier caso, ¡te espero en los comentarios!



Lista de fuentes



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