Terminología: me referiré al lenguaje constructivo interface
como interfaz , ya la interfaz de un tipo u objeto como superficie (por falta de un mejor término).
El acoplamiento suelto se puede lograr haciendo que un objeto dependa de una abstracción en lugar de un tipo concreto.
Correcto.
Esto permite un acoplamiento suelto por dos razones principales: 1 : las abstracciones tienen menos probabilidades de cambiar que los tipos concretos, lo que significa que es menos probable que el código dependiente se rompa. 2 : se pueden utilizar diferentes tipos concretos en el tiempo de ejecución, porque todos se ajustan a la abstracción. También se pueden agregar nuevos tipos concretos 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 específicos de cosas generales es abstracción. Esto generalmente se hace mediante alguna capa de abstracción . Esta capa se puede cambiar a otros específicos sin romper el código que se basa en esta abstracción: se logra un acoplamiento suelto. Ejemplo no OOP: una rutina sort
puede 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 abstracción sort
) se desacopla del real implementación de clasificación.
Lo que he denominado superficie anterior es la parte general de una abstracción. Ahora sucede en OOP que un objeto a veces debe admitir múltiples abstracciones. Un ejemplo no muy óptimo: el java.util.LinkedList
de Java admite tanto la interfaz List
que trata sobre la abstracción de "colección ordenada e indexable", como la interfaz Queue
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 puede definirse como una clase abstracta (es decir, una clase que no puede ser instanciada inmediatamente) que declara, pero no define métodos virtuales. Las clases que implementan las características específicas de una abstracción pueden heredar de esa clase abstracta e implementar los métodos virtuales necesarios.
El problema aquí es que la herencia múltiple puede llevar al problema de diamante , donde el orden en que se buscan las clases para una implementación de método (MRO: orden de resolución de método) puede llevar a "contradicciones". Hay dos respuestas a esto:
-
Defina una orden sana y rechace aquellas órdenes que no pueden ser linealizadas con sensatez. El C3 MRO es bastante sensato y funciona bien. Fue publicado en 1996.
-
Tome la ruta fácil y rechace la herencia múltiple en todo.
Java tomó la última opción y eligió la herencia de comportamiento único. Sin embargo, todavía necesitamos la capacidad de un objeto para admitir múltiples abstracciones. Por lo tanto, se deben usar interfaces que no admitan definiciones de métodos, solo declaraciones.
El resultado es que el MRO es obvio (solo mire cada superclase en orden), y que nuestro objeto puede tener múltiples superficies para cualquier número de abstracciones.
Esto resulta ser bastante insatisfactorio, porque a menudo un poco de comportamiento es parte de la superficie. Considere una interfaz Comparable
:
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 API agradable con muchos métodos convenientes), pero tedioso de implementar. Nos gustaría que la interfaz solo incluya cmp
, e implemente los otros métodos automáticamente en términos de ese método requerido. Mixins , pero lo más importante es los rasgos [ 1 ], [ 2 ] resuelve 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 realmente participando en la MRO, en lugar de eso, los métodos definidos se componen de la clase implementadora.
La interfaz Comparable
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 la 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
}
Entonces, Inty(4) cmp Inty(6)
sería -2
y Inty(4) lt Inty(6)
sería true
.
Muchos idiomas tienen algún soporte para los rasgos, y cualquier idioma que tenga un "Protocolo de Metaobjeto (MOP)" puede tener rasgos agregados. La actualización reciente de Java 8 agregó métodos predeterminados que son similares a los rasgos (los métodos en las interfaces pueden tener implementaciones alternativas, por lo que es opcional implementar clases para implementar estos métodos).
Desafortunadamente, los rasgos son un invento bastante reciente (2002) y, por lo tanto, son bastante raros en los idiomas generales más grandes.