Pull to refresh
1060.84
OTUS
Цифровые навыки от ведущих экспертов

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

Reading time10 min
Views32K
Original author: Ng Cheehou

Предисловие

Интерфейсы — одна из самых важных фич в 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");
    }
}

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

  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 {
    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?

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

  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.


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

Tags:
Hubs:
Total votes 17: ↑9 and ↓8+1
Comments26

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS