Для меня, когда я начинал, смысл этого стал ясен только тогда, когда вы перестали смотреть на них как на вещи, которые делают ваш код легче / быстрее писать - это не их цель. У них есть ряд применений:
(Это приведет к потере аналогии с пиццей, так как это не очень легко визуализировать)
Скажем, вы делаете простую игру на экране, и в ней будут существа, с которыми вы взаимодействуете.
О: Они могут упростить поддержку вашего кода в будущем, введя слабую связь между вашим внешним интерфейсом и вашей серверной реализацией.
Вы можете написать это для начала, так как будут только тролли:
// This is our back-end implementation of a troll
class Troll
{
void Walk(int distance)
{
//Implementation here
}
}
Внешний интерфейс:
function SpawnCreature()
{
Troll aTroll = new Troll();
aTroll.Walk(1);
}
Спустя две недели, маркетологи решили, что вам также нужны орки, так как они читают о них в твиттере, поэтому вам придется сделать что-то вроде:
class Orc
{
void Walk(int distance)
{
//Implementation (orcs are faster than trolls)
}
}
Внешний интерфейс:
void SpawnCreature(creatureType)
{
switch(creatureType)
{
case Orc:
Orc anOrc = new Orc();
anORc.Walk();
case Troll:
Troll aTroll = new Troll();
aTroll.Walk();
}
}
И вы можете видеть, как это начинает становиться грязным. Здесь вы можете использовать интерфейс, чтобы ваш интерфейс был написан один раз и (вот важный момент) протестирован, а затем вы можете подключить дополнительные элементы интерфейса по мере необходимости:
interface ICreature
{
void Walk(int distance)
}
public class Troll : ICreature
public class Orc : ICreature
//etc
Тогда передний конец:
void SpawnCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
creature.Walk();
}
Внешний интерфейс теперь заботится только об интерфейсе ICreature - он не беспокоится о внутренней реализации тролля или орка, а только о том, что они реализуют ICreature.
При рассмотрении этого с этой точки зрения важно отметить, что вы могли бы также легко использовать класс абстрактных существ, и с этой точки зрения это имеет тот же эффект.
И вы можете извлечь творение на фабрику:
public class CreatureFactory {
public ICreature GetCreature(creatureType)
{
ICreature creature;
switch(creatureType)
{
case Orc:
creature = new Orc();
case Troll:
creature = new Troll();
}
return creature;
}
}
И тогда наш интерфейс станет:
CreatureFactory _factory;
void SpawnCreature(creatureType)
{
ICreature creature = _factory.GetCreature(creatureType);
creature.Walk();
}
Внешний интерфейс теперь даже не должен иметь ссылку на библиотеку, в которой реализованы Troll и Orc (при условии, что фабрика находится в отдельной библиотеке) - ему вообще ничего не нужно знать о них.
B: Скажем, у вас есть функциональность, которую будут иметь только некоторые существа в вашей однородной структуре данных , например
interface ICanTurnToStone
{
void TurnToStone();
}
public class Troll: ICreature, ICanTurnToStone
Тогда передний конец может быть:
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: Использование для внедрения зависимости
С большинством инфраструктур внедрения зависимостей легче работать, когда существует очень слабая связь между кодом внешнего интерфейса и реализацией внутреннего интерфейса. Если мы возьмем наш фабричный пример выше и заставим нашу фабрику реализовать интерфейс:
public interface ICreatureFactory {
ICreature GetCreature(string creatureType);
}
Наш внешний интерфейс может затем внедрить это (например, контроллер MVC API) через конструктор (обычно):
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);
}
}
С нашей структурой DI (например, Ninject или Autofac) мы можем настроить их так, чтобы во время выполнения экземпляр CreatureFactory создавался всякий раз, когда в конструкторе требуется ICreatureFactory - это делает наш код красивым и простым.
Это также означает, что когда мы пишем модульный тест для нашего контроллера, мы можем предоставить фиктивную ICreatureFactory (например, если конкретная реализация требует доступа к БД, мы не хотим, чтобы наши модульные тесты зависели от этого) и легко протестировать код в нашем контроллере ,
D: Есть и другие варианты использования, например, у вас есть два проекта A и B, которые по «устаревшим» причинам не имеют четкой структуры, и A имеет ссылку на B.
Затем вы обнаружите функциональность в B, которая должна вызывать метод уже в A. Вы не можете сделать это, используя конкретные реализации, когда получаете циклическую ссылку.
Вы можете иметь интерфейс, объявленный в B, который затем реализует класс в A. Вашему методу в B может быть передан экземпляр класса, который без проблем реализует интерфейс, даже если конкретный объект имеет тип в A.