Pour moi, au début, l'intérêt de ceux-ci n'est devenu clair que lorsque vous cessez de les regarder comme des choses pour rendre votre code plus facile / plus rapide à écrire - ce n'est pas leur but. Ils ont un certain nombre d'utilisations:
(Cela va perdre l'analogie avec la pizza, car il n'est pas très facile de visualiser une utilisation de cela)
Supposons que vous créez un jeu simple à l'écran et qu'il aura des créatures avec lesquelles vous interagirez.
R: Ils peuvent faciliter la maintenance de votre code à l'avenir en introduisant un couplage lâche entre votre implémentation frontale et votre implémentation back end.
Vous pourriez écrire ceci pour commencer, car il n'y aura que des trolls:
// This is our back-end implementation of a troll
class Troll
{
void Walk(int distance)
{
//Implementation here
}
}
L'extrémité avant:
function SpawnCreature()
{
Troll aTroll = new Troll();
aTroll.Walk(1);
}
Deux semaines plus tard, le marketing décide que vous avez également besoin d'Orcs, comme ils en lisent sur Twitter, vous devez donc faire quelque chose comme:
class Orc
{
void Walk(int distance)
{
//Implementation (orcs are faster than trolls)
}
}
L'extrémité avant:
void SpawnCreature(creatureType)
{
switch(creatureType)
{
case Orc:
Orc anOrc = new Orc();
anORc.Walk();
case Troll:
Troll aTroll = new Troll();
aTroll.Walk();
}
}
Et vous pouvez voir comment cela commence à devenir désordonné. Vous pouvez utiliser une interface ici pour que votre front-end soit écrit une fois et (voici le bit important) testé, et vous pouvez ensuite brancher d'autres éléments back-end si nécessaire:
interface ICreature
{
void Walk(int distance)
}
public class Troll : ICreature
public class Orc : ICreature
//etc
L'avant est alors:
void SpawnCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
}
Le frontal ne se soucie désormais que de l'interface ICreature - il ne se soucie pas de l'implémentation interne d'un troll ou d'un orc, mais uniquement du fait qu'ils implémentent ICreature.
Un point important à noter lorsque l'on regarde cela de ce point de vue est que vous auriez également pu facilement utiliser une classe de créature abstraite, et de ce point de vue, cela a le même effet.
Et vous pouvez extraire la création dans une usine:
public class CreatureFactory {
public ICreature GetCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
return creature;
}
}
Et notre front-end deviendrait alors:
CreatureFactory _factory;
void SpawnCreature(creatureType)
{
ICreature creature = _factory.GetCreature(creatureType);
creature.Walk();
}
Désormais, le front-end n'a même plus besoin de faire référence à la bibliothèque où Troll et Orc sont implémentés (à condition que l'usine soit dans une bibliothèque séparée) - il n'a besoin de rien savoir à leur sujet.
B: Supposons que vous ayez des fonctionnalités que seules certaines créatures auront dans votre structure de données par ailleurs homogène , par exemple
interface ICanTurnToStone
{
void TurnToStone();
}
public class Troll: ICreature, ICanTurnToStone
Le frontal pourrait alors être:
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: Utilisation pour l'injection de dépendances
La plupart des frameworks d'injection de dépendances sont plus faciles à utiliser lorsqu'il y a un couplage très lâche entre le code frontal et la mise en œuvre du serveur principal. Si nous prenons notre exemple d'usine ci-dessus et que notre usine implémente une interface:
public interface ICreatureFactory {
ICreature GetCreature(string creatureType);
}
Notre frontal pourrait alors avoir injecté ceci (par exemple un contrôleur API MVC) via le constructeur (généralement):
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);
}
}
Avec notre framework DI (par exemple Ninject ou Autofac), nous pouvons les configurer de telle sorte qu'au moment de l'exécution, une instance de CreatureFactory sera créée chaque fois qu'un ICreatureFactory est nécessaire dans un constructeur - cela rend notre code agréable et simple.
Cela signifie également que lorsque nous écrivons un test unitaire pour notre contrôleur, nous pouvons fournir une ICreatureFactory simulée (par exemple, si l'implémentation concrète nécessite un accès DB, nous ne voulons pas que nos tests unitaires en dépendent) et tester facilement le code dans notre contrôleur .
D: Il existe d'autres utilisations, par exemple, vous avez deux projets A et B qui, pour des raisons «héritées», ne sont pas bien structurés, et A fait référence à B.
Vous trouverez ensuite des fonctionnalités dans B qui doivent appeler une méthode déjà dans A. Vous ne pouvez pas le faire en utilisant des implémentations concrètes car vous obtenez une référence circulaire.
Vous pouvez avoir une interface déclarée en B que la classe de A implémente ensuite. Votre méthode en B peut passer une instance d'une classe qui implémente l'interface sans problème, même si l'objet concret est d'un type en A.