Perché le interfacce sono più utili delle superclassi per ottenere un accoppiamento lento?


15

( Ai fini di questa domanda, quando dico "interfaccia" intendo il costrutto del linguaggiointerface e non una "interfaccia" nell'altro senso della parola, ovvero i metodi pubblici che una classe offre al mondo esterno per comunicare con e manipolarlo. )

L'accoppiamento allentato può essere ottenuto facendo dipendere un oggetto da un'astrazione anziché da un tipo concreto.

Ciò consente un accoppiamento lento per due motivi principali: 1- le astrazioni hanno meno probabilità di cambiare rispetto ai tipi concreti, il che significa che il codice dipendente ha meno probabilità di infrangersi. 2- diversi tipi di calcestruzzo possono essere utilizzati in fase di esecuzione, perché si adattano tutti all'astrazione. Nuovi tipi concreti possono anche essere aggiunti in seguito senza la necessità di modificare il codice dipendente esistente.

Ad esempio, considera una classe Care due sottoclassi Volvoe Mazda.

Se il codice dipende da a Car, può utilizzare a Volvoo a Mazdadurante il runtime. Inoltre in seguito è possibile aggiungere ulteriori sottoclassi senza dover modificare il codice dipendente.

Inoltre, Carche è un'astrazione, è meno probabile che cambi di Volvoo Mazda. Le auto sono state generalmente le stesse per un po 'di tempo, ma Volvos e Mazdas hanno molte più probabilità di cambiare. Cioè le astrazioni sono più stabili dei tipi concreti.

Tutto questo per dimostrare che capisco cos'è l'accoppiamento libero e come si ottiene dipendendo dalle astrazioni e non dalle concrezioni. (Se ho scritto qualcosa di inaccurato, per favore, dillo).

Quello che non capisco è questo:

Le astrazioni possono essere superclassi o interfacce.

In tal caso, perché le interfacce sono elogiate in modo specifico per la loro capacità di consentire un accoppiamento libero? Non vedo come sia diverso dall'uso di una superclasse.

Le uniche differenze che vedo sono: 1- Le interfacce non sono limitate dalla singola eredità, ma ciò non ha molto a che fare con l'argomento dell'accoppiamento libero. 2- Le interfacce sono più "astratte" poiché non hanno alcuna logica di implementazione. Tuttavia, non vedo perché questo faccia una differenza così grande.

Per favore spiegami perché si dice che le interfacce siano grandi nel permettere l'accoppiamento libero, mentre non lo sono le semplici superclassi.


3
La maggior parte delle lingue (ad es. Java, C #) che hanno "interfacce" supportano solo la singola eredità. Poiché ogni classe può avere solo una superclasse immediata, le superclasse (astratte) sono troppo limitate per consentire a un oggetto di supportare più astrazioni. Scopri i tratti (ad esempio Scala o Ruoli di Perl ) per un'alternativa moderna che evita anche il " problema dei diamanti " con eredità multipla.
amon,

@amon Quindi stai dicendo che il vantaggio delle interfacce rispetto alle classi astratte quando si cerca di ottenere un accoppiamento libero non è che non siano limitate dalla singola eredità?
Aviv Cohn,

No, intendevo costoso in termini di compilatore che ha più a che fare quando gestisce una classe astratta , ma questo potrebbe essere probabilmente trascurato.
pastoso,

2
Sembra che @amon sia sulla buona strada, ho trovato questo post in cui si dice che: 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(che mi porta al confronto con C ++, dove le interfacce sono solo classi con funzioni virtuali pure).
pastoso,

Si prega di dire chi dice che le superclasse sono cattive.
Tulains Córdova,

Risposte:


11

Terminologia: mi riferirò al costrutto del linguaggio interfacecome interfaccia e all'interfaccia di un tipo o oggetto come superficie (per mancanza di un termine migliore).

L'accoppiamento allentato può essere ottenuto facendo dipendere un oggetto da un'astrazione anziché da un tipo concreto.

Corretta.

Ciò consente un accoppiamento lento per due motivi principali: 1 - le astrazioni hanno meno probabilità di cambiare rispetto ai tipi concreti, il che significa che il codice dipendente ha meno probabilità di infrangersi. 2 - diversi tipi di calcestruzzo possono essere utilizzati in fase di esecuzione, perché si adattano tutti all'astrazione. Nuovi tipi concreti possono anche essere aggiunti in seguito senza la necessità di modificare il codice dipendente esistente.

Non del tutto corretto. Le lingue attuali generalmente non prevedono che cambierà un'astrazione (sebbene ci siano alcuni modelli di progettazione per gestirla). Separare i dettagli dalle cose generali è l' astrazione. Questo di solito è fatto da qualche strato di astrazione . Questo livello può essere modificato in alcuni altri dettagli senza rompere il codice che si basa su questa astrazione: si ottiene un accoppiamento lento. Esempio non OOP: una sortroutine potrebbe essere cambiata da Quicksort nella versione 1 a Tim Ordinamento nella versione 2. Il codice che dipende solo dal risultato ordinato (ovvero si basa sortsull'astrazione) viene quindi disaccoppiato dall'attuale implementazione dello smistamento.

Ciò che ho definito superficie sopra è la parte generale di un'astrazione. Ora succede in OOP che a volte un oggetto deve supportare più astrazioni. Un esempio non del tutto ottimale: Java java.util.LinkedListsupporta sia l' Listinterfaccia che riguarda l'astrazione "raccolta ordinata, indicizzabile", sia l' Queueinterfaccia che (in termini approssimativi) riguarda l'astrazione "FIFO".

Come può un oggetto supportare più astrazioni?

C ++ non ha interfacce, ma ha ereditarietà multipla, metodi virtuali e classi astratte. Un'astrazione può quindi essere definita come una classe astratta (cioè una classe che non può essere immediatamente istanziata) che dichiara, ma non definisce metodi virtuali. Le classi che implementano i dettagli di un'astrazione possono quindi ereditare da quella classe astratta e implementare i metodi virtuali richiesti.

Il problema qui è che l'ereditarietà multipla può portare al problema del diamante , in cui l'ordine in cui le classi vengono ricercate per un'implementazione del metodo (MRO: ordine di risoluzione del metodo) può portare a "contraddizioni". Ci sono due risposte a questo:

  1. Definisci un ordine sano e rifiuta quegli ordini che non possono essere sensibilmente linearizzati. Il C3 MRO è abbastanza ragionevole e funziona bene. È stato pubblicato nel 1996.

  2. Prendi la strada facile e rifiuta l'eredità multipla in tutto.

Java ha scelto quest'ultima opzione e ha scelto l'eredità comportamentale singola. Tuttavia, abbiamo ancora bisogno della capacità di un oggetto di supportare più astrazioni. Pertanto, è necessario utilizzare interfacce che non supportano le definizioni dei metodi, ma solo le dichiarazioni.

Il risultato è che l'MRO è ovvio (basta guardare ogni superclasse in ordine) e che il nostro oggetto può avere più superfici per qualsiasi numero di astrazioni.

Ciò risulta piuttosto insoddisfacente, perché abbastanza spesso un po 'di comportamento fa parte della superficie. Prendi in considerazione Comparableun'interfaccia:

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
}

Questo è molto user-friendly (una bella API con molti metodi convenienti), ma noioso da implementare. Vorremmo che l'interfaccia includesse cmpe implementasse automaticamente gli altri metodi solo in base a quello richiesto. Mixin , ma soprattutto Traits [ 1 ], [ 2 ] risolvono questo problema senza cadere nelle trappole dell'eredità multipla.

Questo viene fatto definendo una composizione di tratti in modo che i tratti non finiscano effettivamente per prendere parte all'MRO - invece i metodi definiti sono composti nella classe di implementazione.

L' Comparableinterfaccia potrebbe essere espressa in Scala come

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

Quando una classe utilizza quindi quel tratto, gli altri metodi vengono aggiunti alla definizione della classe:

// "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
}

Così Inty(4) cmp Inty(6)sarebbe -2e Inty(4) lt Inty(6)sarebbe true.

Molte lingue hanno un certo supporto per i tratti e qualsiasi linguaggio che ha un "Protocollo Metaobject (MOP)" può avere tratti aggiunti ad esso. Il recente aggiornamento di Java 8 ha aggiunto metodi predefiniti simili ai tratti (i metodi nelle interfacce possono avere implementazioni di fallback in modo che sia facoltativo per implementare le classi per implementare questi metodi).

Sfortunatamente, i tratti sono un'invenzione abbastanza recente (2002) e sono quindi abbastanza rari nelle lingue principali più grandi.


Buona risposta, ma aggiungerei che i linguaggi a ereditarietà singola possono alterare l'ereditarietà multipla utilizzando le interfacce con la composizione.

4

Quello che non capisco è questo:

Le astrazioni possono essere superclassi o interfacce.

In tal caso, perché le interfacce sono elogiate in modo specifico per la loro capacità di consentire un accoppiamento libero? Non vedo come sia diverso dall'uso di una superclasse.

Innanzitutto, il sottotipo e l'astrazione sono due cose diverse. Sottotipare significa semplicemente che posso sostituire i valori di un tipo con valori di un altro tipo - nessuno dei due tipi deve essere astratto.

Ancora più importante, le sottoclassi hanno una dipendenza diretta dai dettagli di implementazione della loro superclasse. Questo è il tipo di accoppiamento più forte che ci sia. In effetti, se la classe base non è progettata pensando all'eredità, le modifiche alla classe base che non cambiano il suo comportamento possono comunque spezzare le sottoclassi e non c'è modo di sapere a priori se si verificherà la rottura. Questo è noto come il fragile problema della classe base .

L'implementazione di un'interfaccia non ti accoppia a nulla tranne all'interfaccia stessa, che non contiene alcun comportamento.


Grazie per aver risposto. Per capire se ho capito: quando vuoi che un oggetto di nome A dipenda da un'astrazione di nome B invece di un'implementazione concreta di quell'astrazione di nome C, spesso è meglio che B sia un'interfaccia implementata da C, anziché una superclasse estesa da C. Questo perché: la sottoclasse C abbina strettamente C a B. Se B cambia - C cambia. Tuttavia C che implementa B (essendo B un'interfaccia) non accoppia B a C: B è solo un elenco di metodi che C deve implementare, quindi nessun accoppiamento stretto. Tuttavia, per quanto riguarda l'oggetto A (il dipendente), non importa se B sia una classe o un'interfaccia.
Aviv Cohn,

Corretta? ..... .....
Aviv Cohn,

Perché considereresti un'interfaccia come accoppiata a qualcosa?
Michael Shaw,

Penso che questa risposta lo inchiodi sulla testa. Uso un po 'C ++, e come è stato affermato in una delle altre risposte, C ++ non ha interfacce ma lo falsi usando superclassi con tutti i metodi lasciati come "puro virtuale" (cioè implementato dai bambini). Il punto è che è facile creare classi base che FANNO qualcosa insieme a funzionalità delegate. In molti, molti, molti casi, io e i miei colleghi scopriamo che facendo così arriva un nuovo caso d'uso che invalida quel po 'di funzionalità condivisa. Se sono necessarie funzionalità condivise, è abbastanza facile creare una classe di supporto.
J Trana,

@Prog La tua linea di pensiero è per lo più corretta, ma ancora una volta, astrazione e sottotipizzazione sono due cose separate. Quando dici you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named Cche stai assumendo che le classi non sono in qualche modo astratte. Un'astrazione è tutto ciò che nasconde i dettagli di implementazione, quindi una classe con campi privati ​​è altrettanto astratta di un'interfaccia con gli stessi metodi pubblici.
Doval,

1

Esiste un accoppiamento tra classi genitore e figlio, poiché il figlio dipende dal genitore.

Supponiamo che abbiamo una classe A e la classe B eredita da essa. Se andiamo in classe A e cambiamo le cose, anche la classe B viene cambiata.

Supponiamo che abbiamo un'interfaccia I e la classe B la implementa. Se cambiamo l'interfaccia I, anche se la classe B potrebbe non implementarla più, la classe B rimane invariata.


Sono curioso di sapere se i downvoter abbiano avuto un motivo o stessero solo passando una brutta giornata.
Michael Shaw,

1
Non ho votato in negativo, ma penso che potrebbe avere a che fare con la prima frase. Le classi secondarie sono accoppiate alle classi parent, non viceversa. Il genitore non ha bisogno di sapere nulla del bambino, ma il bambino ha bisogno di una conoscenza intima del genitore.

@JohnGaughan: grazie per il feedback. A cura di chiarezza.
Michael Shaw,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.