Принцип Инверсии Управления – Голливудский Принцип / Блог им. Adik88


Оригинал: Inversion of Control – The Hollywood Principle.

Среди программистов (включая и меня, так что здесь я публично признаю свою вину) бытует мнение о том, что Инверсия Управления (IoC) является не более, чем синонимом для старого принципа Внедрения Зависимости (DI). Существует простая причина для такой точки зрения: идея Принципа Внедрения зависимостей состоит в реализации классов, чьи элементы обеспечиваются окружающим контекстом, что позволяет не изменять код программы. Таким образом, процесс может быть рассмотрен как часть принципа IoC.

Но, несмотря на то, что уравнение DI=IoC может рассматриваться в целом как справедливое утверждение, все же концепция Инверсии Управления сама по себе гораздо шире. Фактически мы можем сказать, что DI – это частный случай IoC, но далеко не единственный. Это вновь приводит нас к началу: если DI – это всего лишь шаблон, который опирается на сильные стороны IoC, то что же такое Прицип Инверсии Контроля?

Традиционно компоненты приложения были сконструированы для оперирования и контроля над средой исполнения; такой подход справедлив до некоторой степени. Например, модуль логирования (logging module) может выполнять задачу вывода данных в файл; при этом вопросы «как» и «когда» выводить данные будут полностью под контролем модуля. Файл с логами (в данном случае — это часть окружающей среды) будет просто внешним, пассивным элементом, не оказывающим влияния на работу модуля. Но давайте представим, что нам необходимо расширить функциональность модуля и дать ему возможность для дополнительного вывода информации в базу данных или даже возможность вывода посредством email. Усовершенствование модуля для придания дополнительной функциональности повысит его сложность, сделает его более раздутым, в то время как логика, необходимая для выполнения этих дополнительных обязанностей, упакована в тот же API. Подход работает, но не в масштабе.

Эта запутанная ситуация может быть решена довольно простым способом. Вместо того, чтобы делать модуль полностью ответственным за вывод данных в несколько конечных точек, мы можем передать эту ответственность напрямую в контекст. Реализация модуля останется до смешного простой и ограниченной в действиях, как простой диспетчер событий. С другой стороны, контекст будет ответственен за применение всей логики, необходимой для вывода данных полностью независимо от рассматриваемого модуля.

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

Конечно, IoC – это языковая агностическая парадигма, так что ее можно без проблем употребить в мире РНР.

Достижение Инверсии Управления – наблюдение объектов домена

IoC действительно вездесущ, так что довольно просто найти примеры его реализации. Первое, что приходит в голову, это Внедрение Зависимости, но немало и других примеров, отлично это демонстрирующих, особенно, если затронуть вопрос событийно-ориентированного программирования (Event-Driven Design). Если вы сейчас гадаете в какой-такой параллельной вселенной IoC соединяется с механизмами обработки событий, то рассмотрите классический шаблон в репертуаре GoF: шаблон Наблюдатель (The Observer pattern).

Используемые практически повсеместно, даже на стороне клиента посредством JavaScript, наблюдатели – это яркий пример концепции IoC в действии; существует некий высоко-разобщенный субъект, сфокусированный на выполнении всего нескольких узких задач без загрязнения окружающего контекста, в то время как один или более внешних наблюдателей ответственны за реализацию логики, необходимой для управления событиями, вызванными субъектом. Задача управления событиями и даже создание новых событий полностью возложена на наблюдателей, а не на субъект.

Пример поможет мне немного разъяснить всю предыдущую болтовню. Давайте представим, что мы реализовали примитивную доменную модель (Domain Model), которая определяет систему взаимоотношений «один ко многим» между постами в блоге и комментариями. В этом случае мы будем намеренно самоуверенны и присвоим нашей модели способность «выстреливать» электронное письмо для уведомления системного администратора о добавлении нового комментария к посту.

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

<?php
namespace Model;
 
interface PostInterface
{
    public function setTitle($title);
    public function getTitle();
     
    public function setContent($content);
    public function getContent();
     
    public function setComment(CommentInterface $comment);
    public function getComments();
}


<?php
namespace Model;
 
class Post implements PostInterface, SplSubject
{
    private $title;
    private $content;
    private $comments  = [];
    private $observers = [];
 
    public function __construct($title, $content) {
        $this->setTitle($title);
        $this->setContent($content);
    }
     
    public function setTitle($title) {
        if (!is_string($title) 
            || strlen($title) < 2
            || strlen($title) > 100) {
            throw new InvalidArgumentException(
                "The post title is invalid.");
        }
        $this->title = $title;
        return $this;
    }
 
    public function getTitle() {
        return $this->title;
    }
     
    public function setContent($content) {
        if (!is_string($content) || strlen($content) < 10) {
            throw new InvalidArgumentException(
                "The post content is invalid.");
        }
        $this->content = $content;
        return $this;
    }
     
    public function getContent() {
        return $this->content;
    }
 
    public function setComment(CommentInterface $comment) {
        $this->comments[] = $comment;
        $this->notify();
    }
     
    public function getComments() {
        return $this->comments;
    }
 
    public function attach(SplObserver $observer) { 
        $id = spl_object_hash($observer);
        if (!isset($this->observers[$id])) {
            $this->observers[$id] = $observer;
        }
        return $this;
    }
     
