El problema de la clase base frágil es un problema arquitectónico fundamental de los sistemas de programación orientados a objetos , en los que las clases base ( superclases ) se consideran "frágiles" porque las modificaciones aparentemente seguras de una clase base, cuando son heredadas por las clases derivadas , pueden provocar que estas funcionen mal. El programador no puede determinar si un cambio en la clase base es seguro simplemente examinando de forma aislada los métodos de la clase base.
Una posible solución es hacer que las variables de instancia sean privadas para la clase que las define y obligar a las subclases a usar métodos de acceso para modificar los estados de las superclases. Un lenguaje también podría hacer que las subclases puedan controlar qué métodos heredados se exponen públicamente. Estos cambios evitan que las subclases dependan de los detalles de implementación de las superclases y permiten que las subclases expongan solo aquellos métodos de las superclases que sean aplicables a ellas mismas.
Una solución alternativa es tener una interfaz en lugar de una superclase.
El problema de la clase base frágil se ha atribuido a la recursión abierta (envío dinámico de métodos en this), con la sugerencia de que la invocación de métodos en thispor defecto a la recursión cerrada (envío estático, enlace temprano) en lugar de la recursión abierta (envío dinámico, enlace tardío), solo usando la recursión abierta cuando se solicita específicamente; las llamadas externas (sin usar this) se enviarían dinámicamente como de costumbre. [1] [2]
Ejemplo de Java
El siguiente ejemplo trivial está escrito en el lenguaje de programación Java y muestra cómo una modificación aparentemente segura de una clase base puede provocar que una subclase heredada funcione mal al ingresar en una recursión infinita que dará como resultado un desbordamiento de pila .
clase Super { int privado contador = 0 ;
void inc1 () { contador ++ ; }
void inc2 () { contador ++ ; } }
clase Sub extiende Super { @Override void inc2 () { inc1 (); } }
Al llamar al método inc2() enlazado dinámicamente en una instancia de Sub, se incrementará correctamente el contador de campo en uno. Sin embargo, si se cambia el código de la superclase de la siguiente manera:
clase Super {
int privado contador = 0 ;
vacío inc1 () { inc2 (); }
void inc2 () { contador ++ ; } }
Una llamada al método inc2() enlazado dinámicamente en una instancia de Sub provocará una recursión infinita entre sí y el método inc1() de la superclase y, eventualmente, provocará un desbordamiento de pila. Este problema se podría haber evitado declarando los métodos en la superclase como final , lo que haría imposible que una subclase los anule. Sin embargo, esto no siempre es deseable o posible. Por lo tanto, es una buena práctica que las superclases eviten cambiar las llamadas a métodos enlazados dinámicamente.
Soluciones
- Objective-C tiene categorías así como variables de instancia no frágiles .
- El componente Pascal deja obsoletas las llamadas de superclase .
- Java , C++ (desde C++11) y D permiten prohibir la herencia o la anulación de un método de clase etiquetando la declaración de una clase o método, respectivamente, con la palabra clave "
final". En el libro Effective Java , el autor Joshua Bloch escribe (en el punto 17) que los programadores deberían "Diseñar y documentar la herencia o, de lo contrario, prohibirla". - C# y VB.NET, como Java, tienen palabras claves de declaración de clase "
sealed" y "Not Inheritable" para prohibir la herencia y requieren que una subclase use la palabra clave "override" para anular métodos, [3] la misma solución adoptada posteriormente por Scala. - Scala requiere que una subclase use la palabra clave "
override" explícitamente para anular un método de la clase padre. En el libro "Programación en Scala, 2.ª edición", el autor escribe que (con modificaciones aquí) si no hubiera un método f(), la implementación original del método f() del cliente no podría haber tenido un modificador de anulación. Una vez que agregue el método f() a la segunda versión de su clase de biblioteca, una recompilación del código del cliente arrojaría un error de compilación en lugar de un comportamiento incorrecto. - En Kotlin, las clases y los métodos son finales de forma predeterminada. Para habilitar la herencia de clases, la clase debe estar marcada con el
openmodificador. Del mismo modo, un método debe estar marcado comoopenpara permitir la anulación del método. - Julia solo permite la subtipificación de tipos abstractos y utiliza la composición como alternativa a la herencia . Sin embargo, tiene múltiples envíos .
Véase también
- Problema de interfaz binaria frágil
- Herencia de implementación
- Semántica de la herencia
- Fragilidad del software
- Herencia virtual
Referencias
- ^ "Recursión abierta selectiva: una solución al problema de la clase base frágil", Jonathan Aldrich
- ^ "Recursión abierta selectiva: una solución al problema de la clase base frágil", Lambda the Ultimate
- ^ "Modificador de anulación - Referencia de C#".
Enlaces externos
- Mikhajlov, Leonid; Sekerinski, Emil (1998). "ECOOP'98 — Programación orientada a objetos" (PDF) . ECOOP'98 — Programación orientada a objetos . ECOOP 1998. LCNS . Vol. 1445. págs. 355–382. doi :10.1007/BFb0054099. ISBN 978-3-540-64737-9. ISSN 0302-9743. QID 29543920. Consultado el 21 de julio de 2020 .
- Holub, Allen (1 de agosto de 2003). "Por qué las extensiones son malas". Caja de herramientas de Java. JavaWorld . Consultado el 21 de julio de 2020 .