Perché le interfacce sono più utili delle superclassi nel realizzare un accoppiamento lento?

15

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

L'accoppiamento lento può essere ottenuto avendo un oggetto che dipende da un'astrazione invece che da un tipo concreto.

Ciò consente un accoppiamento libero 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 interruzione. 2 - diversi tipi di calcestruzzo possono essere utilizzati in fase di esecuzione, poiché sono tutti adatti all'astrazione. Nuovi tipi di calcestruzzo possono anche essere aggiunti in seguito senza necessità di modificare il codice dipendente esistente.

Ad esempio, considera una classe Car e due sottoclassi Volvo e Mazda .

Se il tuo codice dipende da Car , durante il runtime può utilizzare Volvo o Mazda . Inoltre, è possibile aggiungere ulteriori sottoclassi senza la necessità di modificare il codice dipendente.

Inoltre, Car - che è un'astrazione - è meno probabile che cambi di Volvo o Mazda . Le automobili sono state generalmente le stesse per un bel po 'di tempo, ma Volvos e Mazda hanno molte più probabilità di cambiare. Cioè le astrazioni sono più stabili dei tipi concreti.

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

Ciò che non capisco è questo:

Le astrazioni possono essere superclassi o interfacce.

In caso affermativo, perché le interfacce sono espressamente elogiate per la loro capacità di consentire un accoppiamento lento? Non vedo come sia diverso dall'usare una superclasse.

Le uniche differenze che vedo sono: 1- Le interfacce non sono limitate dalla singola ereditarietà, ma questo non ha molto a che fare con il tema dell'accoppiamento lento. 2- Le interfacce sono più "astratte" poiché non hanno alcuna logica di implementazione. Ma ancora, non vedo perché questo faccia una grande differenza.

Per favore, spiegami perché le interfacce si dicono grandi per consentire l'accoppiamento lento, mentre le superclassi semplici non lo sono.

    
posta Aviv Cohn 26.04.2014 - 20:27
fonte

3 risposte

11

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

Loose coupling can be achieved by having an object depend on an abstraction instead of a concrete type.

Una corretta.

This allows for loose coupling for two main reasons: 1- abstractions are less likely to change than concrete types, which means the dependent code is less likely to break. 2- different concrete types can be used at runtime, because they all fit the abstraction. New concrete types can also be added later with no need to alter the existing dependent code.

Non proprio corretto. Le lingue attuali generalmente non prevedono che un'astrazione cambierà (sebbene ci siano alcuni schemi di progettazione per gestirli). Separare le specifiche dall'astrazione generale è . Questo di solito è fatto da qualche livello di astrazione . Questo livello può essere modificato con altre specifiche senza che il codice di rottura si costruisca su questa astrazione: si ottiene un accoppiamento lento. Esempio non-OOP: una routine sort potrebbe essere modificata da Quicksort nella versione 1 a Tim Sort nella versione 2. Il codice che dipende solo dal risultato ordinato (ovvero build su sort astrazione) è quindi disaccoppiato dall'effettivo implementazione di ordinamento.

Quello che ho definito surface sopra è la parte generale di un'astrazione. Ora accade in OOP che un oggetto deve a volte supportare più astrazioni. Un esempio non proprio ottimale: Java java.util.LinkedList supporta sia l'interfaccia List che riguarda l'astrazione "ordinata, indicizzabile", che supporta l'interfaccia Queue che (in termini approssimativi) riguarda l'astrazione "FIFO" .

In che modo un oggetto può 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 le specifiche 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 di diamante , dove l'ordine in cui le classi vengono cercate per un'implementazione del metodo (MRO: ordine di risoluzione dei metodi) 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 MRO C3 è abbastanza ragionevole e funziona bene. Fu pubblicato nel 1996.

  2. scegli la strada facile e respingi l'ereditarietà multipla.

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

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

Questo risulta essere piuttosto insoddisfacente, perché molto spesso un po 'di comportamento fa parte della superficie. Prendi in considerazione un'interfaccia 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
}

Questo è molto user-friendly (una bella API con molti metodi convenienti), ma noioso da implementare. Vorremmo che l'interfaccia includesse solo cmp e implementasse automaticamente gli altri metodi in base a quel metodo richiesto. Mixins , ma soprattutto Tratti [ 1 ], [ 2 ] risolve questo problema senza cadere nelle trappole dell'ereditarietà multipla .

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

L'interfaccia Comparable 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 usa questa caratteristica, 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
}

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

Molte lingue hanno un certo supporto per i tratti e qualsiasi lingua che abbia un "Metaobject Protocol (MOP)" può avere tratti aggiunti ad essa. 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 l'implementazione delle classi per implementare questi metodi).

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

    
risposta data 26.04.2014 - 21:59
fonte
4

What I don't understand is this:

Abstractions can be superclasses or interfaces.

If so, why are interfaces specifically praised for their ability to allow loose coupling? I don't see how it's different than using a superclass.

In primo luogo, sottotipizzazione e astrazione sono due cose diverse. Sottotipizzazione significa semplicemente che posso sostituire i valori di un tipo per valori di un altro tipo - nessuno dei due tipi deve essere astratto.

Ancora più importante, le sottoclassi dipendono direttamente dai dettagli di implementazione della loro superclasse. Questo è il tipo più strong di accoppiamento che ci sia. In effetti, se la classe base non è progettata pensando all'ereditarietà, le modifiche alla classe base che non cambiano il suo comportamento possono ancora interrompere le sottoclassi, e non c'è modo di sapere a priori se si rompe si verificherà. Questo è noto come problema della classe base fragile .

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

    
risposta data 27.04.2014 - 00:14
fonte
1

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

Diciamo che abbiamo una classe A, e la classe B ne eredita. Se entriamo in classe A e cambiamo le cose, anche la classe B viene cambiata.

Diciamo di avere 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 è invariata.

    
risposta data 26.04.2014 - 21:41
fonte

Leggi altre domande sui tag