Per me, all'inizio, il punto su questi è diventato chiaro solo quando smetti di guardarli come cose per rendere il tuo codice più facile / veloce da scrivere - questo non è il loro scopo. Hanno una serie di usi:
(Questo perderà l'analogia della pizza, poiché non è molto facile visualizzarne l'uso)
Supponi che stai realizzando un semplice gioco sullo schermo e che avrà creature con cui interagisci.
A: Possono semplificare la manutenzione del codice in futuro introducendo un accoppiamento libero tra il front-end e l'implementazione del back-end.
Potresti scrivere questo per cominciare, poiché ci saranno solo troll:
// This is our back-end implementation of a troll
class Troll
{
void Walk(int distance)
{
//Implementation here
}
}
Fine frontale:
function SpawnCreature()
{
Troll aTroll = new Troll();
aTroll.Walk(1);
}
Due settimane dopo, il marketing decide che hai bisogno anche degli Orchi, mentre leggono su di loro su Twitter, quindi dovresti fare qualcosa del tipo:
class Orc
{
void Walk(int distance)
{
//Implementation (orcs are faster than trolls)
}
}
Fine frontale:
void SpawnCreature(creatureType)
{
switch(creatureType)
{
case Orc:
Orc anOrc = new Orc();
anORc.Walk();
case Troll:
Troll aTroll = new Troll();
aTroll.Walk();
}
}
E puoi vedere come questo inizia a diventare disordinato. Puoi usare un'interfaccia qui in modo che il tuo front-end venga scritto una volta e (qui il bit importante) testato, e puoi quindi collegare ulteriori elementi di back-end come richiesto:
interface ICreature
{
void Walk(int distance)
}
public class Troll : ICreature
public class Orc : ICreature
//etc
Il front-end è quindi:
void SpawnCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
}
Il front-end ora si preoccupa solo dell'interfaccia ICreature - non si preoccupa dell'implementazione interna di un troll o di un orco, ma solo del fatto che implementano ICreature.
Un punto importante da notare quando si guarda questo da questo punto di vista è che avresti potuto facilmente usare una classe di creatura astratta, e da questo punto di vista, questo ha lo stesso effetto.
E potresti estrarre la creazione in una fabbrica:
public class CreatureFactory {
public ICreature GetCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
return creature;
}
}
E il nostro front-end diventerebbe quindi:
CreatureFactory _factory;
void SpawnCreature(creatureType)
{
ICreature creature = _factory.GetCreature(creatureType);
creature.Walk();
}
Il front-end ora non deve nemmeno avere un riferimento alla libreria in cui sono implementati Troll e Orc (a condizione che la fabbrica sia in una libreria separata) - non ha bisogno di sapere nulla di loro.
B: Supponi di avere funzionalità che solo alcune creature avranno nella tua struttura di dati altrimenti omogenea , ad es
interface ICanTurnToStone
{
void TurnToStone();
}
public class Troll: ICreature, ICanTurnToStone
Il front-end potrebbe quindi essere:
void SpawnCreatureInSunlight(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
if (creature is ICanTurnToStone)
{
(ICanTurnToStone)creature.TurnToStone();
}
}
C: Utilizzo per iniezione di dipendenza
La maggior parte dei framework di iniezione delle dipendenze è più facile da lavorare quando esiste un accoppiamento molto lento tra il codice front-end e l'implementazione del back-end. Se prendiamo il nostro esempio di fabbrica sopra e la nostra fabbrica implementa un'interfaccia:
public interface ICreatureFactory {
ICreature GetCreature(string creatureType);
}
Il nostro front-end potrebbe quindi iniettare questo (ad esempio un controller API MVC) attraverso il costruttore (in genere):
public class CreatureController : Controller {
private readonly ICreatureFactory _factory;
public CreatureController(ICreatureFactory factory) {
_factory = factory;
}
public HttpResponseMessage TurnToStone(string creatureType) {
ICreature creature = _factory.GetCreature(creatureType);
creature.TurnToStone();
return Request.CreateResponse(HttpStatusCode.OK);
}
}
Con il nostro framework DI (ad esempio Ninject o Autofac), possiamo configurarli in modo che in fase di esecuzione venga creata un'istanza di CreatureFactory ogni volta che è necessario un ICreatureFactory in un costruttore - questo rende il nostro codice piacevole e semplice.
Significa anche che quando scriviamo un test unitario per il nostro controller, possiamo fornire un ICreatureFactory beffardo (ad esempio se l'implementazione concreta richiede l'accesso al DB, non vogliamo che i nostri test unitari dipendano da quello) e testare facilmente il codice nel nostro controller .
D: Esistono altri usi, ad esempio hai due progetti A e B che per motivi "legacy" non sono ben strutturati e A ha un riferimento a B.
Quindi trovi la funzionalità in B che deve chiamare un metodo già in A. Non puoi farlo usando implementazioni concrete quando ottieni un riferimento circolare.
È possibile avere un'interfaccia dichiarata in B che la classe in A quindi implementa. Il tuo metodo in B può essere passato a un'istanza di una classe che implementa l'interfaccia senza problemi, anche se l'oggetto concreto è di un tipo in A.