Оптимизация временных и финансовых затрат является существенной частью любого уважающего себя бизнеса. Один из подходов для решения данной задачи состоит в максимальной унификации ресурсов, как людских, так и материальных. На мой взгляд достаточно очевидным является тот факт, что универсальность сотрудников позволяет максимально эффективно использовать их в рамках производственного процесса, “сглаживая” пиковые нагрузки на выполнение той или иной операции. Яркий пример такого подхода — это горячо любимый детьми всей планеты “МакДональдс”. В зависимости от ситуации каждый сотрудник может и на кассу стать, и гамбургер собрать, и столики протереть, и шарики раздать. При этом сотрудник никогда не бывает без дела, и при необходимости помогает своим коллегам если у них “запар”, что обеспечивает максимальную пропускную способность заведения без привлечения дополнительных ресурсов. Конечно, с точки зрения высоких материй можно (и нужно) поставить под сомнение эффективность использования повара с тремя мишленовскими звездами в качестве полотера, но это вполне допустимо в момент когда гамбургеры никому не нужны, но сам ресторан нуждается в серьезной быстрой уборке после отвязного детского мальчишника. Обсуждение границ разумного применения унификации оставим пока за кадром, как и вопрос целесообразности модного нынче в 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).
Надо четко понимать, что концепция реализации интерфейсов не заменяет, а дополняет концепцию наследования классов. Можно выделить два основных типичных случая использования интерфейсов:
- Объединение (унификация) разнородных сущностей в единую категорию с функциональной точки зрения.
- Расширение функциональности однородных объектов.
В рассмотренном выше примере описан первый случай – создание категории объектов, способных осуществить доставку. Хорошей иллюстрацией второго случая (расширение функциональности) является организация бесперебойной и эффективной работы ресторана “МакДональдс”, основная идея которой изложена в начале второй части статьи. Если кратко, то все сотрудники ресторана по мере продвижения по карьерной и зарплатной лестнице осваивают новые специальности и совершенствуют навыки по ранее полученным. 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 года.
Обратите внимание! В ISsoft открыты вакансии для C# программистов. Мы ждем ваших откликов!