В этой статье я попытаюсь рассказать о принципе проектирования под названием инверсия управления (IoC), также называемом принципом Голливуда.
Я покажу тебе, как это связано с Принцип замещения Барбары Лисково (LSP) , а также внесет свой вклад в священную войну между личным и защищенным.
В качестве предисловия хочу сказать несколько слов о себе.
По образованию я инженер-программист, работаю в IT-индустрии более 10 лет, в последнее время увлекся написанием тематических профессиональных статей.
Некоторые из них оказались успешными.
Ранее я публиковал на другом ресурсе, который, к сожалению, недоступен в России (привет Роскомнадзору).
Если кто-то захочет с ними познакомиться, вы знаете, что делать.
Все примеры кода, как обычно, представлены в статье в виде псевдокода, стилизованного под «ненавистный php».
Исходная проблема
Чтобы было быстрее и понятнее, перейдем к примеру.Отдел продаж хотел увидеть метрики: сколько денег мы зарабатываем ежемесячно, ежедневно, ежечасно.
Мы решаем эту проблему с помощью трех команд, которые выполняются периодически по расписанию:
- Команда ЕжемесячныйОтчет
- Команда DailyReport
- Почасовая команда Resort
Пишем команды отчета (последняя опущена в качестве практического упражнения для тех, кто хочет досконально разобраться и попрактиковаться в его написании самостоятельно):interface ReportCommandInterface { public function createReport(): Money; } interface MoneyRepositoryInterface { /** @return Money[] */ public function getMoney(Period $period): array; } interface MetricRepositoryInterface { public function saveMoneyMetric(Period $period, Money $amount, string $metricType); }
class MonthlyReportCommand implements ReportCommandInterface {
//lets assume constructor is already here
public function createReport(): Money {
$period = new Period(new DateTime('first day of previous month'), new DateTime('last day of previous month'));
$moneyRecords = $this->moneyRepository->getMoney($period);
$amount = $this->calculateTotals($moneyRecords);
$this->metricRepository->saveMoneyMetric($period, $amount, 'monthly income');
}
/** @param Money[] $moneyRecords */
private function calculateTotals(array $moneyRecords): Money {
//here is calculating sum of money records
}
}
class DailyReportCommand implements ReportCommandInterface {
//lets assume constructor is already here
public function createReport(): Money {
$period = new Period(new DateTime('yesterday'), new DateTime('today'));
$moneyRecords = $this->moneyRepository->getMoney($period);
$amount = $this->calculateTotals($moneyRecords);
$this->metricRepository->saveMoneyMetric($period, $amount, 'daily income');
}
/** @param Money[] $moneyRecords */
private function calculateTotals(array $moneyRecords): Money {
//here calculates sum of money records
}
}
class HourlyReportCommand .
{
//the same as previous two but hourly
}
И мы видим, что код метода CalcultTotals() во всех случаях будет абсолютно одинаковым.
Первое, что приходит на ум — поместить дублирующийся код в общий абстрактный класс.
Так:
abstract class AbstractReportCommand {
protected function calculateTotals(array $moneyRecords): Money {
//here calculates sum of money records
}
}
class MonthlyReportCommand extends AbstractReportCommand implements ReportCommandInterface {
public function createReport(): Money {
//realization is here, calls calculateTotals($moneyRecords)
}
}
class DailyReportCommand extends AbstractReportCommand implements ReportCommandInterface {
//the same as previous two but daily
}
class HourlyReportCommand .
{
//the same as previous two but hourly
}
Метод CalculateTotals() является частью внутренних механизмов нашего класса.
Мы благоразумно закрываем его, потому что.
он не должен вызываться несвязанными внешними клиентами — мы не для этого его проектируем.
Мы объявляем этот метод защищенным, т.к.
планируем вызвать его в наследники — это наша цель.
Очевидно, что такой абстрактный класс очень похож на что-то вроде библиотеки — он просто предоставляет некоторые методы (для знатоков PHP: т.е.
работает как Trait).
Секрет абстрактных классов
Пришло время немного отвлечься от примера и вспомнить назначение абстрактных классов: Абстрактный класс инкапсулирует общие механизмы, позволяя потомкам реализовывать свое собственное поведение.Абстракция (лат. абстрактио — отвлечение) — отвлечение от деталей и обобщения.
На данный момент класс AbstractReportCommand обобщает только подсчет денег для всех отчетов.
Но мы можем сделать нашу абстракцию более эффективной, используя голливудский принцип, который звучит следующим образом: «Не звоните нам, мы сами вам позвоним»
Чтобы увидеть, как это работает, давайте добавим в AbstractReportCommand общий механизм отчетов:
abstract class AbstractReportCommand implements ReportCommandInterface {
/** @var MoneyRepositoryInterface */
private $moneyRepository;
/** @var MetricRepositoryInterface */
private $metricRepository;
//lets assume constructor is already here
public function createReport(): Money {
$period = $this->getPeriod();
$metricType = $this->getMetricType();
$moneyRecords = $this->moneyRepository->getMoney($period);
$amount = $this->calculateTotals($moneyRecords);
$this->metricRepository->saveMoneyMetric($period, $amount, $metricType);
}
abstract protected function getPeriod(): Period;
abstract protected function getMetricType(): string;
private function calculateTotals(array $moneyRecords): Money {
//here calculates sum of money records
}
}
class MonthlyReportCommand extends AbstractReportCommand {
protected function getPeriod(): Period {
return new Period(new DateTime('first day of previous month'), new DateTime('last day of previous month'));
}
protected function getMetricType(): string {
return 'monthly income';
}
}
class DailyReportCommand extends AbstractReportCommand {
protected function getPeriod(): Period {
return new Period(new DateTime('yesterday'), new DateTime('today'));
}
protected function getMetricType(): string {
return 'daily income';
}
}
class HourlyReportCommand .
{
//the same as previous two but hourly
}
Что мы получили.
Потомки абстрактного класса больше не имеют доступа к общим механизмам (не звоните нам).
Вместо этого абстракция дает своим преемникам общую схему работы и требует от них реализации конкретных поведенческих функций, используя только результаты (мы будем называть вас сами).
А как насчет обещанного IoC, LSP, частного или защищенного?
Так причем тут инверсия управления? Откуда это имя? Все очень просто: сначала мы определяем последовательность вызовов непосредственно в финальных реализациях, контролируя, что и когда будет сделано.А позже мы перевели эту логику в общую абстракцию.
Теперь абстракция управляет тем, что и когда будет вызываться, а реализации просто этому подчиняются.
То есть мы инвертировали управление.
Чтобы закрепить такое поведение и избежать проблем с Принцип замещения Барбары Лисков (LSP) вы можете закрыть метод createReport(), включив Final в объявление метода.
Ведь всем известно, что LSP напрямую связан с наследованием.
abstract class AbstractReportCommand implements ReportCommandInterface {
final public function createReport(): Money {
//bla-bla realization
}
.
}
Тогда все потомки класса AbstractReportCommand становятся строго подчинены единой логике, которую невозможно переопределить.
Железная дисциплина, порядок, светлое будущее.
По этой же причине становится очевидным преимущество частного над защищенным.
Все, что относится к общим механизмам функционирования, должно быть встроено в абстрактный класс и не может быть переопределено — приватно.
Все, что должно быть переопределено/реализовано в особых случаях, является абстрактно защищенным.
Любые методы созданы для определенных целей.
И если вы не знаете точно, какую область действия установить для метода, это означает, что вы не знаете, зачем его создаете.
Этот дизайн стоит пересмотреть.
выводы
Построение абстрактных классов всегда предпочтительнее с использованием Inversion of Control, т.к.позволяет использовать идею абстракции в полной мере.
Но использование абстрактных классов в качестве библиотек в некоторых случаях тоже может быть оправдано.
Если посмотреть шире, то наше локальное противостояние голливудского принципа и класса абстрактной библиотеки превращается в спор: фреймворк (IoC по-взрослому) vs библиотека.
Нет смысла доказывать, какой из них лучше – каждый создан для определенной цели.
Важно лишь сознательное создание таких структур.
Спасибо всем, кто внимательно прочитал от начала до конца — вы мои любимые читатели.
Теги: #программирование #Анализ и проектирование систем #ООП #IoC #принципы проектирования #Голливудский принцип
-
Понимание Компьютерной Памяти
19 Oct, 24 -
Демография
19 Oct, 24 -
Пусть Сережа Программирует
19 Oct, 24 -
Google Против Гудже
19 Oct, 24 -
Apple Защищает Свой Логотип
19 Oct, 24 -
Википоиск, Социальный Google
19 Oct, 24 -
Windows Vista Будет Разрешена Для Загрузки
19 Oct, 24 -
Проект Завершен, А Денег Нет.
19 Oct, 24