Para mim, quando eu comecei, o ponto para isso ficou claro quando você parava de considerá-los coisas para tornar seu código mais fácil / rápido de escrever - esse não é o objetivo deles. Eles têm vários usos:
(Isso vai perder a analogia da pizza, pois não é muito fácil visualizar um uso disso)
Digamos que você esteja criando um jogo simples na tela e ele terá criaturas com as quais você interage.
R: Eles podem tornar seu código mais fácil de manter no futuro, introduzindo um acoplamento flexível entre o front-end e a implementação do back-end.
Você pode escrever isso para começar, pois só haverá trolls:
// This is our back-end implementation of a troll
class Troll
{
void Walk(int distance)
{
//Implementation here
}
}
A parte dianteira:
function SpawnCreature()
{
Troll aTroll = new Troll();
aTroll.Walk(1);
}
Duas semanas depois, o marketing decide que você também precisa de Orcs, como eles leram sobre eles no twitter, então você teria que fazer algo como:
class Orc
{
void Walk(int distance)
{
//Implementation (orcs are faster than trolls)
}
}
A parte dianteira:
void SpawnCreature(creatureType)
{
switch(creatureType)
{
case Orc:
Orc anOrc = new Orc();
anORc.Walk();
case Troll:
Troll aTroll = new Troll();
aTroll.Walk();
}
}
E você pode ver como isso começa a ficar confuso. Você pode usar uma interface aqui para que seu front-end seja gravado uma vez e (aqui está a parte importante) testado e, em seguida, você poderá conectar outros itens de back-end conforme necessário:
interface ICreature
{
void Walk(int distance)
}
public class Troll : ICreature
public class Orc : ICreature
//etc
Front end é então:
void SpawnCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
}
O front end agora se preocupa apenas com a interface ICreature - não se preocupa com a implementação interna de um troll ou um orc, mas apenas com o fato de eles implementarem o ICreature.
Um ponto importante a ser observado ao olhar para esse ponto de vista é que você também poderia facilmente ter usado uma classe de criatura abstrata e, dessa perspectiva, isso tem o mesmo efeito.
E você pode extrair a criação para uma fábrica:
public class CreatureFactory {
public ICreature GetCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
return creature;
}
}
E nosso front end se tornaria:
CreatureFactory _factory;
void SpawnCreature(creatureType)
{
ICreature creature = _factory.GetCreature(creatureType);
creature.Walk();
}
O front end agora nem precisa ter uma referência à biblioteca onde Troll e Orc são implementados (desde que a fábrica esteja em uma biblioteca separada) - ele não precisa saber nada sobre eles.
B: Digamos que você tenha uma funcionalidade que apenas algumas criaturas terão em sua estrutura de dados homogênea , por exemplo
interface ICanTurnToStone
{
void TurnToStone();
}
public class Troll: ICreature, ICanTurnToStone
O front end pode então ser:
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: Uso para injeção de dependência
A maioria das estruturas de injeção de dependência é mais fácil de trabalhar quando há um acoplamento muito frouxo entre o código do front-end e a implementação do back-end. Se pegarmos nosso exemplo de fábrica acima e fazer com que nossa fábrica implemente uma interface:
public interface ICreatureFactory {
ICreature GetCreature(string creatureType);
}
Nosso front-end pode então ter isso injetado (por exemplo, um controlador de API MVC) através do construtor (normalmente):
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);
}
}
Com nossa estrutura de DI (por exemplo, Ninject ou Autofac), podemos configurá-los para que, em tempo de execução, uma instância do CreatureFactory seja criada sempre que um ICreatureFactory for necessário em um construtor - isso torna nosso código simples e agradável.
Isso também significa que, quando escrevemos um teste de unidade para o nosso controlador, podemos fornecer um ICreatureFactory zombado (por exemplo, se a implementação concreta exigir acesso ao banco de dados, não queremos que nossos testes de unidade dependam disso) e testar facilmente o código em nosso controlador .
D: Existem outros usos, por exemplo, você tem dois projetos A e B que, por razões de 'legado', não são bem estruturados e A tem uma referência a B.
Em seguida, você encontra a funcionalidade em B que precisa chamar um método já em A. Você não pode fazê-lo usando implementações concretas ao obter uma referência circular.
Você pode ter uma interface declarada em B que a classe em A implementa. Seu método em B pode receber uma instância de uma classe que implementa a interface sem problemas, mesmo que o objeto concreto seja do tipo A.