Pull to refresh

Голливудский принцип (IoC)

Reading time6 min
Views6.6K
В этой статье я постараюсь рассказать о принципе проектирования, называемом Inversion of Control / Инверсия управления (IoC), называемом еще Голливудским принципом. Покажу, какое отношение это имеет к принципу подстановки Барбары Лисково (LSP), а также внесу свою лепту в священную войну private vs protected.



В качестве предисловия хочу сказать несколько слов о себе. По образованию я инженер-программист, в индустрии IT работаю уже более 10 лет и в последнее время увлекаюсь написанием тематических профессиональных статей. Некоторые из них были удачными. Ранее я публиковалась на другом ресурсе, к сожалению, недоступном на территории России (привет Роскомнадзору). Если кто-то захочет с ними ознакомиться — вы знаете что делать.

Все примеры кода, как обычно, представлены в статье псевдокодом, стилизованным под “ненавистный php”.

Исходная задача


Чтобы было быстрее и понятнее, сразу переходим к примеру. Из отдела продаж поступила хотелка видеть метрики: сколько денег мы зарабатываем ежемесячно, ежедневно, ежечасно.

Мы решаем эту задачу с помощью трех команд, периодически запускаемых по расписанию:

  • MonthlyReportCommand
  • DailyReportCommand
  • HourlyRerortCommand

Нам понадобятся интерфейсы:

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
}

И видим, что код метода calculateTotals() будет совершенно одинаковый во всех случаях. Первое, что приходит в голову — это положить дублирующийся код в общий абстрактный класс. Вот так:



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() является частью внутренних механизмов нашего класса. Мы предусмотрительно закрываем его, т.к. он не должен быть вызван посторонними внешними клиентами — мы не для того его проектируем. Мы объявляем этот метод protected, т.к. планируем вызывать его в наследниках — вот наша цель. Очевидно, что такой абстрактный класс очень похож на нечто вроде библиотеки — он просто предоставляет какие-то методы (для php-знатоков: т.е. работает как Trait).

Секрет абстрактных классов


Настало время немного отвлечься от примера и вспомнить назначение абстрактных классов:

Абстрактный класс инкапсулирует общие механизмы, в то же время позволяя наследникам реализовывать собственные частные особенности поведения.

Абстракция (лат. abstractio — отвлечение) — это отвлечение от деталей и обобщение. На данный момент класс 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, private vs protected?


Так при чем тут Инверсия управления? Откуда такое название? Очень просто: сначала мы задаем последовательность вызовов непосредственно в конечных реализациях, управляя тем, что и когда будет сделано. А в дальнейшем, мы перенесли эту логику в общую абстракцию. Теперь абстракция управляет тем, что и когда будет вызвано, а реализации просто подчиняются этому. Т.е мы инвертировали управление.

Чтобы закрепить такое поведение и избежать проблем с принципом подстановки Барбары Лисков (LSP), можно закрыть метод createReport() с помощью включения final в объявление метода. Ведь всем известно, что LSP имеет прямое отношение к наследованию.

abstract class AbstractReportCommand implements ReportCommandInterface {
    final public function createReport(): Money {
        //bla-bla realization
    }
    ...
}

Тогда все наследники класса AbstractReportCommand становятся жестко подчинены единой логике, которую невозможно переопределить. Железная дисциплина, порядок, светлое будущее.

По той же причине становится очевидным преимущество private перед protected. Все, что относится к общим механизмам функционирования, должно быть зашито в абстрактном классе и недоступно к переопределению — private. Все, что должно быть переопределено/реализовано в частных случаях — abstract protected. Любые методы конструируются для конкретных целей. И если вы не знаете, какую именно область видимости задать методу — это значит, что вы не знаете, зачем вы его создаете. Такой дизайн стоит пересмотреть.

Выводы


Конструирование абстрактных классов всегда предпочтительнее с применением Инверсии управления, т.к. позволяет пользоваться идеей абстракции на полную катушку. Но и использование абстрактных классов в качестве библиотек в некоторых случаях тоже может быть оправдано.

Если взглянуть шире, то наше местечковое противостояние Голливудского принципа и абстрактного класса-библиотечки превращается в спор: фреймворк (IoC по-взрослому) vs библиотека. Нет смысла доказывать, что из них лучше — каждый создается с определенной целью. Единственно важное — осознанное создание подобных структур.

Спасибо всем, кто внимательно прочитал от начала до конца — вы мои любимые читатели.
Tags:
Hubs:
+17
Comments11

Articles