    public function detach(SplObserver $observer) {
        $id = spl_object_hash($observer);
        if (!isset($this->observers[$id])) {
            throw new RuntimeException(
                "Unable to detach the requested observer.");
        }
        unset($this->observers[$id]); 
        return $this;
    }
     
    public function notify() {
        foreach ($this->observers as $observer) {    
            $observer->update($this);
        }
    }
}


<?php
namespace Model;
 
interface CommentInterface
{
    public function setContent($content);
    public function getContent();
     
    public function setAuthor($author);
    public function getAuthor();
}


<?php
namespace Model;
 
class Comment implements CommentInterface
{
    private $content;
    private $author;
     
    public function __construct($content, $author) {
       $this->setContent($content);
       $this->setAuthor($author);
    }
     
    public function setContent($content) {
        if (!is_string($content) || strlen($content) < 10) {
            throw new InvalidArgumentException(
                "The comment is invalid.");
        }
        $this->content = $content;
        return $this;
    }
     
    public function getContent() {
        return $this->content;
    }
     
    public function setAuthor($author) {
        if (!is_string($author) 
            || strlen($author) < 2
            || strlen($author) > 50) {
            throw new InvalidArgumentException(
                "The author is invalid.");
        }
        $this->author = $author;
        return $this;
    }
     
    public function getAuthor() {
        return $this->author;
    }
}


Взаимоотношение между классами Post и Comment тривиально, но класс Post заслуживает более внимательного рассмотрения. Фактически он был спроектирован как «классический» субъект, а, следовательно, обеспечивающий типичный API, который позволяет прикреплять/отделять и уведомлять наблюдателей по желанию.

Наиболее интересным аспектом в этом процессе является реализация setComment(), где по-сути и реализуется принцип инверсии управления. Метод просто вызывается «notify» уведомление всем зарегистрированным наблюдателям независимо от того, когда был добавлен комментарий. Это означает, что вся логика, необходимая для отправления уведомлений по электронной почте, делегирована одному или более внешним наблюдателям, таким образом разгружая Post от грязной работы и сохраняя его сфокусированным только на его собственной задаче.
При такой простой и эффективной схеме инверсии управления в конкретном месте единственная структура, которую осталось добавить для полноты картины, это, по крайней мере, один наблюдатель, который должен быть ответственен за отправку вышеупомянутых электронных писем. Чтобы сделать вещи проще, я собираюсь представить наблюдателя в виде живой сущности на уровне сервиса.

Делегирование управления во внешнюю среду – реализация Сервиса уведомлений о комментариях

Создание сервиса наблюдателя, способного вызывать уведомления по электронной почте при добавлении нового комментария к посту в блоге, — это простой процесс, который сводится к определению класса, реализующего метод update(). Если вам любопытно и вы хотите увидеть как выглядит рассматриваемый сервис, то вот он:

<?php
namespace Service;
 
class CommentService implements SplObserver
{
    public function update(SplSubject $post) {
        $subject = "New comment posted!";
        $message = "A comment has been made on a post entitled " .
            $post->getTitle();
        $headers = "From: "Notification System" <[email protected]>rnMIME-Version: 1.0rn";
        if (!@mail("[email protected]", $subject, $message, $headers)) {
            throw new RuntimeException("Unable to send the update.");
        }
    }
}


CommentService класс делает в точности то, что должен: он запускает метод update() для отправки электронного письма сисадмину каждый раз, когда пользователь размещает комментарий, относящийся к данному посту.

Будет намного проще увидеть все преимущества принципа инверсии управления, если я покажу вам скрипт, в котором работают все классы-образцы, так что ниже приведу немного кода:

<?php
use LibraryLoaderAutoloader,
    ModelPost,
    ModelComment,
    ServiceCommentService;
          
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader();
$autoloader->register();
 
$post = new Post(
    "A sample post",
    "This is the content of the sample post"
);
 
$post->attach(new CommentService());
 
$comment = new Comment(
    "A sample comment",
    "Just commenting on the previous post"
);
 
$post->setComment($comment);


Вполне вероятно (и это только мое предпочтение), что будет не совсем правильно внедрять сервис внутрь объекта домена. Как мы всегда утверждали, слой домена обязан быть агностиком по отношению к слою сервиса (если, конечно, речь не идет о разделенных интерфейсах) и он должен находится на верхнем уровне, взаимодействуя со множеством клиентов. Однако в данном случае этот подход не настолько плох, учитывая, что Post класс — это всего лишь контейнер для зарегистрированных наблюдателей, которые используются при обновлении события. Более того, принимая во внимание как аккуратно инвертированы зоны ответственности между рассматриваемым сервисом и классом Post, мои жалобы – это не более, чем капризы.

Заключение

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

Если в своих приложениях вы используете принцип Внедрения Зависимости (а вы ведь используете, не так ли?), то, несомненно, ваши профессиональные инстинкты вас не подвели, ведь вы уже пользуетесь преимуществами Инверсии управления. Однако в своей статье я пытался продемонстрировать, что существует широкий диапазон ситуаций, для которых данный подход отлично работает, и это отнюдь не только правильное управление классом зависимостей. И событийно-ориентированное программирование (Event-Driven Design) здесь яркий пример.