
Предисловие
Интерфейсы — одна из самых важных фич в 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) { Random random = new Random(); executive.KPI = random.Next(40, 100); return 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 {executive.Name} with KPI {executive.KPI} has been terminated because of KPI below 70"); } public void TerminateManager(Manager manager) { Console.WriteLine($ "Employee {manager.Name} with KPI {manager.KPI} has been terminated because of KPI below 70"); } }
Обратите внимание, что:
-
Хоть классы
ManagerиCSuiteимеют методEvaluateSubordinate, они принимают разные типы аргументов. -
CSuiteимеет две функцииTerminateExecutive, которые принимают различные типы аргументов.
Допустим мы хотим сгруппировать все инстансы этих классов в один список и проитерировать по всем сотрудникам, чтобы выполнить следующие задачи:
-
Отобразить их имя и должность.
-
Соответствующий руководитель оценит их KPI.
-
Топ-менеджер уволит тех, у кого 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 { IEvaluatedEmployee EvaluateSubordinate(IEvaluatedEmployee employee); }
Наконец, мы знаем, что только топ-менеджер имеет право увольнять сотрудников, поэтому я создал интерфейс ICSuite_Privilege с функцией увольнения сотрудников.
public interface ICSuite_Privilege: IEmployee { bool TerminateEmployee(IEvaluatedEmployee executive); }
Я закончил с интерфейсами, так что давайте теперь посмотрим, как наши классы будут их реализовывать.
Для класса 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 {evemp.Name} with KPI {evemp.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($ "{employee.Name} is a {employee.Designation}"); }
Преимущества использования интерфейсов не ограничивается на возможности группировки взаимосвязанных классов. Они также дают нам гибкость при написании функций. Давайте вернемся к функцию для увольнения сотрудников топ-менеджером. Нам достаточно написать только одну функцию TerminateEmployee, которая принимает аргументы с типом IEvaluatedEmployee (который реализуют Executive и Manager), вместо двух функций для удаления Executive и Manager соответственно.
public bool TerminateEmployee(IEvaluatedEmployee evemp) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($ "Employee {evemp.Name} with KPI {evemp.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?
Потому, что в нашем случае:
-
CSuiteтакже реализуетIEmployee, -
но сам
CSuiteникем не оценивается, -
а по какому показателю будет оцениваться сотрудник? В нашем случае это 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: Анализ воздействия
И, скажем, в будущем, председатель правления компании представит новую политику для сотрудников
-
Топ-менеджеры тоже подлежат оценке
-
Введет еще два параметра (a и b) для оценки
Поскольку мой пример — это небольшая программа, я могу изменить свой код за пару секунд без риска возникновения каких-либо ошибок. Однако в реальной жизни система может быть огромной и очень сложной, и без интерфейсов очень сложно поддерживать или применять подобные изменения.
В целях демонстрации, чтобы реализовать новую политику двух компаний, я
-
Заставлю класс
CSuiteреализовывать интерфейсIEvaluatedEmployee
public class CSuite : IManagementLevelEmployee, ICSuite_Privilege, IEvaluatedEmployee
-
Введу еще два параметра
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.
Чем отличается объектно-ориентированное программирование от программирования на основе абстрактных типов данных? Приглашаем всех желающих на бесплатное открытое занятие, на котором разберем: что такое наследование, критерий правильного его применения и примеры ошибочного применения наследования. Регистрация — по ссылке.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/674756/
Добавить комментарий