Интерфейсы - какой смысл? - PullRequest
241 голосов
/ 23 июля 2011

Причина интерфейсов действительно ускользает от меня.Насколько я понимаю, это своего рода обходной путь для несуществующего мульти-наследования, которого нет в C # (или мне так сказали).

Все, что я вижу, это то, что вы предопределите некоторых членови функции, которые затем должны быть заново определены в классе.Таким образом, делая интерфейс избыточным.Это просто похоже на синтаксис ... ну, чушь для меня.базовый класс Pizza вместо интерфейса.

простой пример (взят из другого вклада переполнения стека)

public interface IPizza
{
    public void Order();
}

public class PepperoniPizza : IPizza
{
    public void Order()
    {
        //Order Pepperoni pizza
    }
}

public class HawaiiPizza : IPizza
{
    public void Order()
    {
        //Order HawaiiPizza
    }
}

Ответы [ 28 ]

420 голосов
/ 30 июля 2011

Никто на самом деле не объяснил в понятной форме, как интерфейсы полезны, поэтому я собираюсь дать ему шанс (и немного украду идею из ответа Шамима).

Давайте рассмотрим идею заказа пиццы.Вы можете иметь несколько видов пиццы, и общее действие для каждой пиццы готовит заказ в системе.Каждая пицца должна быть приготовлена ​​, но каждая пицца готовится по-разному .Например, при заказе пиццы с начинкой из корочки система, вероятно, должна проверить наличие в ресторане определенных ингредиентов и отложить те, которые не нужны для пиццы с глубоким блюдом.

При техническом написании этого кодаВы могли бы просто сделать

public class Pizza()
{
    public void Prepare(PizzaType tp)
    {
        switch (tp)
        {
            case PizzaType.StuffedCrust:
                // prepare stuffed crust ingredients in system
                break;

            case PizzaType.DeepDish:
                // prepare deep dish ingredients in system
                break;

            //.... etc.
        }
    }
}

