Dla mnie, kiedy zaczynam, ich znaczenie stało się jasne, kiedy przestajesz patrzeć na nie jako na rzeczy ułatwiające / przyspieszające pisanie kodu - to nie jest ich cel. Mają wiele zastosowań:
(To straci analogię do pizzy, ponieważ wizualizacja zastosowania tego nie jest łatwa)
Załóżmy, że tworzysz prostą grę na ekranie i będzie ona zawierała stworzenia, z którymi będziesz współdziałać.
Odp .: Mogą ułatwić utrzymanie kodu w przyszłości, wprowadzając luźne połączenie między interfejsem użytkownika a implementacją zaplecza.
Możesz to napisać na początek, ponieważ będą tylko trolle:
// This is our back-end implementation of a troll
class Troll
{
void Walk(int distance)
{
//Implementation here
}
}
Przód:
function SpawnCreature()
{
Troll aTroll = new Troll();
aTroll.Walk(1);
}
Dwa tygodnie później marketing decyduje, że potrzebujesz również Orków, ponieważ czytają o nich na Twitterze, więc musisz zrobić coś takiego:
class Orc
{
void Walk(int distance)
{
//Implementation (orcs are faster than trolls)
}
}
Przód:
void SpawnCreature(creatureType)
{
switch(creatureType)
{
case Orc:
Orc anOrc = new Orc();
anORc.Walk();
case Troll:
Troll aTroll = new Troll();
aTroll.Walk();
}
}
I możesz zobaczyć, jak zaczyna się robić bałagan. Możesz tutaj użyć interfejsu, aby twój interfejs został napisany raz i (tutaj jest ważny bit) przetestowany, a następnie możesz w razie potrzeby podłączyć kolejne elementy zaplecza:
interface ICreature
{
void Walk(int distance)
}
public class Troll : ICreature
public class Orc : ICreature
//etc
Interfejs jest wtedy:
void SpawnCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
}
Interfejs teraz dba tylko o interfejs ICreature - nie przejmuje się wewnętrzną implementacją trolla lub orka, a jedynie faktem, że implementują ICreature.
Ważną kwestią, na którą należy zwrócić uwagę, patrząc na to z tego punktu widzenia, jest to, że można łatwo użyć abstrakcyjnej klasy stworzeń i z tej perspektywy ma to ten sam efekt.
I możesz wyodrębnić kreację do fabryki:
public class CreatureFactory {
public ICreature GetCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
return creature;
}
}
A nasz front stałby się wtedy:
CreatureFactory _factory;
void SpawnCreature(creatureType)
{
ICreature creature = _factory.GetCreature(creatureType);
creature.Walk();
}
Interfejs użytkownika nie musi już nawet zawierać odwołania do biblioteki, w której zaimplementowano Troll i Orka (pod warunkiem, że fabryka znajduje się w osobnej bibliotece) - nie musi nic o nich wiedzieć.
B: Powiedzmy, że masz funkcjonalność, którą tylko niektóre stworzenia będą miały w twojej jednorodnej strukturze danych , np
interface ICanTurnToStone
{
void TurnToStone();
}
public class Troll: ICreature, ICanTurnToStone
Front może być wtedy:
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: Zastosowanie do wstrzykiwania zależności
Większość platform wstrzykiwania zależności jest łatwiejsza w obsłudze, gdy istnieje bardzo luźne połączenie między kodem frontonu a implementacją back-endu. Jeśli weźmiemy powyższy przykład fabryki i wdrożymy interfejs w naszej fabryce:
public interface ICreatureFactory {
ICreature GetCreature(string creatureType);
}
Nasz interfejs może wtedy zostać wstrzyknięty (np. Kontroler API MVC) przez konstruktor (zazwyczaj):
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);
}
}
Za pomocą naszego frameworka DI (np. Ninject lub Autofac) możemy je skonfigurować tak, aby w czasie wykonywania utworzono instancję CreatureFactory, ilekroć w konstruktorze jest potrzebna ICreatureFactory, co czyni nasz kod ładnym i prostym.
Oznacza to również, że kiedy piszemy test jednostkowy dla naszego kontrolera, możemy zapewnić wyśmiewany ICreatureFactory (np. Jeśli konkretna implementacja wymaga dostępu do DB, nie chcemy, aby nasze testy jednostkowe zależały od tego) i łatwo przetestować kod w naszym kontrolerze .
D: Istnieją inne zastosowania, np. Masz dwa projekty A i B, które z przyczyn „starszych” nie są dobrze zorganizowane, a A ma odniesienie do B.
Następnie znajdziesz funkcjonalność w B, która musi wywołać metodę już w A. Nie możesz tego zrobić za pomocą konkretnych implementacji, ponieważ otrzymujesz cykliczne odwołanie.
Możesz zadeklarować interfejs w B, który następnie implementuje klasa A. Do metody w B można przekazać instancję klasy, która bez problemu implementuje interfejs, nawet jeśli konkretny obiekt jest typu w A.