К списку

Абстрактные классы и интерфейсы. Часть 2

4 февраля 2020

Оптимизация временных и финансовых затрат является существенной частью любого уважающего себя бизнеса. Один из подходов для решения данной задачи состоит в максимальной унификации ресурсов, как людских, так и материальных. На мой взгляд достаточно очевидным является тот факт, что универсальность сотрудников позволяет максимально эффективно использовать их в рамках производственного процесса, “сглаживая” пиковые нагрузки на выполнение той или иной операции. Яркий пример такого подхода — это горячо любимый детьми всей планеты “МакДональдс”. В зависимости от ситуации каждый сотрудник может и на кассу стать, и гамбургер собрать, и столики протереть, и шарики раздать. При этом сотрудник никогда не бывает без дела, и при необходимости помогает своим коллегам если у них “запар”, что обеспечивает максимальную пропускную способность заведения без привлечения дополнительных ресурсов. Конечно, с точки зрения высоких материй можно (и нужно) поставить под сомнение эффективность использования повара с тремя мишленовскими звездами в качестве полотера, но это вполне допустимо в момент когда гамбургеры никому не нужны, но сам ресторан нуждается в серьезной быстрой уборке после отвязного детского мальчишника. Обсуждение границ разумного применения унификации оставим пока за кадром, как и вопрос целесообразности модного нынче в IT подхода “try {Dev as QA, QA as BA, BA as  Dev} catch(EpicFailedException) {}”.

В любом случае, для унификации сотрудников важно не то, что ты из себя представляешь как объект, а то, что ты умеешь делать в рамках некоторого “контракта”.

Продолжим тему быстрого и полезного питания, совместив ее с вопросами эффективной доставки его клиентам. Гамбургер сделан и протестирован, и перед менеджером стоит задача обеспечить его доставку по адресу заказчика в установленные временные лимиты и с минимальными финансовыми затратами. К его услугам разношерстный штат курьеров с не менее разнообразными средствами доставки — автомобилем, мопедом, велосипедом, быстрыми ногами, и, наконец, дронами. У каждого из них есть свои достоинства и недостатки в условиях загруженного города. Главное, что ни менеджера, ни клиента не интересует кто (или что) доставит продукт по заданному адресу, важны только сам факт успешной доставки в срок, и соответственно, положительные отзывы в инстаграме о четкой работе заведения. Очевидно, что при этом весь богатый внутренний мир курьера и детали осуществления доставки (количество проездов на красный свет, порванные кроссовки и т.д.) никого не волнуют и не должны волновать — это вредит бизнесу. Детали ничто, контракт — все! А контракт предельно простой: курьер получает заказ на доставку, и, если соглашается с указанным временем, то должен доставить, и все. Это очень упрощает прозрачность и устойчивость работы. У менеджера есть некий безликий пул доставщиков без какой либо детализации, и ему нужно просто нажать кнопку “начать доставку”.

Попробуем теперь создать архитектуру приложения, поддерживающую подобный “контракт”. Предположим, что доставка может быть осуществлена автомобилем, мопедом, пешим курьером либо дроном. Как уже обсуждалось в первой части статьи, использование полиморфизма посредством механизма виртуальных функций и абстрактных классов позволяет возложить ответственность за реализацию метода на конкретный тип. В нашем случае это будут типы Car, Motorbike, Pedestrian, Dron, реализующие общий метод Deliver(/*some params*/). Теперь необходимо их связать неким общим типом для “унификации”, через ссылку на объект которого будет осуществляться связь между менеджером и непосредственно объектом-доставщиком. Пока все красиво, но не безоблачно.

Первая проблема возникает казалась бы из воздуха: а как назвать базовый класс для этих четырех сущностей? Попробуем использовать в качестве базового класса Vehicle, уже описанный ранее. Однако есть неувязка. Сама идея наследования подразумевает, что потомок связан с родительским (базовым) классом отношением “IS” (сокращенно от “ISsoft”, шутка). Car is Vehicle – это правда, Motorbike is Vehicle— тоже правда, а с правдивостью утверждения Pedestrian is Vehicle можно сильно поспорить. Надо четко понимать, что проблема в первую очередь не семантическая, а идеологическая. Между сущностями Car, Motorbike, Pedestrian и Dron просто нет ничего общего ни физиологически, ни физически, а значит наследниками общего класса они быть не могут. Разумеется, скептики могут продолжать упорствовать и искать связь между данными сущностями, аппеллируя к их единому атомному составу и таблице Менделеева, но оставим это на их совести.

