Terminología: me referiré a la construcción del lenguaje interfacecomo interfaz , y a la interfaz de un tipo u objeto como superficie (a falta de un término mejor).
El acoplamiento flojo se puede lograr haciendo que un objeto dependa de una abstracción en lugar de un tipo concreto.
Correcto.
Esto permite un acoplamiento flojo por dos razones principales: 1 : es menos probable que las abstracciones cambien que los tipos concretos, lo que significa que es menos probable que el código dependiente se rompa. 2 : se pueden usar diferentes tipos de concreto en tiempo de ejecución, porque todos se ajustan a la abstracción. También se pueden agregar nuevos tipos de concreto más adelante sin necesidad de alterar el código dependiente existente.
No del todo correcto. Los lenguajes actuales generalmente no anticipan que una abstracción cambiará (aunque hay algunos patrones de diseño para manejar eso). Separar detalles de cosas generales es abstracción. Esto generalmente se hace mediante una capa de abstracción . Esta capa se puede cambiar a otros detalles sin romper el código que se basa en esta abstracción: se logra un acoplamiento flexible. Ejemplo que no es OOP: una sortrutina podría cambiarse de Quicksort en la versión 1 a Tim Sort en la versión 2. El código que solo depende del resultado que se está ordenando (es decir, se basa en la sortabstracción) se desacopla de la implementación de clasificación real.
Lo que llamé superficie arriba es la parte general de una abstracción. Ahora sucede en OOP que un objeto a veces debe soportar múltiples abstracciones. Un ejemplo no bastante óptimo: Java java.util.LinkedListadmite tanto la Listinterfaz que trata sobre la abstracción de "colección ordenada e indexable", como la Queueinterfaz que (en términos generales) trata sobre la abstracción "FIFO".
¿Cómo puede un objeto soportar múltiples abstracciones?
C ++ no tiene interfaces, pero tiene herencia múltiple, métodos virtuales y clases abstractas. Una abstracción se puede definir como una clase abstracta (es decir, una clase que no se puede instanciar de inmediato) que declara, pero no define métodos virtuales. Las clases que implementan los detalles de una abstracción pueden heredar de esa clase abstracta e implementar los métodos virtuales requeridos.
El problema aquí es que la herencia múltiple puede conducir al problema del diamante , donde el orden en el que se buscan las clases para la implementación de un método (MRO: orden de resolución del método) puede conducir a "contradicciones". Hay dos respuestas a esto:
Defina un orden sensato y rechace aquellos que no pueden ser linealmente sensatos. El C3 MRO es bastante sensible y funciona bien. Fue publicado en 1996.
Tome la ruta fácil y rechace la herencia múltiple en todo momento.
Java tomó la última opción y eligió la herencia de comportamiento único. Sin embargo, todavía necesitamos la capacidad de un objeto para soportar múltiples abstracciones. Por lo tanto, deben usarse interfaces que no admiten definiciones de métodos, solo declaraciones.
El resultado es que el MRO es obvio (solo mira cada superclase en orden), y que nuestro objeto puede tener múltiples superficies para cualquier cantidad de abstracciones.
Esto resulta ser bastante insatisfactorio, porque a menudo un poco de comportamiento es parte de la superficie. Considere una Comparableinterfaz:
interface Comparable<T> {
public int cmp(T that);
public boolean lt(T that); // less than
public boolean le(T that); // less than or equal
public boolean eq(T that); // equal
public boolean ne(T that); // not equal
public boolean ge(T that); // greater than or equal
public boolean gt(T that); // greater than
}
Esto es muy fácil de usar (una buena API con muchos métodos convenientes), pero es tedioso de implementar. Nos gustaría que la interfaz solo incluya cmpe implemente los otros métodos automáticamente en términos de ese método requerido. Los mixins , pero más importante, los rasgos [ 1 ], [ 2 ] resuelven este problema sin caer en las trampas de la herencia múltiple.
Esto se hace definiendo una composición de rasgos para que los rasgos no terminen participando en el MRO; en cambio, los métodos definidos se componen en la clase de implementación.
La Comparableinterfaz podría expresarse en Scala como
trait Comparable[T] {
def cmp(that: T): Int
def lt(that: T): Boolean = this.cmp(that) < 0
def le(that: T): Boolean = this.cmp(that) <= 0
...
}
Cuando una clase usa ese rasgo, los otros métodos se agregan a la definición de clase:
// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
override def cmp(that: Inty) = this.x - that.x
// lt etc. get added automatically
}
Así Inty(4) cmp Inty(6)sería -2y Inty(4) lt Inty(6)sería true.
Muchos idiomas tienen algún soporte para rasgos, y cualquier lenguaje que tenga un "Protocolo de Metaobjetos (MOP)" puede tener rasgos agregados. La reciente actualización de Java 8 agregó métodos predeterminados que son similares a los rasgos (los métodos en las interfaces pueden tener implementaciones alternativas para que sea opcional implementar clases para implementar estos métodos).
Desafortunadamente, los rasgos son una invención bastante reciente (2002) y, por lo tanto, son bastante raros en los idiomas principales más grandes.