Зачем нужны интерфейсы c
Перейти к содержимому

Зачем нужны интерфейсы c

  • автор:

Зачем придумали интерфейсы?

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

interface IMovable

Реализация в классе:

// применение интерфейса в классе class Person : IMovable < public void Move() < Console.WriteLine("Человек идет"); >> // применение интерфейса в структуре struct Car : IMovable < public void Move() < Console.WriteLine("Машина едет"); >> 

Но если я просто удалю интерфейс и сделаю вот так

class Person < public void Move() < Console.WriteLine("Человек идет"); >> 

Код по прежнему будет работать.
Вот и вопрос зачем все это нужно?
Я просто не могу представить себе ситуацию где бы было необходимо использовать интерфейсы.
В C#8 добавили реализацию метода по умолчанию в интерфейсе тем самым сделав из интерфейса недо-класс.
Смотрю видео на ютубе и там все используют интерфейсы но не говорят зачем, просто как будто так и надо.
Не спроста же все их используют? Но без понимания зачем все это нужно у меня просто не получается найти смысл их применения и понять как работает чужой код когда в нем есть интерфейсы.
Единственный смысл в них вижу в том что не нужно писать документацию к классам, просто наследуем от интерфейса а дальше пусть уже другой программист лезет в код и смотрит чего там и как.
Ни в коем случае не хочу сказать что я тут самый умный а все остальные ошибались, просто хочу разобраться почему придумали интерфейсы если и без них все хорошо работает?
Извините что так сумбурно написал, сам на эмоциях, неделю уже читаю статьи по теме и ни как не могу осилить.

Отслеживать
4,952 1 1 золотой знак 8 8 серебряных знаков 27 27 бронзовых знаков
задан 17 янв 2020 в 23:07
User12351259599491 User12351259599491
272 4 4 серебряных знака 17 17 бронзовых знаков
+1 за желание разобраться.
– user176262
17 янв 2020 в 23:11

Дополню небольшим примером из видео о том как сделать парсер сайта: Человек создает интерфейс с разными полями в частности там есть поле string BaseUrl < get; set; >и в классе он пишет реализацию public string BaseUrl < get; set; >= «https://habrahabr.ru»; У меня просто в этот момент происходит крик в голове «Зачем ты создал этот интерфейс если все равно в классе ты прописал тоже самое. » Это же просто лишний код, который будет путать людей если они не очень хорошо знают c#.

17 янв 2020 в 23:45

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

17 янв 2020 в 23:46
см. видео 1 и 2
18 янв 2020 в 0:06

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

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

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

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

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

В такой ситуации вам поможет интерфейс. Например, у вас есть класс, который вам надо сохранить.

public class MyGameState < //. my game data >

Напишем для него интерфейс сохранения

public interface IGameStore

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

Другими словами, интерфейс помогает определять взаимодействие между модулями вашей программы, даже если эти модули ещё не существуют.

Но тут вы можете возразить мне и сказать — что вы с таким же успехом можете определить класс для сохранения, например

public absract class GameStore

Да, это верно, вы можете определить абстрактный класс. В таком случае, для реализации сохранения, вам надо будет унаследоваться от абстрактрого класса и реализовать нужные методы. Но давайте подумаем, какие ограничения это накладывает на наш класс сохранения? А вот какие — наш класс сохранения должен будет быть унаследован именно от указанного абстрактного класса и больше ни от кого. Вы можете возразить — мол, в случае интерфейса будет то же самое — ведь класс сохранения должен его реализовать. Но вот и нет — в C# реализация интерфейса никак не ограничивает класс, так как класс может реализовать сколько угодно интерфейсов. А вот наследование от абстрактного класса — уже дело другое, ведь вы не можете насловаться от 2 классов. Отсюда вторая причина использования интерфйсов — Использование интерфйсов позволяет обойти ограничения множественного наследования.

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

public absract class GameStore < protected string PlayerNamepublic GameStore(string playerName) < PlayerName = playerName; >public abstract void SaveGame(MyGameState state); public abstract MyGameState Load(); > 

Теперь посмотрите что произошло. Мы не только заставляем классы для сохранения игры наследоваться от нашего абстрактного класса, но ещё и накладываем на них определенные огреничения. Интересны ли эти ограничения остальной части игры? Думаю, нет. В таком случае, зачем остальной части игры знать об этих огреничениях? Нет никаких причин для этого. Таким образом, Используя интерфейс, вы декларируете желаемое поведение. Используя класс — вы декларируете детали реализации. То есть, когда у вас был интерфейс в игре, вы декларировали, что «не важно что тут будет за объект, не важно как он будет написан, но мне надо чтобы он мог сохранять и загружать состояние игры» — то есть вам было важно, что объект может делать, при этом не важно, как он это делает. В этом суть инкапсуляции. Когда же вы используете класс, особенно если класс содержит какие либо посторонние детали, вы уже покажываете, что вам важно не только что объект может делать, но и также кем объект является и какие у объекта есть детали реализации.

А теперь давайте посмотрим на это с точки зрения игры. Игре, на самом деле, не интересно знать, как и куда будет происходить сохранение. Игре не интересны детали сохранения. Игре не интересен даже факт — сущестует ли уже написанный класс сохранения. Все, что игре надо — это получить объект, который содержит нужные игре методы. Всё. При таком подходе связь между игрой и реальным классом сохранения лежит только через интерфейс, что является связью гораздо более слабой. Сам класс может быть написан как угодно, содержать какие угодно огранчения или детали реализации, эти ограничения и детали могут меняться и быть переписаны любое количество раз и все будет работать, пока класс сохранения реализует интерфейс. Таким образом, связь классов через интерфейс является более слабой связью, чем связь классов через абстрактный класс или напрямую, откуда следует выводы:

  1. Если вы хотите держать свои модули слабо связанными (а вы должны хотеть), используйте для связи интерфейсы
  2. При использовании интерфейов для связи, на них ложится бремя совместимости — интерфейсы должны меняться как можно реже
  3. Так как интерфейсы должны меняться как можно реже, написание интерфейса становится задачай более ответственной, чем написание класса. Класс, закрытый интерфейсом, всегда можно переписать. Интерфейс же, используемый во многих компонентах, переписать возможно не всегда, при этом есть большой риск потери обратной совместимости с уже написанным до этого кодом.

Programming stuff

В чем сила интерфейсов? Сила интерфейсов в правде слабости системы типов!

Принято считать, что интерфейсы предназначены для моделирования абстракций и обеспечения слабой связанности (loose coupling). Звучит умно, но что это значит?

Что дают интерфейсы?

Наследование моделирует семейство объектов с общим поведением. Интерфейс и абстрактный класс определяет «протокол» этого семейства, а наследники определяют конкретную реализацию. Разные виды предусловий можно спрятать за интерфейсом IPrecondition, разные виды стратегий сортировки – за ISorter, разные виды импортеров/экспортеров – за Importer/Exporter.

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

Существуют разные уровни абстрагирования или игнорирования деталей. Домашний кондиционер является разновидностью «устройства управления климатом», но конкретный кондиционер, установленный лично в вашем доме тоже является абстракцией – он прячет от вас свои детали реализации.

Конкретный класс «абстрактен» — его поведение определяется его открытым интерфейсом, а детали реализации скрыты. Будет ли класс реализовывать интерфейс или нет, определяется не требованием «абстрактности», а моделью. Если в вашем «мире» нужно смотреть на конкретный экземпляр кондиционера через призму охладительным приборов, то интерфейс нужен, в противном случае – нет.

Когда класс должен реализовывать интерфейс?

1. Класс реализует стратегию или является частью семейства объектов: IRepository, IFormatter, IPrecondition.

2. Класс реализует ролевой интерфейс (следствие ISP): ICloneable, IComparable etc.

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

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

Когда класс должен принимать интерфейс в качестве зависимости?

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

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

Класс работает с семейством типов. «Семейство типов» уже существует и определяется требованиями текущей модели, а не воображением разработчика.

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

Для обеспечения тестируемости. Полезно, только если реализация «абстракции» завязана на внешнее окружение. Даже в этом случае можно обойтись Шаблонным Методом: выделить общение с внешним ресурсом не во внешнюю зависимость, а в виртуальный метод и переопределить его в тесте.