Технически связать представленные сущности базовым классом с некоторым вымученным названием DeliveryItem конечно можно, однако это в корне неверно и порождает больше проблем, чем позволяет решить. В первую очередь нужно определиться с функционалом, который обязан обеспечивать класс-наследник. Предположим, это два метода: запрос у объекта GetExpectedDeliveryTime(), и непосредственно команда на выполнение заказа Deliver(). Однако это ограничивает гибкость использования  объектов типа Car и Motorbike — их нельзя использовать унифицированным образом, предположим, для использования в качестве такси нет соответствующего метода. Добавление такого метода в DeliveryItem приведет к тому, что и Pedestrian должен выполнять общий контракт, и отработать доставщиком пассажиров, что как минимум нереально. Добавление метода PassengerPickUp() отдельно для Car и Motorbike не позволит их использовать единообразным способом через ссылку на базовый класс, поскольку DeliveryItem ничего не знает о возможности перевозки пассажиров.

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

Определим некоторый интерфейс IDeliver и два его метода:

public interface IDeliver
{
  DateTime GetExpectedTime(string address);
  bool Deliver(string address);
}

Пусть два класса Car и Dron реализуют этот интерфейс:

public class Car : IDeliver
{
  /*
   * Some specific fields
   */
  public DateTime GetExpectedTime(string address)
  {
    return /* expected delivery time */;
  }
  public bool Deliver(string address)
  {
    return /* delivery success/not success result */;
  }
}
public class Dron : IDeliver
{
  /*
   * Some specific fields
   */
  public DateTime GetExpectedTime(string address)
  {
    return /* expected delivery time */;
  }
  public bool Deliver(string address)
  {
    return /* delivery success/not success result */;
  }
}

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

public class DeliveryManager 
    {
        List<IDeliver> _deliveryItems = new List<IDeliver> {new Car(), new Dron() };
        /*
         * Some specific fields         
         */
        public bool Deliver(string address, DateTime expectedTime) 
        {
            foreach (var item in _deliveryItems)
            {
                if (item.GetExpectedTime(address) <= expectedTime) 
                {
                    return item.Deliver(address);                
                }
            }
            return false;
        }
    }

Логику выбора доставщика можно усложнить, введя дополнительно оценку издержек доставки, и осуществлять выбор исходя не только из времени, но и из стоимости доставки. Для этого достаточно будет расширить интерфейс IDelivery соответствующим методом, например, GetCost(string address).

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

  1. Объединение (унификация) разнородных сущностей в единую категорию с функциональной точки зрения.
  2. Расширение функциональности однородных объектов.

В рассмотренном выше примере описан первый случай – создание категории объектов, способных осуществить доставку. Хорошей иллюстрацией второго случая (расширение функциональности) является организация бесперебойной и эффективной работы ресторана “МакДональдс”, основная идея которой изложена в начале второй части статьи. Если кратко, то все сотрудники ресторана по мере продвижения по карьерной и зарплатной лестнице осваивают новые специальности и совершенствуют навыки по ранее полученным. Manager, Administrator, Cooker, Cleaner, Animator — это наследники базового абстрактного класса Employee (содержащего персональные данные сотрудника), а их должностные обязанности согласно контракта описываются соответствующими интерфейсами:

public class Cleaner : Employee, ICleaner
…
public class Cooker : Employee, ICooker, ICleaner
…
public class Manager : Employee, IManager, IIssueResolver, IAnimator, ICooker
…

и т.д.

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

    public bool EmergencyTableClean()
    {
        foreach (var employee in AvailableEmployeePool)
        {
            if (employee is ICleaner)
            {
                return (employee as ICleaner).CleanTable();
            }
        }
        return false;
    }

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

Кратко резюмируя все вышесказанное можно сформулировать следующее формальное правило: для структурно схожих сущностей (имеющих набор общих полей) необходимо использовать наследование, для сущностей, схожих по функциональности – реализацию общих интерфейсов. И да пребудет с вами великая сила ООП!

Автор материала – Игорь Хейдоров, преподаватель Тренинг-центра ISsoft.

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

Опыт работы: C++/C# разработчик с 1997 года.