Para mí, al comenzar, el punto a estos solo se hizo claro cuando dejas de verlos como cosas para hacer que tu código sea más fácil / rápido de escribir, este no es su propósito. Tienen varios usos:
(Esto va a perder la analogía de la pizza, ya que no es muy fácil visualizar el uso de esto)
Digamos que estás haciendo un juego simple en la pantalla y tendrá criaturas con las que interactúas.
R: Pueden hacer que su código sea más fácil de mantener en el futuro al introducir un acoplamiento suelto entre su front-end y su implementación de back-end.
Para empezar, podría escribir esto, ya que solo habrá trolls:
// This is our back-end implementation of a troll
class Troll
{
void Walk(int distance)
{
//Implementation here
}
}
Interfaz:
function SpawnCreature()
{
Troll aTroll = new Troll();
aTroll.Walk(1);
}
Dos semanas después, el departamento de marketing decide que también necesita Orcos, ya que leyeron sobre ellos en Twitter, por lo que tendría que hacer algo como:
class Orc
{
void Walk(int distance)
{
//Implementation (orcs are faster than trolls)
}
}
Interfaz:
void SpawnCreature(creatureType)
{
switch(creatureType)
{
case Orc:
Orc anOrc = new Orc();
anORc.Walk();
case Troll:
Troll aTroll = new Troll();
aTroll.Walk();
}
}
Y puedes ver cómo esto comienza a complicarse. Puede usar una interfaz aquí para que su front-end se escriba una vez y (aquí está el bit importante) probado, y luego puede conectar más elementos de back-end según sea necesario:
interface ICreature
{
void Walk(int distance)
}
public class Troll : ICreature
public class Orc : ICreature
//etc
El extremo frontal es entonces:
void SpawnCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
}
El front end ahora solo se preocupa por la interfaz ICreature: no le preocupa la implementación interna de un troll o un orco, sino solo el hecho de que implementan ICreature.
Un punto importante a tener en cuenta al mirar esto desde este punto de vista es que también podrías haber usado fácilmente una clase de criatura abstracta, y desde esta perspectiva, esto tiene el mismo efecto.
Y podría extraer la creación a una 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;
}
}
Y nuestro front end se convertiría en:
CreatureFactory _factory;
void SpawnCreature(creatureType)
{
ICreature creature = _factory.GetCreature(creatureType);
creature.Walk();
}
El front end ahora ni siquiera tiene que tener una referencia a la biblioteca donde se implementan Troll y Orc (siempre que la fábrica esté en una biblioteca separada); no necesita saber nada de ellos.
B: Digamos que tiene una funcionalidad que solo algunas criaturas tendrán en su estructura de datos homogénea , por ejemplo
interface ICanTurnToStone
{
void TurnToStone();
}
public class Troll: ICreature, ICanTurnToStone
El front end podría 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 inyección de dependencia
La mayoría de los marcos de inyección de dependencia son más fáciles de trabajar cuando hay un acoplamiento muy flojo entre el código de front-end y la implementación de back-end. Si tomamos nuestro ejemplo de fábrica anterior y hacemos que nuestra fábrica implemente una interfaz:
public interface ICreatureFactory {
ICreature GetCreature(string creatureType);
}
Nuestro front end podría entonces tener esto inyectado (por ejemplo, un controlador MVC API) a través del constructor (típicamente):
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 nuestro marco DI (por ejemplo, Ninject o Autofac), podemos configurarlos para que en tiempo de ejecución se cree una instancia de CreatureFactory siempre que se necesite un ICreatureFactory en un constructor; esto hace que nuestro código sea agradable y simple.
También significa que cuando escribimos una prueba unitaria para nuestro controlador, podemos proporcionar una ICreatureFactory simulada (por ejemplo, si la implementación concreta requiere acceso a la base de datos, no queremos que nuestras pruebas unitarias dependan de eso) y probar fácilmente el código en nuestro controlador .
D: Hay otros usos, por ejemplo, tiene dos proyectos A y B que por razones de 'legado' no están bien estructurados, y A tiene una referencia a B.
Luego encontrará funcionalidad en B que necesita llamar a un método que ya está en A. No puede hacerlo utilizando implementaciones concretas a medida que obtiene una referencia circular.
Puede tener una interfaz declarada en B que la clase en A luego implementa. Su método en B puede pasar una instancia de una clase que implementa la interfaz sin problemas, incluso si el objeto concreto es de un tipo en A.