La composición preferida no se trata solo de polimorfismo. Aunque eso es parte de esto, y tiene razón en que (al menos en lenguajes de escritura nominal) lo que la gente realmente quiere decir es "preferir una combinación de composición e implementación de interfaz". Pero, las razones para preferir la composición (en muchas circunstancias) son profundas.
El polimorfismo se trata de una cosa que se comporta de múltiples maneras. Por lo tanto, los genéricos / plantillas son una característica "polimórfica" en la medida en que permiten que una sola pieza de código varíe su comportamiento con los tipos. De hecho, este tipo de polimorfismo es realmente el mejor comportamiento y generalmente se conoce como polimorfismo paramétrico porque la variación está definida por un parámetro.
Muchos idiomas proporcionan una forma de polimorfismo llamada "sobrecarga" o polimorfismo ad hoc en el que múltiples procedimientos con el mismo nombre se definen de manera ad hoc y donde el idioma elige uno (quizás el más específico). Este es el tipo de polimorfismo menos bien comportado, ya que nada conecta el comportamiento de los dos procedimientos, excepto la convención desarrollada.
Un tercer tipo de polimorfismo es el subtipo de polimorfismo . Aquí un procedimiento definido en un tipo dado, también puede funcionar en una familia completa de "subtipos" de ese tipo. Cuando implementa una interfaz o extiende una clase, generalmente declara su intención de crear un subtipo. Los subtipos verdaderos se rigen por el Principio de sustitución de Liskov, que dice que si puede probar algo sobre todos los objetos en un supertipo, puede probarlo sobre todas las instancias en un subtipo. Sin embargo, la vida se vuelve peligrosa, ya que en lenguajes como C ++ y Java, las personas generalmente tienen suposiciones no aplicadas, y a menudo indocumentadas, sobre clases que pueden ser ciertas o no sobre sus subclases. Es decir, el código se escribe como si se pudiera demostrar más de lo que realmente es, lo que produce una gran cantidad de problemas cuando se subtipe descuidadamente.
La herencia es en realidad independiente del polimorfismo. Dada alguna cosa "T" que tiene una referencia a sí misma, la herencia ocurre cuando crea una nueva cosa "S" a partir de "T" reemplazando la referencia de "T" a sí misma con una referencia a "S". Esa definición es intencionalmente vaga, ya que la herencia puede ocurrir en muchas situaciones, pero la más común es la subclasificación de un objeto que tiene el efecto de reemplazar el this
puntero llamado por funciones virtuales con el this
puntero al subtipo.
La herencia es peligrosa, como todas las cosas muy poderosas, la herencia tiene el poder de causar estragos. Por ejemplo, suponga que anula un método cuando hereda de alguna clase: todo está bien hasta que algún otro método de esa clase asume que el método que hereda se comporta de cierta manera, después de todo, así es como lo diseñó el autor de la clase original. . Puede protegerse parcialmente contra esto declarando que todos los métodos invocados por otro de sus métodos son privados o no virtuales (final), a menos que estén diseñados para ser anulados. Aunque esto no siempre es lo suficientemente bueno. A veces puede ver algo como esto (en pseudo Java, con suerte legible para usuarios de C ++ y C #)
interface UsefulThingsInterface {
void doThings();
void doMoreThings();
}
...
class WayOfDoingUsefulThings implements UsefulThingsInterface{
private foo stuff;
public final int getStuff();
void doThings(){
//modifies stuff, such that ...
...
}
...
void doMoreThings(){
//ignores stuff
...
}
}
crees que esto es encantador y tienes tu propia forma de hacer "cosas", pero usas la herencia para adquirir la capacidad de hacer "más cosas",
class MyUsefulThings extends WayOfDoingUsefulThings{
void doThings {
//my way
}
}
Y todo está bien y bien. WayOfDoingUsefulThings
fue diseñado de tal manera que reemplazar un método no cambia la semántica de ningún otro ... excepto esperar, no, no lo fue. Parece que fue, pero doThings
cambió el estado mutable que importaba. Entonces, a pesar de que no llamó a ninguna función de anulación,
void dealWithStuff(WayOfDoingUsefulThings bar){
bar.doThings()
use(bar.getStuff());
}
ahora hace algo diferente de lo esperado cuando lo pasa a MyUsefulThings
. Lo que es peor, es posible que ni siquiera sepa que WayOfDoingUsefulThings
hizo esas promesas. Tal vez dealWithStuff
proviene de la misma biblioteca WayOfDoingUsefulThings
y getStuff()
ni siquiera es exportada por la biblioteca (piense en las clases de amigos en C ++). Peor aún, has derrotado las comprobaciones estáticas del lenguaje sin darte cuenta: dealWithStuff
tomaste una WayOfDoingUsefulThings
decisión justa para asegurarte de que tuviera una getStuff()
función que se comportara de cierta manera.
Usando composición
class MyUsefulThings implements UsefulThingsInterface{
private way = new WayOfDoingUsefulThings()
void doThings() {
//my way
}
void doMoreThings() {
this.way.doMoreThings();
}
}
trae de vuelta la seguridad de tipo estático. En general, la composición es más fácil de usar y más segura que la herencia cuando se implementa el subtipo. También le permite anular los métodos finales, lo que significa que debe sentirse libre de declarar todo final / no virtual, excepto en las interfaces la gran mayoría de las veces.
En un mundo mejor, los idiomas insertarían automáticamente el repetitivo con una delegation
palabra clave. La mayoría no, así que un inconveniente son las clases más grandes. Sin embargo, puede hacer que su IDE escriba la instancia de delegación por usted.
Ahora, la vida no se trata solo de polimorfismo. No puede necesitar subtipo todo el tiempo. El objetivo del polimorfismo es generalmente la reutilización del código, pero no es la única forma de lograr ese objetivo. A menudo, tiene sentido usar la composición, sin polimorfismo de subtipo, como una forma de administrar la funcionalidad.
Además, la herencia conductual tiene sus usos. Es una de las ideas más poderosas en informática. Es solo que, la mayoría de las veces, las buenas aplicaciones de OOP se pueden escribir utilizando solo el uso de herencia de interfaz y composiciones. Los dos principios
- Prohibir herencia o diseño para ello
- Composición preferida
son una buena guía por los motivos anteriores y no incurre en costos sustanciales.