¿Por qué las interfaces son más útiles que las superclases para lograr un acoplamiento flexible?

15

( Para el propósito de esta pregunta, cuando digo 'interfaz' me refiero a la construcción del lenguajeinterface , y no a una 'interfaz' en el otro sentido de la palabra, es decir, los métodos públicos que una clase ofrece al mundo exterior para comunicarse con y manipularlo )

El acoplamiento flojo se puede lograr haciendo que un objeto dependa de una abstracción en lugar de un tipo concreto.

Esto permite un acoplamiento flojo 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. Se pueden usar 2 tipos de concreto diferentes 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.

Por ejemplo, considere una clase Cary dos subclases Volvoy Mazda.

Si su código depende de a Car, puede usar a Volvoo a Mazdadurante el tiempo de ejecución. También más adelante se podrían agregar subclases adicionales sin necesidad de cambiar el código dependiente.

Además, Carque es una abstracción, es menos probable que cambie que Volvoo Mazda. Los autos han sido generalmente los mismos durante bastante tiempo, pero Volvos y Mazdas tienen muchas más probabilidades de cambiar. Es decir, las abstracciones son más estables que los tipos concretos.

Todo esto fue para demostrar que entiendo qué es el acoplamiento flojo y cómo se logra al depender de abstracciones y no de concreciones. (Si escribí algo inexacto, por favor dígalo).

Lo que no entiendo es esto:

Las abstracciones pueden ser superclases o interfaces.

Si es así, ¿por qué se elogia específicamente a las interfaces por su capacidad de permitir un acoplamiento suelto? No veo cómo es diferente que usar una superclase.

Las únicas diferencias que veo son: 1- Las interfaces no están limitadas por una sola herencia, pero eso no tiene mucho que ver con el tema del acoplamiento flexible. 2- Las interfaces son más 'abstractas' ya que no tienen lógica de implementación en absoluto. Pero aún así, no veo por qué eso hace una gran diferencia.

Explíqueme por qué se dice que las interfaces son excelentes para permitir un acoplamiento flojo, mientras que las superclases simples no lo son.

Aviv Cohn
fuente
3
La mayoría de los lenguajes (por ejemplo, Java, C #) que tienen "interfaces" solo admiten herencia única. Como cada clase solo puede tener una superclase inmediata, las superclases (abstractas) son demasiado limitadas para que un objeto admita múltiples abstracciones. Echa un vistazo a los rasgos (por ejemplo, Scala o Roles de Perl ) para una alternativa moderna que también evita el " problema del diamante " con la herencia múltiple.
amon
@amon ¿Entonces estás diciendo que la ventaja de las interfaces sobre las clases abstractas cuando se trata de lograr un acoplamiento flexible es que no están limitadas por una sola herencia?
Aviv Cohn
No, quise decir que costoso en términos de que el compilador tiene más que ver cuando maneja una clase abstracta , pero esto podría ser descuidado.
pastoso
2
Parece que @amon está en el camino correcto, encontré esta publicación donde se dice que: interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class(lo que me lleva a la comparación con C ++, donde las interfaces son solo clases con funciones virtuales puras).
pastoso
Por favor, diga quién dice que las superclases son malas.
Tulains Córdova

Respuestas:

11

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:

  1. 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.

  2. 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.

amon
fuente
Buena respuesta, pero agregaría que los lenguajes de herencia única pueden evitar la herencia múltiple usando interfaces con composición.
4

Lo que no entiendo es esto:

Las abstracciones pueden ser superclases o interfaces.

Si es así, ¿por qué se elogia específicamente a las interfaces por su capacidad de permitir un acoplamiento suelto? No veo cómo es diferente que usar una superclase.

Primero, el subtipo y la abstracción son dos cosas diferentes. Subtipar simplemente significa que puedo sustituir los valores de un tipo por valores de otro tipo; ninguno de los tipos debe ser abstracto.

Más importante aún, las subclases dependen directamente de los detalles de implementación de su superclase. Ese es el tipo de acoplamiento más fuerte que existe. De hecho, si la clase base no está diseñada teniendo en cuenta la herencia, los cambios en la clase base que no cambian su comportamiento aún pueden romper las subclases, y no hay forma de saber a priori si se producirá una ruptura. Esto se conoce como el problema de la clase base frágil .

La implementación de una interfaz no lo acopla a nada, excepto a la interfaz en sí, que no contiene ningún comportamiento.

Doval
fuente
Gracias por responder. Para ver si entiendo: cuando desea que un objeto llamado A dependa de una abstracción llamada B en lugar de una implementación concreta de esa abstracción llamada C, a menudo es mejor que B sea una interfaz implementada por C, en lugar de una superclase extendida por C. Esto se debe a que: C la subclasificación B une firmemente C a B. Si B cambia, C cambia. Sin embargo, la implementación de C B (B es una interfaz) no combina B con C: B es solo una lista de métodos que C debe implementar, por lo tanto, no hay un acoplamiento estrecho. Sin embargo, con respecto al objeto A (el dependiente), no importa si B es una clase o interfaz.
Aviv Cohn
¿Correcto? ..... .....
Aviv Cohn
¿Por qué considerarías que una interfaz está acoplada a algo?
Michael Shaw
Creo que esta respuesta lo clava en la cabeza. Yo uso C ++ bastante, y como se dijo en una de las otras respuestas, C ++ no tiene interfaces, pero lo falsifica al usar superclases con todos los métodos que quedan como "virtuales puros" (es decir, implementados por niños). El punto es que es fácil crear clases base que HAGAN algo junto con la funcionalidad delegada. En muchos, muchos, muchos casos, yo y mis compañeros de trabajo descubrimos que, al hacerlo, aparece un nuevo caso de uso que invalida esa funcionalidad compartida. Si se necesita una funcionalidad compartida, es bastante fácil crear una clase auxiliar.
J Trana
@Prog Su línea de pensamiento es mayormente correcta, pero nuevamente, la abstracción y el subtipo son dos cosas separadas. Cuando dices you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named Cque estás asumiendo que las clases de alguna manera no son abstractas. Una abstracción es cualquier cosa que oculta detalles de implementación, por lo que una clase con campos privados es tan abstracta como una interfaz con los mismos métodos públicos.
Doval
1

Existe un acoplamiento entre las clases padre e hijo, ya que el hijo depende del padre.

Digamos que tenemos una clase A, y la clase B hereda de ella. Si entramos en la clase A y cambiamos las cosas, la clase B también cambia.

Digamos que tenemos una interfaz I, y la clase B la implementa. Si cambiamos la interfaz I, aunque la clase B podría no implementarla más, la clase B no cambia.

Michael Shaw
fuente
Tengo curiosidad por saber si los downvoters tenían una razón o si simplemente estaban teniendo un mal día.
Michael Shaw
1
No voté en contra, pero creo que puede tener que ver con la primera oración. Las clases secundarias se acoplan a las clases para padres, no al revés. El padre no necesita saber nada sobre el niño, pero el niño necesita un conocimiento íntimo sobre el padre.
@JohnGaughan: Gracias por los comentarios. Editado para mayor claridad.
Michael Shaw