Existen diferentes tipos de polimorfismo, el de interés suele ser el polimorfismo / despacho dinámico en tiempo de ejecución.
Una descripción de muy alto nivel del polimorfismo de tiempo de ejecución es que una llamada a un método hace cosas diferentes dependiendo del tipo de tiempo de ejecución de sus argumentos: el objeto mismo es responsable de resolver una llamada a un método. Esto permite una gran cantidad de flexibilidad.
Una de las formas más comunes de usar esta flexibilidad es para la inyección de dependencia , por ejemplo, para que pueda cambiar entre diferentes implementaciones o inyectar objetos simulados para realizar pruebas. Si sé de antemano que solo habrá un número limitado de opciones posibles, podría intentar codificarlas con condicionales, por ejemplo:
void foo() {
if (isTesting) {
... // do mock stuff
} else {
... // do normal stuff
}
}
Esto hace que el código sea difícil de seguir. La alternativa es introducir una interfaz para esa operación y escribir una implementación normal y una implementación simulada de esa interfaz, e "inyectar" a la implementación deseada en tiempo de ejecución. "Inyección de dependencia" es un término complicado para "pasar el objeto correcto como argumento".
Como ejemplo del mundo real, actualmente estoy trabajando en un tipo de problema de aprendizaje automático. Tengo un algoritmo que requiere un modelo de predicción. Pero quiero probar diferentes algoritmos de aprendizaje automático. Entonces definí una interfaz. ¿Qué necesito de mi modelo de predicción? Dada alguna muestra de entrada, la predicción y sus errores:
interface Model {
def predict(sample) -> (prediction: float, std: float);
}
Mi algoritmo toma una función de fábrica que entrena un modelo:
def my_algorithm(..., train_model: (observations) -> Model, ...) {
...
Model model = train_model(observations);
...
y, std = model.predict(x)
...
}
Ahora tengo varias implementaciones de la interfaz del modelo y puedo compararlas entre sí. Una de estas implementaciones en realidad toma otros dos modelos y los combina en un modelo impulsado. Entonces, gracias a esta interfaz:
- mi algoritmo no necesita saber sobre modelos específicos de antemano,
- Puedo cambiar fácilmente modelos, y
- Tengo mucha flexibilidad para implementar mis modelos.
Un caso de uso clásico del polimorfismo está en las GUI. En un marco GUI como Java AWT / Swing / ... hay diferentes componentes . La interfaz de componente / clase base describe acciones como pintar en la pantalla o reaccionar a los clics del mouse. Muchos componentes son contenedores que administran subcomponentes. ¿Cómo podría dibujarse tal contenedor?
void paint(Graphics g) {
super.paint(g);
for (Component child : this.subComponents)
child.paint(g);
}
Aquí, el contenedor no necesita conocer de antemano los tipos exactos de los subcomponentes; siempre que se ajusten a la Component
interfaz, el contenedor simplemente puede llamar al polimórficopaint()
método . Esto me da la libertad de extender la jerarquía de clases AWT con nuevos componentes arbitrarios.
Hay muchos problemas recurrentes a lo largo del desarrollo de software que se pueden resolver aplicando el polimorfismo como técnica. Estos pares recurrentes de problemas y soluciones se denominan patrones de diseño , y algunos de ellos se recopilan en el libro del mismo nombre. En los términos de ese libro, mi modelo de aprendizaje automático inyectado sería una estrategia que utilizo para "definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables". El ejemplo de Java-AWT donde un componente puede contener subcomponentes es un ejemplo de un compuesto .
Pero no todos los diseños necesitan usar polimorfismo (más allá de habilitar la inyección de dependencia para las pruebas unitarias, que es un caso de uso realmente bueno). La mayoría de los problemas son muy estáticos. Como consecuencia, las clases y los métodos a menudo no se usan para el polimorfismo, sino simplemente como espacios de nombres convenientes y para la sintaxis de llamada del método bonito. Por ejemplo, muchos desarrolladores prefieren llamadas a métodos como account.getBalance()
sobre una llamada de función en gran medida equivalente Account_getBalance(account)
. Ese es un enfoque perfectamente bueno, es solo que muchas llamadas de "método" no tienen nada que ver con el polimorfismo.