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