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
open
y override
deberí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
- , : . , , : , , cohesion . , .
- ( ), . , : , .
- . - , . , , ? - — , .. .
- . - , - . , , .
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
Stack
y 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
encapsulated
necesario tanto en sobrecarga method
como en implementación abstractMethod
. Es decir, sin romper la encapsulación, la clase Concrete
no se puede dividir en un hijo Abstract
y 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:
- Las instancias de Mixin pueden contener datos que no estaban en la clase base;
- 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:
- , , . , - . , .
- , . :
interface Base { new(values: Array<int>) } class Subclass extends Base { // ... } class DoesntFit { new(values: Array<int>, mode: Mode) { // ... } }
DoesntFit
Subclass
, - .Subclass
DoesntFit
,Base
. - 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 superclaseA
como el herederoB
usan los mismos parámetros, pero este nombre no se especifica en la interfaz de la clase base paraB
.
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