Mixin o tratti sono una terza alternativa all'ereditarietà o alla composizione. Che siano migliori dipende spesso dalla particolare implementazione. L'importante caratteristica dei mixin o dei tratti è che sono come classi astratte, ma possono essere applicati a più classi (e una classe può consumare più mixin). Ci sono - in alcune implementazioni - alcuni dettagli riguardanti l'ordine di risoluzione dei metodi che sono diversi tra classi astratte e mixin, ma altrimenti sono realmente equivalenti, fino al punto che questa differenza è irrilevante se il linguaggio consente l'ereditarietà multipla (MI) .
La cosa in cui i tratti sono davvero grandi è che forniscono zero o più metodi e richiedono zero o più metodi. Questo rende l'uso dei tratti abbastanza semplice. Tuttavia, la maggior parte delle lingue non dispone di spazi dei nomi per i metodi: tutte le definizioni vengono inserite negli stessi spazi dei nomi, il che può portare a conflitti tra i tratti.
Il tuo esempio non arriva nemmeno così lontano, perché il tuo mixin dichiara solo implicitamente i requisiti sull'utilizzo delle classi (che freshness_threshold
deve essere una data e @content
deve avere un campo created_at
che deve essere una data ). Questo è anche peggio di una classe astratta, per esempio, Java - eccetto che Java non ha MI, quindi sei forzato per usare la composizione quando "eredita" più comportamenti.
Al contrario, la composizione ti obbliga ad essere più esplicito sul flusso di dati. Questo è buono, perché il tuo codice diventa più ovvio! Nel tuo particolare esempio, il mixin ha solo un singolo metodo, quindi per tutti gli scopi è equivalente a una funzione libera (o Proc
o qualunque sia chiamata in Ruby). Questa funzione richiede due input: la data freshness_threshold
e la data created_at
. Data una tale funzione gratuita is_new(creation_date, threshold_date)
, è ora molto semplice rendere questo comportamento disponibile alle tue classi senza dover inquinare l'interfaccia pubblica di questi oggetti con una data pubblica freshness_threshold
. Per esempio. potrebbero definire
def new?
t = Time.now
return is_new(@content.created_at, Time.new(t.year, t.month))
end
Se i tuoi oggetti dipendono da is_new
come dipendenza esplicita, ad esempio:
def initialize(content, is_new = is_new_default)
@content = content
@is_new = is_new
end
, quindi appare un importante vantaggio della composizione: possiamo fornire dinamicamente un'implementazione diversa (senza dover ricorrere a riflessioni o simili), che ci consente di testare facilmente e separatamente sia l'operazione is_new
che il codice che dipende da questa operazione Questo vantaggio è meno ovvio per un semplice confronto come qui o altre operazioni "funzionalmente pure", ma sarà veramente importante se la dipendenza fornisce effetti collaterali esterni, ad es. richieste di rete, query di database, .... A meno che la lingua non abbia la gestione delle dipendenze integrata nel sistema di ereditarietà o nel sistema di mixaggio, è difficile da battere.
Se Book
s e Magazine
s erano entrambi utilizzati in un contesto polimorfico in cui dovresti scoprire se sono new?
senza sapere se sono Book
o Magazine
, quindi definire un'interfaccia comune che dichiari che questo metodo sarebbe ragionevole (assumendo un linguaggio tipicamente più statico). Tuttavia, l'ereditarietà dell'interfaccia è completamente ortogonale rispetto all'ereditarietà rispetto alla discussione sulla composizione.