Почему я не могу понять интерфейсы? - PullRequest
50 голосов
/ 23 сентября 2008

Может ли кто-нибудь, пожалуйста, демистифицировать интерфейсы для меня или указать мне несколько хороших примеров? Я постоянно вижу всплывающие интерфейсы здесь и там, но я никогда не получал хороших объяснений интерфейсов или того, когда их использовать.

Я говорю об интерфейсах в контексте интерфейсов против абстрактных классов.

Ответы [ 26 ]

73 голосов
/ 23 сентября 2008

Интерфейсы позволяют вам программировать по «описанию», а не по типу, что позволяет более свободно связывать элементы вашего программного обеспечения.

Подумайте об этом так: вы хотите поделиться данными с кем-то в кубе рядом с вами, чтобы вы вытащили флешку и скопировали / вставили. Вы идете по соседству, и парень говорит "это USB?" а ты говоришь да - все готово. Неважно, размер флешки или производителя - все, что имеет значение, это то, что это USB.

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

public void Paint(Car car, System.Drawing.Color color)...

Это будет работать, пока ваш клиент не скажет "теперь я хочу покрасить грузовики", чтобы вы могли сделать это:

public void Paint (Vehicle vehicle, System.Drawing.Color color)...

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

public interface IPaintable{
   void Paint(System.Drawing.Color color);
}

... и передал это в вашу рутину:

public void Paint(IPaintable item, System.Drawing.Color color){
   item.Paint(color);
}

Надеюсь, это имеет смысл - это довольно упрощенное объяснение, но, мы надеемся, доходит до сути.

45 голосов
/ 23 сентября 2008

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

Скажем, у вас есть пара классов, которые называются Dog, Cat и Mouse. Каждый из этих классов является домашним животным, и теоретически вы можете унаследовать их от другого класса, называемого домашним животным, но здесь есть проблема. Домашние животные сами по себе ничего не делают. Вы не можете пойти в магазин и купить домашнее животное. Вы можете пойти и купить собаку или кошку, но домашнее животное - это абстрактное понятие, а не конкретное.

Итак, Вы знаете, что домашние животные могут делать определенные вещи. Они могут спать или есть и т. Д. Итак, вы определяете интерфейс под названием IPet, и он выглядит примерно так (синтаксис C #)

public interface IPet
{
    void Eat(object food);
    void Sleep(int duration);
}

Каждый из ваших классов Dog, Cat и Mouse реализует IPet.

public class Dog : IPet

Так что теперь у каждого из этих классов должна быть своя реализация Eat и Sleep. У тебя есть контракт ... Теперь какой смысл.

Далее, скажем, вы хотите создать новый объект с именем PetStore. И это не очень хороший PetStore, поэтому они просто продают вам случайных питомцев (да, я знаю, что это надуманный пример).

public class PetStore
{
     public static IPet GetRandomPet()
     {    
          //Code to return a random Dog, Cat, or Mouse
     } 
}

IPet myNewRandomPet = PetStore.GetRandomPet();
myNewRandomPet.Sleep(10);

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

Таким образом, этот ответ, возможно, не был полезен вообще, но общая идея состоит в том, что интерфейсы позволяют вам делать аккуратные вещи, такие как Dependency Injection и Inversion of Control, где вы можете получить объект, иметь четко определенный список вещей, которые может сделать объект не ДЕЙСТВИТЕЛЬНО зная, каков конкретный тип этого объекта.

33 голосов
/ 23 сентября 2008

Самый простой ответ: интерфейсы определяют, что может делать ваш класс. Это «контракт», в котором говорится, что ваш класс сможет выполнить это действие.

Public Interface IRollOver
    Sub RollOver()
End Interface

Public Class Dog Implements IRollOver
    Public Sub RollOver() Implements IRollOver.RollOver
        Console.WriteLine("Rolling Over!")
    End Sub
End Class

Public Sub Main()
    Dim d as New Dog()
    Dim ro as IRollOver = TryCast(d, IRollOver)
    If ro isNot Nothing Then
        ro.RollOver()
    End If
End Sub

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

16 голосов
/ 23 сентября 2008

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

14 голосов
/ 24 сентября 2008

Интерфейсы - это механизм, уменьшающий связь между различными, возможно разрозненными частями системы.

С .NET перспектива

  • Определение интерфейса представляет собой список операций и / или свойств.
  • Методы интерфейса всегда общедоступны.
  • Сам интерфейс не должен быть публичным.

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

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

Пример, который немного конкретнее:

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

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

// First pass - not maintainable
void SubmitToWorkflow(object o, User u)
{
  if (o is StreetMap)
  {
     var map = (StreetMap)o;
     map.LastUpdated = DateTime.UtcNow;
     map.UpdatedByUser = u.UserID;
  }
  else if (o is Person)
  {
     var person = (Person)o;
     person.LastUpdated = DateTime.Now; // Whoops .. should be UtcNow
     person.UpdatedByUser = u.UserID;
  }
  // Whoa - very unmaintainable.

В приведенном выше коде SubmitToWorkflow() должен знать о каждом объекте. Кроме того, код представляет собой беспорядок с одним массовым переключателем if / else /, нарушающим принцип не повторять себя (DRY), и требует от разработчиков запоминать изменения копирования / вставки при каждом добавлении нового объекта в систему.

// Second pass - brittle
void SubmitToWorkflow(object o, User u)
{
  if (o is DTOBase)
  {
     DTOBase dto = (DTOBase)o;
     dto.LastUpdated = DateTime.UtcNow;
     dto.UpdatedByUser = u.UserID;
  }

Это немного лучше, но все еще хрупкое. Если мы хотим передать другие типы объектов, нам все еще нужно больше операторов case. и т.д.

// Third pass pass - also brittle
void SubmitToWorkflow(DTOBase dto, User u)
{
  dto.LastUpdated = DateTime.UtcNow;
  dto.UpdatedByUser = u.UserID;

Это все еще хрупко, и оба метода накладывают ограничение, что все DTO должны реализовать это свойство, которое мы указали, не было универсально применимым. Некоторые разработчики могут испытывать желание написать бесполезные методы, но это плохо пахнет. Мы не хотим, чтобы классы делали вид, что они поддерживают отслеживание обновлений, но не делаем.

Интерфейсы, как они могут помочь?

Если мы определим очень простой интерфейс:

public interface IUpdateTracked
{
  DateTime LastUpdated { get; set; }
  int UpdatedByUser { get; set; }
}

Интерфейс может реализовать любой класс, которому требуется это автоматическое отслеживание обновлений.

public class SomeDTO : IUpdateTracked
{
  // IUpdateTracked implementation as well as other methods for SomeDTO
}

Можно сделать метод рабочего процесса намного более общим, меньшим и более обслуживаемым, и он будет продолжать работать независимо от того, сколько классов реализует интерфейс (DTO или другие), потому что он имеет дело только с интерфейсом.

void SubmitToWorkflow(object o, User u)
{
  IUpdateTracked updateTracked = o as IUpdateTracked;
  if (updateTracked != null)
  {
     updateTracked.LastUpdated = DateTime.UtcNow;
     updateTracked.UpdatedByUser = u.UserID;
  }
  // ...
  • Мы можем отметить, что изменение void SubmitToWorkflow(IUpdateTracked updateTracked, User u) будет гарантировать безопасность типов, однако в этих обстоятельствах оно не выглядит уместным.

В некотором производственном коде, который мы используем, у нас есть генерация кода для создания этих классов DTO из определения базы данных. Единственное, что делает разработчик, - это правильно создает имя поля и украшает класс интерфейсом. Пока свойства называются LastUpdated и UpdatedByUser, они просто работают.

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

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

// Using an interface to bridge properties
public class LegacyDTO : IUpdateTracked
{
    public int LegacyUserID { get; set; }
    public DateTime LastSaved { get; set; }

    public int UpdatedByUser
    {
        get { return LegacyUserID; }
        set { LegacyUserID = value; }
    }
    public DateTime LastUpdated
    {
        get { return LastSaved; }
        set { LastSaved = value; }
    }
}

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

Это означает, что свойства IUpdateTracked будут видны только тогда, когда мы используем ссылку на IUpdateTracked. Обратите внимание, что в объявлении нет открытого модификатора, и объявление содержит имя интерфейса.

// Explicit implementation of an interface
public class YetAnotherObject : IUpdatable
{
    int IUpdatable.UpdatedByUser
    { ... }
    DateTime IUpdatable.LastUpdated
    { ... }

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

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

Как я упоминал ранее и другими респондентами, вы можете создавать методы, которые принимают и / или возвращают ссылки на интерфейсы, а не на конкретную ссылку на класс. Если бы мне нужно было найти дубликаты в списке, я мог бы написать метод, который принимает и возвращает IList (интерфейс, определяющий операции, работающие со списками), и я не ограничен конкретным классом коллекции.

// Decouples the caller and the code as both
// operate only on IList, and are free to swap
// out the concrete collection.
public IList<T> FindDuplicates( IList<T> list )
{
    var duplicates = new List<T>()
    // TODO - write some code to detect duplicate items
    return duplicates;
}

Предупреждение о версии

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

См. этот взломанный пост для хорошего обсуждения.

Интерфейсы против абстрактных (базовых) классов

Абстрактные классы могут обеспечивать реализацию, а интерфейсы - нет. Абстрактные классы в некоторой степени более гибки в аспекте управления версиями, если вы следуете некоторым рекомендациям, таким как шаблон NVPI (Non-Virtual Public Interface).

Стоит повторить, что в .NET класс может наследовать только от одного класса, но класс может реализовывать столько интерфейсов, сколько ему захочется.

Внедрение зависимостей

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

class AnnualRaiseAdjuster
   : ISalaryAdjuster
{
   AnnualRaiseAdjuster(IPayGradeDetermination payGradeDetermination) { ...  }

   void AdjustSalary(Staff s)
   {
      var payGrade = payGradeDetermination.Determine(s);
      s.Salary = s.Salary * 1.01 + payGrade.Bonus;
   }
}

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

Это не учебник для начинающих, хотя есть целые книги, посвященные этой теме; приведенный выше пример очень упрощен.

6 голосов
/ 23 сентября 2008

Это довольно «длинный» предмет, но позвольте мне сказать проще.

Интерфейс - как "они его называют" - контракт. Но забудь об этом слове.

Лучший способ понять их - это какой-то пример псевдокода. Вот как я понял их давным-давно.

Предположим, у вас есть приложение, которое обрабатывает сообщения. Сообщение содержит некоторые вещи, такие как тема, текст и т. Д.

Итак, вы пишете свой MessageController для чтения базы данных и извлечения сообщений. Это очень приятно, пока вы вдруг не услышите, что факсы также будут внедрены в ближайшее время. Теперь вам придется читать «Факсы» и обрабатывать их как сообщения!

Это может легко превратиться в код Спагетти. Таким образом, вместо того, чтобы использовать MessageController, который управляет только «сообщениями», вы можете работать с интерфейсом , называемым IMessage (I - обычное использование, но не обязательное).

Ваш интерфейс IMessage, содержит некоторые основные данные, которые вам необходимы , чтобы убедиться, что вы можете обрабатывать Сообщение как таковое.

Поэтому, когда вы создаете свои классы EMail, Fax, PhoneCall, вы делаете их Реализация Интерфейс с именем IMessage .

Итак, в вашем MessageController у вас может быть метод, который называется так:

private void ProcessMessage(IMessage oneMessage)
{
    DoSomething();
}

Если бы вы не использовали интерфейсы, вам понадобилось бы:

private void ProcessEmail(Email someEmail);
private void ProcessFax(Fax someFax);
etc.

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

Почему или как ?

Поскольку интерфейсом является контракт , который определяет некоторые вещи, которые вы должны придерживаться (или внедрять), чтобы иметь возможность использовать его. Думайте об этом как значок . Если ваш объект «Факс» не имеет интерфейса IMessage, то ваш метод ProcessMessage не сможет с этим работать, он даст вам недопустимый тип, потому что вы передаете факс в метод, который ожидает IMessage объект.

Ты видишь смысл?

Думайте об интерфейсе как о «подмножестве» методов и свойств, которые вам будут доступны, несмотря на реальный тип объекта. Если исходный объект (факс, электронная почта, PhoneCall и т. Д.) Реализует этот интерфейс, вы можете безопасно передать его через методы, которым необходим этот интерфейс.

Там скрыто больше магии, вы можете ОТКЛОНИТЬ интерфейсы обратно к их исходным объектам:

Fax myFax = (Fax) SomeIMessageThatIReceive;

ArrayList () в .NET 1.1 имел приятный интерфейс под названием IList. Если у вас есть IList (очень «универсальный»), вы можете преобразовать его в ArrayList:

ArrayList ar = (ArrayList)SomeIList;

А в дикой природе тысячи образцов.

Интерфейсы, такие как ISortable, IComparable и т. Д., Определяют методы и свойства, которые должны реализовать в вашем классе для достижения этой функциональности.

Чтобы расширить наш образец, у вас может быть список <> электронных писем, факсов, телефонных звонков, все в одном списке, если типом является IMessage, но вы не можете собрать их все вместе, если объекты были просто электронной почтой, Факс и т. Д.

Если вы хотите отсортировать (или перечислить, например) ваши объекты, вам нужно, чтобы они реализовали соответствующий интерфейс. В примере .NET, если у вас есть список объектов «Факс» и вы хотите иметь возможность сортировать их с помощью MyList.Sort (), вам нужно , чтобы сделать ваш факс класс как это:

public class Fax : ISorteable 
{
   //implement the ISorteable stuff here.
}

Надеюсь, это подсказывает вам. Другие пользователи могут публиковать другие хорошие примеры. Удачи! и обними силу интерфейса.

предупреждение : Не все хорошо с интерфейсами, есть некоторые проблемы с ними, пуристы ООП начнут войну с этим. Я останусь в стороне. Один из недостатков Interfce (по крайней мере, в .NET 2.0) заключается в том, что у вас не может быть участников PRIVATE, или они не защищены, он должен быть открытым. Это имеет некоторый смысл, но иногда вы хотите просто объявить вещи частными или защищенными.

5 голосов
/ 23 сентября 2008

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

Кодовая база с хорошо продуманными интерфейсами внезапно становится намного проще для обсуждения. «Да, вам нужен CredentialsManager для регистрации новых удаленных серверов». «Передайте PropertyMap в ThingFactory, чтобы получить рабочий экземпляр.»

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

4 голосов
/ 23 сентября 2008

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

interface IReport
{
    string RenderReport();
}

class MyNewReport : IReport
{
    public string RenderReport()
    {
        return "Hello World Report!";

    }
}

class AnotherReport : IReport
{
    public string RenderReport()
    {
        return "Another Report!";

    }
}

//This class can process any report that implements IReport!
class ReportEmailer()
{
     public void EmailReport(IReport report)
     {
         Email(report.RenderReport());
     }
}

class MyApp()
{
    void Main()
    {
        //create specific "MyNewReport" report using interface
        IReport newReport = new MyNewReport();

        //create specific "AnotherReport" report using interface
        IReport anotherReport = new AnotherReport();

        ReportEmailer reportEmailer = new ReportEmailer();

        //emailer expects interface
        reportEmailer.EmailReport(newReport);
        reportEmailer.EmailReport(anotherReport);



    }

}
4 голосов
/ 23 сентября 2008

Интерфейсы также являются ключом к полиморфизму, одному из «ТРИ ОПОРЫ ООД».

Некоторые люди касались этого выше, полиморфизм просто означает, что данный класс может принимать различные «формы». Это означает, что если у нас есть два класса «Dog» и «Cat» и оба реализуют интерфейс «INeedFreshFoodAndWater» (хе-хе) - ваш код может сделать что-то вроде этого (псевдокод):

INeedFreshFoodAndWater[] array = new INeedFreshFoodAndWater[];
array.Add(new Dog());
array.Add(new Cat());

foreach(INeedFreshFoodAndWater item in array)
{
   item.Feed();
   item.Water();
}

Это мощно, потому что позволяет вам абстрактно обрабатывать различные классы объектов и позволяет делать такие вещи, как сделать ваши объекты более слабо связанными и т. Д.

3 голосов
/ 23 сентября 2008

ОК, так что речь идет об абстрактных классах и интерфейсах ...

Концептуально, абстрактные классы предназначены для использования в качестве базовых классов. Довольно часто они сами уже предоставляют некоторую базовую функциональность, и подклассы должны обеспечивать собственную реализацию абстрактных методов (это методы, которые не реализованы в абстрактном базовом классе).

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

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

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