Предисловие

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


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

Комментарии (19)


  1. dyadyaSerezha
    03.07.2022 13:05
    +3

    1. Почти все приведённые аргументы относятся к базовым простым и базовым абстрактным классам.

    2. Не показано самого главного - когда применять базовые классы, а когда интерфейсы.


    1. plus
      03.07.2022 14:07
      +1

      Про инверсию зависимостей я тоже не увидел


      1. dyadyaSerezha
        03.07.2022 20:25
        -2

        И инверсия тоже прекрасно делается через классы.


        1. Wolfdp
          04.07.2022 09:15

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


          1. dyadyaSerezha
            04.07.2022 19:02

            Объет может создаваться как угодно всегда.

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


            1. questor
              04.07.2022 23:47

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


        1. utkaka
          04.07.2022 10:28

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


          1. Druj
            04.07.2022 14:16

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


            1. utkaka
              04.07.2022 14:30

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


            1. vabka
              04.07.2022 14:47

              Это значит что у вас проблемы с архитектурой

              Не обязательно.
              Например у DTO-шек вполне может быть много интерфейсов, что удобно для написания обобщённых linq-запросов.


            1. playermet
              04.07.2022 21:58
              +2

              Это значит что у вас проблемы с архитектурой.

              Нет, не значит. Это тотально распростаненный случай, когда разные компоненты требуют чтобы объект который они принимают имел нужный им интерфейс. Можно открыть библиотеку NET, и увидеть что почти все его классы имеют более одного интерфейса. У банального Int32 их пять штук. Через один интерфейс объект может сравниваться для сортировки, через второй считать хеш, через другие итерироваться, клонироваться, сериализоваться, и т.д. и т.п, всяких I*able интерфейсов десятки.

              Более того, это и есть Interface Segregation Principle из SOLID, т.е. идиоматическое ООП.


          1. dyadyaSerezha
            04.07.2022 19:09

            Именно это и является единственным (пока) очевидным случаем.


    1. petuhov_k
      04.07.2022 13:12
      +1

      Для internal можно закрыть глаза и делать как душа пожелает - всегда можно отрефакторить. А вот когда из публичного API торчат интерфейсы, там где надо было бы абстрактные классы c protected internal конструкторами, вот тогда возникают проблемы. Любое изменение будет либо ломающим, либо добавлением интерфейса вида ICoolStufExtended_Version_2.

      Но, видимо, "ведущие эксперты по цифровым навыкам" не готовы делиться таким секретами.


    1. LordDarklight
      05.07.2022 13:35

      интерфейс по своей природе и есть абстрактный класс (в с++ из которого он пошёл развиваться далее - сначала в Java - не уверен в каком ЯП раньше, но в одном из императивных языков появился интерфейс как замена абстрактному классу), затем и в C#.

      Сейчас interface в C# имеет неоспоримое отличительное преимущество по сравнению с abstract class - и меняно:

      1. Множественное "наследование" вернее реализацию - "производный" класс может наследоваться от одного базового класса и при этом реализовывать (поддерживать) любое количество интерфейсов (конечно для C++ это не было бы ограничением для абстрактных классов - но это было бы плохим дизайном)

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

      3. Классы могут скрыто поддерживать интерфейсы - когда их API данных интерфейсов снаружи не будет виден или виден под другим API. Скрытый API за интерфейсом позволяет не перегружать класс лишним API - доступ к которому будет только через приведение к интерфейсу, а если тип интерфейса будет вне зоны видимости - то вообще этот API будет скрыть в этой зоне алгоритма и не будет доступен к вызову. Не уверен, что такое можно сделать на абстрактных классах даже в С++ (в C# то уж точно не выйдет).

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

      5. Интерфейсы в C# могут иметь реализацию членов по умолчанию (для многих это спорная фишка) - очень мощный механизм как увеличения повторного использования кода - так и повышения уровня абстракции этого кода (особенно в сочетании к дженериками). "Множественное" наследование интерфейсов возносит эти возможности до недостижимых для абстрактных классов высот с сохранением стабильности и надёжности алгоритмов. В том числе при последующем развитии API.

      6. Интерфейсы - отличный способ избегать конфликтов имён членов - в т.ч. при "множественном" наследовании (реализации) интерфейсов классами, в т.ч. с реализациями по умолчанию и последующим добавлением в интерфейсы новых членов. /то один самых главных недостатков абстрактных классов и реального множественного наследования оных.

        Но, хочу так же сказать - что Интерфейсы не панацея и не манна небесная. В C# их реализация тоже не идеальна. Считаю более правильным движением от Интерфейсов к Контрактам или даже к Сюитам-Гирам-Эквипментам - снаряжению - которое бы давалось классу и через которое с ним можно было бы взаимодействовать - т.е. вся работа с экземпляром класс должна идти через снаряжение - в т.ч. автогенерируемое - из явно экспортируемого инструментария всего имеющегося снаряжения как бы в структуру класса по умолчанию. То есть сам класс становится более абстрактным понятием - все процессы взаимодействия - только через посредников, раскрывающих свою реализацию только на стадии компиляции в машинный код (или в исполнительный байт-код виртуальной машины - но уже с известной реализацией с подключёнными всеми библиотеками). Тем самым через посредников алгоритмы могут вызывать разные операции над асбтрактными данными - не вникая в особенности структуры этих данных и реализации операций - для алгоритма важен только факт применения той или или иной операции в некоем контексте.

        Ну а самым шиком будет - динамическое и статическое подключение дополнительного оснащения прям к экземплярам и существующим классам - эдакое развитие идеи хелперов. Чтобы можно было добавить или видоизменить публичный API класса не перекомпилирую сам класс (тут лукавлю - т.к. это всё должно быть определено до финальной компиляции в исполняемый байт код; просто имею в виду что не надо трогать исходное описание класса, тем более когда оно в чужой библиотеке) - чисто по месту потребления! Когда требуется иметь доп API для экземпляра класса, или требуется его полиморфное видоизменение - когда тееущая реализация не подходит, и не хочется/ не можется заворачиваться с реализацией класса-наследника.

        Но это оснащение так же должно включать в себя и части для проведения самотестирования - чем выше уровень абстракции - тем обязательнее должны быть юнит-тесты!


      1. LordDarklight
        06.07.2022 09:50

        Добавлю ещё общее преимущество как для интерфейсов так и для абстрактных классов.

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

        Функция так же может просто возвращать Интерфейс - создавая (или получая откуда-либо ещё) экземпляр некоторого класса - и вызывающий код не будет знать что это за класс - для него доступен просто тип Интерфейса - с ним он и будет работать!

        И это одно из самых главных преимуществ Интерфейсного подхода (или абстрактных классов с множественным наследованием). Очень активно это получило развитие в технологии COM/OLE - когда разные приложения/библиотеки стали динамически взаимодействовать друг с другом - ничего не знаю о своих метаданных (это было ещё задолго до рефлексии .NET и даже до Java с её ущербной структурой метаданных, и даже до Delphi c её продвинутой структурой метаданных RTTI).

        Не заменимы интерфейсы и при организации удалённых вызовов - начиная от устаревшей технологии DCOM - и до более современных SOAP и REST протоколов.

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

        Для эффективного решения этой проблемы нужна очередная революция в программировании. Скорее всего уже вместе с переходом на декларативно-императивное программирование и 5-тое поколение ЯП (с развитием уже АОП как наследника ООП)


  1. loltrol
    03.07.2022 22:41
    +2

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


    1. mkvmaks
      05.07.2022 19:14

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


      1. LordDarklight
        06.07.2022 09:25

        Если нет большого опыта написания приложений в парадигме ООП - переносить на неё уже готовое процедурное или функциональное боле менее приложение будет крайне сложно - это всегда плохая идея. Для небольших приложений ООП то и не особо то и нужен - поэтому в C#10 стало возможно проектировать приложения без единого класса.

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

        И для применения ООП требуется некая ломка сознания. Вникать в ООП нужно на практических примерах - либо реализуя их по книжкам либо по учебным курсам либо копаясь и дорабатывая готовые программы и библиотеки.

        ООП сложнее в освоении чем просто процедурное программирование и сложнее в использовании - но зато в итоге потом получаются более простые, надёжные и легко дорабатываемые приложения (ну если ООП грамотно использован, конечно).

        Когда-то давно я изучал исходные коды игры Doom - она написана была на чистом Си - и вот там ООП просто напрашивался на каждом шагу!

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


  1. SIMPLicity
    04.07.2022 01:45

    В C# я, безусловно, лох. Наверное именно по этому я так и не понял,- что же автор хотел нам рассказать.