Понимание интерфейсов - PullRequest
16 голосов
/ 25 июня 2009

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

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

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

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

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

Я также не понимаю, что вы можете сделать как ссылку на объект, подобный этому:

ICalculator myInterface = new JustSomeClass();

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

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

Как, например, этот пример:

public AuthenticationController(IFormsAuthentication formsAuth)
{
    FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();
}

public class FormsAuthenticationWrapper : IFormsAuthentication
{
    public void SetAuthCookie(string userName, bool createPersistentCookie)
    {
        FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
    }
    public void SignOut()
    {
        FormsAuthentication.SignOut();
    }
}

public IFormsAuthentication FormsAuth
{
    get;
    set;
}

Например, зачем создавать этот интерфейс? Почему бы просто не создать FormsAuthenticationWrapper с методами в нем и вызвать его за день? Зачем сначала создавать интерфейс, затем Wrapper должен реализовывать интерфейс, а затем, наконец, писать методы?

Тогда я не понимаю, что на самом деле говорится в утверждении.

Как теперь я знаю, что в заявлении говорится это

FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();

если formsAuth равен нулю, создайте новый FormsAuthenticationWrapper и затем назначьте его свойству, которое является интерфейсом.

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

Тогда в тестовом файле они имеют:

var formsAuthenticationMock = new Mock<AuthenticationController.IFormsAuthentication>();

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

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

Разве это не просто делает заглушки для вас?

Ответы [ 9 ]

17 голосов
/ 25 июня 2009

Хорошо, сначала мне тоже было трудно понять, так что не беспокойтесь об этом.

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

public class Character
{
}

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

interface IWeapon
{
    public Use();
}

Итак, давайте дадим персонажу оружие:

public class Character
{
    IWeapon weapon;

    public void GiveWeapon(IWeapon weapon)
    {
        this.weapon = weapon;
    }

    public void UseWeapon()
    {
        weapon.Use();
    }
}

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

public class Gun : IWeapon
{
    public void Use()
    {
        Console.Writeline("Weapon Fired");
    }
}

Тогда вы можете соединить это:

Character bob = new character();
Gun pistol = new Gun();
bob.GiveWeapon(pistol);
bob.UseWeapon();

Теперь это общий пример, но он дает много силы. Вы можете прочитать об этом больше, если вы посмотрите на шаблон стратегии.

14 голосов
/ 25 июня 2009

Интерфейсы определяют контракты .

В приведенном вами примере оператор ?? просто предоставляет значение по умолчанию, если вы передаете null конструктору и не имеет никакого отношения к интерфейсам.

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

В этом отношении интерфейсы во многом похожи на обычное наследование с одним важным отличием. В C # класс может наследовать только от одного типа, но может реализовывать много интерфейсов. Таким образом, интерфейсы позволяют выполнять несколько контрактов в одном классе. Объект может быть объектом IFormsAuthentication и также может быть чем-то другим, например IEnumerable.

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

void OutputValues(string[] values)
{
   foreach (string value in values)
   {
       Console.Writeline(value);
   }
}

Принимает массив и выводит его на консоль. Теперь примените это простое изменение, чтобы использовать интерфейс:

void OutputValues(IEnumerable<string> values)
{
   foreach (string value in values)
   {
       Console.Writeline(value);
   }
}

Этот код still принимает массив и выводит его на консоль. Но также требуется List<string> или что-то еще, что вам нужно, чтобы дать ему то, что реализует IEnumerable<string>. Итак, мы взяли интерфейс и использовали его, чтобы сделать простой блок кода намного более мощным.

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

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

10 голосов
/ 25 июня 2009

Возможно, лучший способ получить хорошее представление об интерфейсах - это использовать пример из .NET Framework.

Рассмотрим следующую функцию:

void printValues(IEnumerable sequence)
{
    foreach (var value in sequence)
        Console.WriteLine(value);
}

Теперь я мог бы написать эту функцию для принятия List<T>, object[] или любого другого типа конкретной последовательности. Но поскольку я написал эту функцию для приема параметра типа IEnumerable, это означает, что я могу передать в эту функцию любой конкретный тип, реализующий интерфейс IEnumerable.

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

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

5 голосов
/ 25 июня 2009

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

У класса есть методы и свойства, и каждый из методов и свойств класса имеет сигнатуру и тело

public int Add(int x, int y)
{
return x + y;
}

Подпись метода Add - это все, что находится перед первым символом фигурных скобок

public int Add(int x, int y)

Цель сигнатуры метода - присвоить имя методу, а также описать его уровень защиты (открытый, защищенный, внутренний, частный и / или виртуальный), который определяет, откуда к методу можно получить доступ в коде

