Terminologia: mi riferirò al costrutto del linguaggio interface
come 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 sort
routine 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 sort
sull'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.LinkedList
supporta sia l' List
interfaccia che riguarda l'astrazione "raccolta ordinata, indicizzabile", sia l' Queue
interfaccia 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:
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.
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 Comparable
un'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 cmp
e 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' Comparable
interfaccia 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 -2
e 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.