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