Сигнатура также определяет тип значения, возвращаемого методом, метод Add выше возвращает int и аргументы, которые метод ожидает передать ему вызывающие

Методы, как правило, рассматриваются как нечто, что может сделать объект, приведенный выше пример подразумевает, что класс, определенный методом, работает с числами

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

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

Другими словами, интерфейс может описывать действия (методы) класса, но он никогда не должен описывать, как должно выполняться действие.

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

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

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

Представьте, что вы являетесь автором очень популярного веб-браузера, которым пользуются миллионы людей каждый день, и у вас есть тысячи запросов о функциях от людей, некоторые большие, маленькие, хорошие и такие, как "вернуть <maquee> и <blink> поддержка ".

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

Итак, вы решаете разрешить пользователям разрабатывать свои собственные плагины, чтобы они могли <blink 'пока коровы не вернутся домой.

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

public class Plugin
{
public void Run (PluginHost browser)
{
//do stuff here....
}
}

Но как вы могли бы разумно реализовать этот метод? Вы не можете точно знать, как будет работать каждый возможный будущий плагин

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

public interface IPlugin
{
void Run(PluginHost browser);
}

public class PluginHost
{
public void RunPlugins (IPlugin[] plugins)
{
foreach plugin in plugins
{
plugin.Run(this);
}
}
}

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

Чтобы продемонстрировать аспект отношения между классом и интерфейсом «может быть», я напишу плагин для хоста плагинов, который реализует тег <blink>.

public class BlinkPlugin: IPlugin
{
private void MakeTextBlink(string text)
{
//code to make text blink.
}
public void Run(PluginHost browser)
{
MakeTextBlink(browser.CurrentPage.ParsedHtml);
}
}

С этой точки зрения вы можете видеть, что плагин определен в классе с именем BlinkPlugin, но поскольку он также реализует интерфейс IPlugin, на него также можно ссылаться как на объект IPlugin, как это делает класс PluginHost выше, потому что он не знать или заботиться о том, какой тип класса на самом деле, просто это может быть IPlugin

Надеюсь, это помогло, я действительно не собирался быть таким длинным.

2 голосов
/ 25 июня 2009

Ниже приведу пример, но позвольте мне начать с одного из ваших утверждений. «Я не знаю, как определить, когда использовать один». поставить его на край. Вам не нужно определять, когда его использовать, а когда не использовать. Любой параметр (по крайней мере, для открытых методов), любое (публичное) свойство (и лично я бы фактически расширил список до всего остального) должно быть объявлено как что-то вроде интерфейса, а не определенного класса. Единственный раз, когда я объявлял что-то определенного типа, это когда не было подходящего интерфейса.

Я бы пошел

IEnumerable<T> sequence;

когда я могу и вряд ли когда-либо (единственный случай, о котором я могу подумать, - действительно ли мне нужен метод ForEach)

List<T> sequence;

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

Цены на автомобили определяются на основе набора веб-сайтов, а цены на компьютеры - на основе набора услуг.

решение может быть: создайте одну веб-страницу, скажем, с сеткой данных и внедрением зависимостей IDataRetriever (где IDataRetriver - это некоторый интерфейс, обеспечивающий возможность извлечения данных без необходимости знать, откуда поступили данные (БД, XML, веб-службы или ...) или как они были извлечены (добыча данных, SQL-запросы в домашних данных или чтение файл).

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

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

1 голос
/ 25 июня 2009

Почти все, что написано об интерфейсах, позвольте мне попробовать.

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

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

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

например.

Интерфейс LivingBeings

- speak () // говорит любой, кто IS живой человек должен определить, как он говорит

собака класса реализует живые существа

- speak () {bark;} // реализация говорить как собака

птица класса реализует живые существа

- speak () {chirp;} // реализация говорить как птица

1 голос
/ 25 июня 2009

Как я понял, это было:

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

Итак, интерфейс - это отношение «может быть», например, собака может быть собакой-шпионом, собака может быть собакой-цирком и т. Д. Но чтобы достичь этого, собака должна выучить некоторые конкретные вещи. Что в терминологии ОО означает, что ваш класс должен иметь возможность делать некоторые конкретные вещи (контракт, как они его называют), если он реализует интерфейс. например, если ваш класс реализует IEnumerable, это ясно означает, что ваш класс имеет (должен иметь) такую ​​функциональность, что его объекты можно перечислять.

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

1 голос
/ 25 июня 2009

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

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

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

0 голосов
/ 25 июня 2009
ICalculator myInterface = new JustSomeClass();
JustSomeClass myObject = (JustSomeClass) myInterface;

Теперь у вас есть оба «интерфейса» для работы с объектом.

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

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

Надеюсь, это поможет.

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