Однако для пиццы с глубоким блюдом (в терминах C #) может потребоваться установить другие свойства в методе Prepare(), чем фаршированная корочка, и, таким образом, вы получите множество дополнительныхсвойства, а класс плохо масштабируется (что, если вы добавите новые типы пиццы).

Правильный способ решить эту проблему - использовать интерфейс.Интерфейс заявляет, что все пиццы могут быть приготовлены, но каждая пицца может быть приготовлена ​​по-разному.Поэтому, если у вас есть следующие интерфейсы:

public interface IPizza
{
    void Prepare();
}

public class StuffedCrustPizza : IPizza
{
    public void Prepare()
    {
        // Set settings in system for stuffed crust preparations
    }
}

public class DeepDishPizza : IPizza
{
    public void Prepare()
    {
        // Set settings in system for deep dish preparations
    }
}

Теперь вашему коду обработки заказа не нужно точно знать, какие типы пиццы были заказаны для обработки ингредиентов.Он просто имеет:

public PreparePizzas(IList<IPizza> pizzas)
{
    foreach (IPizza pizza in pizzas)
        pizza.Prepare();
}

Даже если каждый тип пиццы готовится по-разному, эта часть кода не должна заботиться о том, с каким типом пиццы мы имеем дело, она просто знает, что ее называютдля пиццы и, следовательно, каждый вызов Prepare автоматически подготовит каждую пиццу правильно, в зависимости от ее типа, даже если в коллекции есть несколько типов пиццы.

168 голосов
/ 23 июля 2011

Дело в том, что интерфейс представляет собой контракт .Набор открытых методов, которые должен иметь любой реализующий класс.Технически интерфейс управляет только синтаксисом, т. Е. Какие методы существуют, какие аргументы они получают и что возвращают.Обычно они также инкапсулируют семантику, хотя только в документации.

Вы можете иметь различные реализации интерфейса и менять их по своему желанию.В вашем примере, поскольку каждый экземпляр пиццы является IPizza, вы можете использовать IPizza везде, где вы обрабатываете экземпляр неизвестного типа пиццы.Любой экземпляр, тип которого наследуется от IPizza, гарантированно будет заказуемым, поскольку он имеет метод Order().

Python не имеет статической типизации, поэтому типы сохраняются и проверяются во время выполнения.Поэтому вы можете попробовать вызвать метод Order() для любого объекта.Среда выполнения счастлива, если у объекта есть такой метод, и, вероятно, просто пожимает плечами и говорит «Мех», если его нет.Не так в C #.Компилятор отвечает за правильные вызовы, и если он просто имеет случайный object, компилятор еще не знает, будет ли экземпляр во время выполнения иметь этот метод.С точки зрения компилятора он недействителен, поскольку не может это проверить.(Вы можете делать такие вещи с помощью отражения или ключевого слова dynamic, но, думаю, сейчас это немного далеко).

Также обратите внимание, что интерфейс в обычном смысле не обязательно должен бытьC # interface, это может быть также абстрактный класс или даже обычный класс (который может пригодиться, если все подклассы должны совместно использовать какой-то общий код - в большинстве случаев, однако, достаточно interface).

103 голосов
/ 11 мая 2013

Для меня смысл этого стал ясен только тогда, когда вы перестали смотреть на них как на вещи, которые делают ваш код легче / быстрее писать - это не их цель.У них есть несколько применений:

(Это потеряет аналогию с пиццей, так как ее использование будет не очень легко представить)

Скажем, вы делаете простую игру на экранеи у него будут существа, с которыми вы взаимодействуете.

A: Они могут упростить поддержку вашего кода в будущем, введя слабую связь между вашим внешним интерфейсом и вашей серверной реализацией.

Вы могли бы написать это для начала, так как будут только тролли:

// 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.

32 голосов
/ 23 июля 2011

Вот ваши новые примеры:

public interface IFood // not Pizza
{
    public void Prepare();

}

public class Pizza : IFood
{
    public void Prepare() // Not order for explanations sake
    {
        //Prepare Pizza
    }
}

public class Burger : IFood
{
    public void Prepare()
    {
        //Prepare Burger
    }
}
22 голосов
/ 20 февраля 2015

Примеры выше не имеют особого смысла. Вы можете выполнить все приведенные выше примеры, используя классы (абстрактный класс, если вы хотите, чтобы он вел себя только как контракт ):

public abstract class Food {
    public abstract void Prepare();
}

public class Pizza : Food  {
    public override void Prepare() { /* Prepare pizza */ }
}

public class Burger : Food  {
    public override void Prepare() { /* Prepare Burger */ }
}

Вы получаете то же поведение, что и с интерфейсом. Вы можете создать List<Food> и выполнять итерацию, не зная, какой класс находится сверху.

Более адекватным примером будет множественное наследование:

public abstract class MenuItem {
    public string Name { get; set; }
    public abstract void BringToTable();
}

// Notice Soda only inherits from MenuItem
public class Soda : MenuItem {
    public override void BringToTable() { /* Bring soda to table */ }
}


// All food needs to be cooked (real food) so we add this
// feature to all food menu items
public interface IFood {
    void Cook();
}

public class Pizza : MenuItem, IFood {
    public override void BringToTable() { /* Bring pizza to table */ }
    public void Cook() { /* Cook Pizza */ }
}

public class Burger : MenuItem, IFood {
    public override void BringToTable() { /* Bring burger to table */ }
    public void Cook() { /* Cook Burger */ }
}

Тогда вы можете использовать их все как MenuItem и не заботиться о том, как они обрабатывают каждый вызов метода.

public class Waiter {
    public void TakeOrder(IEnumerable<MenuItem> order) 
    {
        // Cook first
        // (all except soda because soda is not IFood)
        foreach (var food in order.OfType<IFood>())
            food.Cook();

        // Bring them all to the table
        // (everything, including soda, pizza and burger because they're all menu items)
        foreach (var menuItem in order)
            menuItem.BringToTable();
    }
}
20 голосов
/ 29 марта 2017

Простое объяснение с аналогией

Проблема, которую нужно решить: какова цель полиморфизма?

Аналогия: Значит, я прораб на стройке.

Торговцы все время ходят по стройке. Я не знаю, кто будет проходить через эти двери. Но я в основном говорю им, что делать.

  1. Если это плотник, я говорю: строить деревянные леса.
  2. Если это сантехник, я говорю: «Настройте трубы»
  3. Если это электрик, я говорю: «Вытащите кабели и замените их оптоволоконными».

Проблема с вышеуказанным подходом заключается в том, что я должен: (i) знать, кто входит в эту дверь, и в зависимости от того, кто это, я должен сказать им, что делать. Это означает, что я должен знать все о конкретной сделке. Есть затраты / выгоды, связанные с этим подходом:

Значение знания того, что делать:

  • Это означает, что если код плотника изменится с: BuildScaffolding() на BuildScaffold() (т. Е. Небольшое изменение имени), то мне также придется изменить класс вызова (т. Е. Класс Foreperson) - вам нужно будет внести два изменений в код вместо (в основном) только одного. С полиморфизмом вам (в основном) нужно всего лишь сделать одно изменение, чтобы достичь того же результата.

  • Во-вторых, вам не придется постоянно спрашивать: кто вы? хорошо, сделай это ... кто ты? хорошо, сделайте это ..... полиморфизм - он сушит этот код и очень эффективен в определенных ситуациях:

  • с полиморфизмом вы можете легко добавлять дополнительные классы торговцев без изменения какого-либо существующего кода. (т.е. второй из принципов проектирования SOLID: принцип Open-Close).

Решение

Представьте себе сценарий, в котором, независимо от того, кто входит в дверь, я могу сказать: «Работа ()», и они выполняют свою уважительную работу, на которой специализируются: сантехник будет заниматься трубами, а электрик - проводами. .

Преимущество этого подхода заключается в том, что: (i) мне не нужно точно знать, кто входит в эту дверь - все, что мне нужно знать, это то, что они будут своего рода обычным делом и смогут выполнять работу и, во-вторых, (ii) мне не нужно ничего знать об этой конкретной сделке. Об этом позаботится традиция.

Так что вместо этого:

If(electrician) then  electrician.FixCablesAndElectricity() 

if(plumber) then plumber.IncreaseWaterPressureAndFixLeaks() 

Я могу сделать что-то вроде этого:

ITradesman tradie = Tradesman.Factory(); // in reality i know it's a plumber, but in the real world you won't know who's on the other side of the tradie assignment.

tradie.Work(); // and then tradie will do the work of a plumber, or electrician etc. depending on what type of tradesman he is. The foreman doesn't need to know anything, apart from telling the anonymous tradie to get to Work()!!

Какая выгода?

Преимущество состоит в том, что если конкретные требования к работе плотника и т. Д. Изменятся, то руководителю не нужно будет менять свой код - ему не нужно знать или заботиться. Все, что имеет значение, это то, что плотник знает, что подразумевается под Work (). Во-вторых, если на стройплощадку приходит новый тип строителя, то бригадир не должен ничего знать о торговле - все, что волнует бригадира, это если строитель (например, Welder, Glazier, Tiler и т. Д.) получить некоторую работу ().


Иллюстрированная проблема и решение (с интерфейсами и без):

Нет интерфейса (пример 1):

Example 1: without an interface

Нет интерфейса (пример 2):

Example 2: without an interface

С интерфейсом:

Example 3: The benefits of Using an Interface

Резюме

Интерфейс позволяет вам заставить человека выполнять работу, для которой он назначен, без вашего знания того, кто он такой, или специфики того, что он может делать. Это позволяет вам легко добавлять новые типы (торговли) без изменения существующего кода (технически вы действительно меняете его чуть-чуть), и это реальное преимущество ООП-подхода по сравнению с более функциональной методологией программирования.

Если вы не понимаете ничего из вышеперечисленного или если это не ясно, спросите в комментарии, и я постараюсь сделать ответ лучше.

11 голосов
/ 23 июля 2011

Пример Pizza плох, потому что вы должны использовать абстрактный класс, который обрабатывает порядок, а пицца должна просто переопределять тип пиццы, например.

Вы используете интерфейсы, когда у вас есть общее свойство, но ваши классы наследуются из разных мест, или когда у вас нет общего кода, который вы могли бы использовать. Например, это использованные вещи, которые можно утилизировать IDisposable, вы знаете, что они будут утилизированы, вы просто не знаете, что произойдет, когда они будут утилизированы.

Интерфейс - это просто контракт, который говорит вам о том, что может делать объект, какие параметры и какие возвращаемые типы следует ожидать.

11 голосов
/ 23 июля 2011

В отсутствие duck, набирающего , поскольку вы можете использовать его в Python, C # полагается на интерфейсы для предоставления абстракций. Если все зависимости класса были конкретными типами, вы не могли бы передать любой другой тип - используя интерфейсы, вы можете передать любой тип, который реализует интерфейс.

10 голосов
/ 23 июля 2011

Рассмотрим случай, когда вы не управляете или не владеете базовыми классами.

Возьмите визуальные элементы управления, например, в .NET для Winforms все они наследуются от базового класса Control, который полностью определен в .NET Framework.

Давайте предположим, что вы занимаетесь созданием пользовательских элементов управления. Вы хотите создавать новые кнопки, текстовые поля, списки, сетки и многое другое, и вы хотите, чтобы все они имели определенные функции, уникальные для вашего набора элементов управления.

Например, вам может понадобиться общий способ обработки тем или общий способ локализации.

В этом случае вы не можете «просто создать базовый класс», потому что если вы это сделаете, вам придется переопределить все , что относится к элементам управления.

Вместо этого вы перейдете из Button, TextBox, ListView, GridView и т. Д. И добавите свой код.

Но это создает проблему: как вы теперь можете определить, какие элементы управления являются «вашими», как вы можете создать некоторый код, который говорит: «Для всех элементов управления в моей форме установите тему на X». *

Введите интерфейсы.

Интерфейсы - это способ посмотреть на объект, чтобы определить, что объект придерживается определенного контракта.

Вы бы создали «YourButton», спустились с кнопки и добавили поддержку всех необходимых вам интерфейсов.

Это позволит вам написать код, подобный следующему:

foreach (Control ctrl in Controls)
{
    if (ctrl is IMyThemableControl)
        ((IMyThemableControl)ctrl).SetTheme(newTheme);
}

Это было бы невозможно без интерфейсов, вместо этого вам пришлось бы писать такой код:

foreach (Control ctrl in Controls)
{
    if (ctrl is MyThemableButton)
        ((MyThemableButton)ctrl).SetTheme(newTheme);
    else if (ctrl is MyThemableTextBox)
        ((MyThemableTextBox)ctrl).SetTheme(newTheme);
    else if (ctrl is MyThemableGridView)
        ((MyThemableGridView)ctrl).SetTheme(newTheme);
    else ....
}
7 голосов
/ 23 июля 2011

В этом случае вы могли бы (и, вероятно, могли бы) просто определить базовый класс Pizza и наследовать от них. Однако есть две причины, по которым интерфейсы позволяют вам делать вещи, которые не могут быть достигнуты другими способами:

  1. Класс может реализовывать несколько интерфейсов. Он просто определяет функции, которые должен иметь класс. Реализация ряда интерфейсов означает, что класс может выполнять несколько функций в разных местах.

  2. Интерфейс может быть определен в более высоком объеме, чем класс или вызывающая сторона. Это означает, что вы можете разделить функциональность, отделить зависимость проекта и сохранить функциональность в одном проекте или классе и реализовать это в другом месте.

Одним из следствий 2 является то, что вы можете изменить используемый класс, просто требуя, чтобы он реализовал соответствующий интерфейс.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...