Выделять интерфейсы «на всякий случай» — не нужно! Если это библиотечный код, то интерфейсы там полное зло: любое их изменение – ломает всех клиентов. А если это продакшн код, то он все равно будет меняться, и наличие лишних интерфейсов лишь сделает этот процесс более сложным.

Интерфейсы

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

Определение интерфейса

Для определения интерфейса используется ключевое слово interface . Как правило, названия интерфейсов в C# начинаются с заглавной буквы I , например, IComparable, IEnumerable (так называемая венгерская нотация), однако это не обязательное требование, а больше стиль программирования.

Что может определять интерфейс? В целом интерфейсы могут определять следующие сущности:

  • Методы
  • Свойства
  • Индексаторы
  • События
  • Статические поля и константы (начиная с версии C# 8.0)

Однако интерфейсы не могут определять нестатические переменные. Например, простейший интерфейс, который определяет все эти компоненты:

interface IMovable < // константа const int minSpeed = 0; // минимальная скорость // статическая переменная static int maxSpeed = 60; // максимальная скорость // метод void Move(); // движение // свойство string Name < get; set; >// название delegate void MoveHandler(string message); // определение делегата для события // событие event MoveHandler MoveEvent; // событие движения >

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

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

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

Модификаторы доступа

Еще один момент в объявлении интерфейса: если его члены — методы и свойства не имеют модификаторов доступа, то фактически по умолчанию доступ public , так как цель интерфейса — определение функционала для реализации его классом. Это касается также и констант и статических переменных, которые в классах и структурах по умолчанию имееют модификатор private. В интерфейсах же они имеют по умолчанию модификатор public. И например, мы могли бы обратиться к константе minSpeed и переменной maxSpeed интерфейса IMovable:

Console.WriteLine(IMovable.maxSpeed); // 60 Console.WriteLine(IMovable.minSpeed); // 0

Но также, начиная с версии C# 8.0, мы можем явно указывать модификаторы доступа у компонентов интерфейса:

interface IMovable < public const int minSpeed = 0; // минимальная скорость private static int maxSpeed = 60; // максимальная скорость public void Move(); protected internal string Name < get; set; >// название public delegate void MoveHandler(string message); // определение делегата для события public event MoveHandler MoveEvent; // событие движения >

Как и классы, интерфейсы по умолчанию имеют уровень доступа internal , то есть такой интерфейс доступен только в рамках текущего проекта. Но с помощью модификатора public мы можем сделать интерфейс общедоступным:

public interface IMovable

Реализация по умолчанию

Также начиная с версии C# 8.0 интерфейсы поддерживают реализацию методов и свойств по умолчанию. Это значит, что мы можем определить в интерфейсах полноценные методы и свойства, которые имеют реализацию как в обычных классах и структурах. Например, определим реализацию метода Move по умолчанию:

interface IMovable < // реализация метода по умолчанию void Move() < Console.WriteLine("Walking"); >>

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

interface IMovable < // реализация метода по умолчанию void Move() =>Console.WriteLine("Walking"); // реализация свойства по умолчанию // свойство только для чтения int MaxSpeed < get < return 0; >> >

Стоит отметить, что если интерфейс имеет приватные методы и свойства (то есть с модификатором private), то они должны иметь реализацию по умолчанию. То же самое относится к статическим методам (не обязательно приватным):

Console.WriteLine(IMovable.MaxSpeed); // 60 IMovable.MaxSpeed = 65; Console.WriteLine(IMovable.MaxSpeed); // 65 double time = IMovable.GetTime(500, 10); Console.WriteLine(time); // 50 interface IMovable < public const int minSpeed = 0; // минимальная скорость private static int maxSpeed = 60; // максимальная скорость // находим время, за которое надо пройти расстояние distance со скоростью speed static double GetTime(double distance, double speed) =>distance / speed; static int MaxSpeed < get =>maxSpeed; set < if (value >0) maxSpeed = value; > > >

Добавление интерфейса

Стоит отметить, что в Visual Studio есть специальный компонент для добавления нового интерфейса в отдельном файле. Для добавления интерфейса в проект можно нажать правой кнопкой мыши на проект и в появившемся контекстном меню выбрать Add -> New Item. и в диалоговом окне добавления нового компонента выбрать пункт Interface :

Интерфейсы в C#

Хотя мы также может добавить стандартный файл класса или любой другой файл кода C# и в нем определить интерфейс.

Интерфейсы в C#: зачем они нужны?

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

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

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

Для иллюстрации мы используем типовой пример «Сотрудник в компании»:

Рассмотрим следующий сценарий: в компании есть 3 типа сотрудников: служащий (Executive), менеджер (Manager) и топ-менеджер (C-suite).

Служащий имеет имя (Name), должность (Designation) и ключевой показатель эффективности (KPI). Вот так может выглядеть класс Executive :

public class Executive < public string Name < get; set; >public string Designation < get; set; >public int KPI < get; set; >>

У менеджера все то же самое, что и у служащего, но у него еще есть дополнительные права оценивать сотрудников уровня “служащий”. Вот так может выглядеть класс Manager :

public string Name < get; set; >public string Designation < get; set; >public int KPI < get; set; >public Executive EvaluateSubordinate(Executive executive)

У топ-менеджеров есть имя и должность, но нет KPI. Мы предполагаем, что их KPI связаны с доходностью акций компании или другими показателями. Топ-менеджеры также имеют право оценивать только сотрудников уровня “менеджер”, и, кроме того, они имеют право уволить любого неэффективного сотрудника. Вот так может выглядеть класс CSuite :

public class CSuite < public string Name < get; set; >public string Designation < get; set; >public Manager EvaluateSubordinate(Manager manager) < Random random = new Random(); manager.KPI = random.Next(60, 100); return manager; >public void TerminateExecutive(Executive executive) < Console.WriteLine($ "Employee with KPI has been terminated because of KPI below 70"); > public void TerminateManager(Manager manager) < Console.WriteLine($ "Employee with KPI has been terminated because of KPI below 70"); > >

Обратите внимание, что:

  1. Хоть классы Manager и CSuite имеют метод EvaluateSubordinate , они принимают разные типы аргументов.
  2. CSuite имеет две функции TerminateExecutive , которые принимают различные типы аргументов.

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

  1. Отобразить их имя и должность.
  2. Соответствующий руководитель оценит их KPI.
  3. Топ-менеджер уволит тех, у кого KPI меньше 70.

И получить что-то вроде этого:

Для начала я инициализирую всех своих сотрудников:

Executive executive = new Executive() < Name = "Alice", Designation = "Programmer">; Manager manager = new Manager() < Name = "Bob", Designation = "Sales Manager">; CSuite cSuite = new CSuite() < Name = "Daisy", Designation = "CFO" >;

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

Польза от интерфейса № 1: Может выступать в роли обобщенного класса

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

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

public interface IEmployee < string Name < get; set; >string Designation < get; set; >>

Во-вторых, поскольку KPI должны оцениваться только для сотрудников классов Executive и Manager, я создал интерфейс IEvaluatedEmployee , который имеет только одно свойство: KPI. Обратите внимание, что мой интерфейс IEvaluatedEmployee также реализует интерфейс IEmployee . Это означает, что любой класс, реализующий этот интерфейс, также будет иметь свойства IEmployee (а именно имя и должность).

public interface IEvaluatedEmployee: IEmployee < int KPI < get; set; >> >

Я создал еще один интерфейс с именем IManagementLevelEmployee , который указывает, что этот интерфейс имеет право управлять людьми, т. е. в нашем примере оценивать сотрудников по их KPI.

public interface IManagementLevelEmployee: IEmployee

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

public interface ICSuite_Privilege: IEmployee

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

Для класса Executive :

public class Executive: IEvaluatedEmployee < public string Name < get; set; >public string Designation < get; set; >public int KPI < get; set; >>

Для класса Manager :

public class Manager: IManagementLevelEmployee, IEvaluatedEmployee < public string Name < get; set; >public string Designation < get; set; >public int KPI < get; set; >public IEvaluatedEmployee EvaluateSubordinate(IEvaluatedEmployee evaluatedemployee) < Random random = new Random(); evaluatedemployee.KPI = random.Next(40, 100); return evaluatedemployee; >>

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

Наконец, наш класс C-Suite :

public class CSuite: IManagementLevelEmployee, ICSuite_Privilege < public string Name < get; set; >public string Designation < get; set; >public IEvaluatedEmployee EvaluateSubordinate(IEvaluatedEmployee Manager) < Random random = new Random(); Manager.KPI = random.Next(60, 100); return Manager; >public bool TerminateEmployee(IEvaluatedEmployee evemp) < Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($ "Employee with KPI has been terminated because of KPI below 70"); Console.ForegroundColor = ConsoleColor.White; return true; > >

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

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

employees.Add(new Executive() < Name = "Alex", Designation = "Programmer" >); employees.Add(new Manager() < Name = "Bob", Designation = "Sales Manager" >); employees.Add(new CSuite() < Name = "Daisy", Designation = "CFO" >); #region Display Employees Info Console.WriteLine("-----Display Employee's Information-----"); foreach(IEmployee employee in employees) < DisplayEmployeeInfo(employee); >Console.WriteLine(); #endregion static void DisplayEmployeeInfo(IEmployee employee) < Console.WriteLine($ "is a "); >

Преимущества использования интерфейсов не ограничивается на возможности группировки взаимосвязанных классов. Они также дают нам гибкость при написании функций. Давайте вернемся к функции для увольнения сотрудников топ-менеджером. Нам достаточно написать только одну функцию TerminateEmployee , которая принимает аргументы с типом IEvaluatedEmployee (который реализуют Executive и Manager), вместо двух функций для удаления Executive и Manager соответственно.

public bool TerminateEmployee(IEvaluatedEmployee evemp) < Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($ "Employee with KPI has been terminated because of KPI below 70"); Console.ForegroundColor = ConsoleColor.White; return true; >

Польза от интерфейса № 2: Обязывающий “контакт” и расширение возможностей класса

Многие люди рассматривают интерфейсы как “контракты” в мире классов. Рассмотрим пример из реальной жизни: владелец обувной фабрики подписывает контракт с инвестором о том, что фабрика должна будет произведет 100 пар обуви в течении одного месяца. Фабрика ДОЛЖНА произвести 100 пар обуви для инвестора, и невыполнение этого обязательства повлечет за собой штраф.

public class CSuite : IManagementLevelEmployee, ICSuite_Privilege

Для примера, класс CSuite реализует два интерфейса: IManagementLevelEmployee и ICSuite_Privilege . Это “контракт”, который обязывает этот класс иметь все функции из этих двух интерфейсов (оценки и увольнения сотрудника). Если мы не позаботимся о их создании, то компилятор выдаст ошибку (аналог штрафа в реальном мире).

Скажем, в будущем будет введен новый класс под названием “Board” (правление/совет директоров). Мы сможем назначить аналогичные привилегии классу Board, чтобы гарантировать, что он будет иметь такие же полномочия, как и CSuite. Благодаря этому мы можем гарантировать, что все инстансы Board будут иметь функцию увольнения сотрудника. Эта фича дает программисту возможность быстро оценить назначение и ответственность каждого класса, просто посмотрев на интерфейсы, реализуемые ими.

public class Board : ICSuite_Privilege

Польза от интерфейса № 3: Множественное наследование для разделения ответственности

Вы могли заметить, что интерфейс IManagementLevelEmployee имеет функцию EvaluateSubordinate , и параметр, который он принимает, — IEvaluatedEmployee , а не IEmployee .

IEvaluatedEmployee EvaluateSubordinate(IEvaluatedEmployee employee);//почему IEvaluatedEmployee IEvaluatedEmployee EvaluateSubordinate(IEmployee employee);//вместо IEmployee?

Функция EvaluateSubordinate будет так же прекрасно работать, если она будет принимать IEmployee , поскольку классы Executive и Manager также реализуют IEmployee . Так почему вместо этого я использую IEvaluatedEmployee ?

Потому, что в нашем случае:

  1. CSuite также реализует IEmployee ,
  2. но сам CSuite никем не оценивается,
  3. а по какому показателю будет оцениваться сотрудник? В нашем случае это KPI.

Поэтому, чтобы удовлетворить всем требованиям, я создал интерфейс под названием IEvaluatedEmployee , у которого есть свойство KPI, и в то же время он реализует IEmployee . Таким образом, это означает, что кто бы ни реализовывал этот интерфейс (Executive и Manager), он будет иметь не только KPI, но и все свойства IEmployee (имя и должность).

public interface IEvaluatedEmployee: IEmployee < int KPI < get; set; >> public class Executive: IEvaluatedEmployee public class Manager: IManagementLevelEmployee, IEvaluatedEmployee public class CSuite: IManagementLevelEmployee, ICSuite_Privilege

Но класс CSuite не реализует интерфейс IEvaluatedEmployee . Поэтому мы можем предотвратить передачу в функцию EvaluateSubordinate инстанс класса CSuite , установив такое ограничение с помощью интерфейса. Это один из способов не нарушать нашу бизнес-логику нашим кодом.

Польза от интерфейса № 4: Анализ воздействия

И, скажем, в будущем, председатель правления компании представит новую политику для сотрудников

  1. Топ-менеджеры тоже подлежат оценке
  2. Введет еще два параметра (a и b) для оценки

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

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

  1. Заставлю класс CSuite реализовывать интерфейс IEvaluatedEmployee
public class CSuite : IManagementLevelEmployee, ICSuite_Privilege, IEvaluatedEmployee
  1. Введу еще два параметра a и b для EvaluateSubordinate в интерфейсе IManagementLevelEmployee
public interface IManagementLevelEmployee : IEmployee < IEvaluatedEmployee EvaluateSubordinate(IEvaluatedEmployee employee, string a, string b);//добавляем еще два параметра a и b >

Visual Studio немедленно выдаст мне предупреждение об ошибке, указывающее, что мой CSuite не реализует IEvaluatedEmployee (отсутствует свойство int KPI), а класс Manager и CSuite неправильно реализует интерфейс IManagementLevelEmployee (функция EvaluateSubordinate требует два новых параметра).

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

Польза от интерфейса № 5: Абстракция планирования

В реальной жизни, как правило, проект разрабатывается сразу несколькими разработчиками. И очень часто мы начинаем разработку еще до окончательного оформления бизнес-требований. Таким образом, с помощью интерфейсов ведущий программист или архитектор может стандартизировать функции каждого класса. Используя тот же пример, технический руководитель может не знать, какова точная бизнес-логика для оценки сотрудника, но с помощью интерфейса он/она может наглядно продемонстрировать для всех программистов, работающих с классами Manager или CSuite , что они должны содержать функцию EvaluateSubordinate , а также иметь имя и должность для каждого сотрудника и т. д., указав это в интерфейсе.

Польза от интерфейса № 6: Модульное тестирование

И последнее, но не менее важное (хотя на самом деле это может быть одной из самых важных причин для реализации интерфейса) — модульное тестирование. Эта польза очень похожа на пользу № 5, поскольку многие компании применяют методологию Test Driven Development (TDD) в процессе разработки своего программного обеспечения, поэтому на этапе планирования оглядка на модульное тестирование очень важна до фактического начала разработки.

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

public class Executive: IEvaluatedEmployee < public string Name < get; set; >public string Designation < get; set; >public int KPI < get; set; >public bool CheckEmployee() < //давайте сделаем вид, что здесь мы делаем вызов к базе данныхe return false; >>

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

[TestMethod] public void TestMethod1() < Mock < IEvaluatedEmployee >EvEmp = new Mock < IEvaluatedEmployee >(); EvEmp.Setup(x => x.CheckEmployee()).Returns(true); CSuite obje = new CSuite(); Assert.AreEqual(obje.TerminateEmployee(EvEmp.Object), true); >

Мы использовали интерфейс IEvaluatedEmployee , чтобы мокнуть класс Executive , чтобы функция CheckEmployee всегда возвращала значение true .

Если у нас не реализован интерфейс и мы используем реальный класс Executive для создания моков, Visual Studio выдаст ошибку. Это связано с тем, что система не может переопределить функцию конкретного класса, чтобы она всегда возвращала либо true , либо false .

Заключение

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

Чем отличается объектно-ориентированное программирование от программирования на основе абстрактных типов данных? Приглашаем всех желающих на бесплатное открытое занятие, на котором разберем: что такое наследование, критерий правильного его применения и примеры ошибочного применения наследования. Регистрация — по ссылке.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *