Среди программистов существует консенсус (включая меня, так что вот моя собственная публичная ошибка ), что Inversion of Control (IoC) — это не более чем синоним простого старого Dependency Injection (DI). Существует довольно интуитивная причина, которая поддерживает этот образ мышления: если мотивация DI состоит в том, чтобы продвигать дизайн классов, внешние коллабораторы которых обеспечиваются окружающим контекстом, а не обращать их внимание на обратный взгляд, процесс можно эффективно рассматривать как форму IoC.
Но хотя уравнение DI = IoC можно считать в целом верным, сама концепция инверсии управления гораздо шире. На самом деле, можно сказать, что DI — это особый вариант использования, который использует преимущества IoC, но далеко не единственный. Это возвращает нас к началу; если DI — это просто модель, опирающаяся на сильные стороны IoC, то что же на самом деле представляет собой IoC?
Традиционно компоненты приложения были разработаны для работы и управления средой исполнения, что в некоторой степени дает хороший результат. Например, модуль регистрации может быть реализован для записи данных в файл, и как и когда регистрировать данные будет полностью под контролем модуля. Файл журнала (в данном случае это часть среды) будет просто внешним пассивным элементом, не влияющим на работу модуля. Но, скажем, нам нужно расширить функциональность модуля и дать ему возможность дополнительно регистрировать данные в базе данных или даже по электронной почте. Обновление модуля для предоставления дополнительной функциональности усложнит его, сделав его более раздутым, поскольку логика, необходимая для выполнения этих дополнительных обязанностей, упакована в один и тот же API. Подход работает, но не масштабируется вообще.
Эта запутанная ситуация может быть решена довольно простым способом. Вместо того чтобы делать модуль полностью ответственным за регистрацию данных на нескольких конечных точках, мы можем перенести ответственность прямо во внешнюю среду. Реализация модуля останется смехотворно простой, ограничиваясь просто диспетчером событий. С другой стороны, среда будет отвечать за реализацию всей логики, необходимой для регистрации данных, полностью независимых от рассматриваемого модуля.
Неудивительно, что процесс перераспределения этих обязанностей между компонентами и средой формально известен как «Инверсия контроля» (или, если говорить более мягко, «Голливудский принцип» ), и его реализация может стать настоящим стимулом, когда речь идет о разработке расширяемых Отсоединенные программные модули.
Конечно, IoC — это не зависящая от языка парадигма, и поэтому ее можно использовать в мире PHP без особых суеты.
Достижение инверсии управления — наблюдение за объектами домена
IoC действительно присутствует повсеместно, поэтому довольно легко найти реализации его производства. Первый случай использования, который приходит на ум, — это инъекция зависимости, но есть много других примеров, в равной степени показательных, особенно при переходе на рельеф событийно-управляемого дизайна. Если вам интересно, как IoC в параллельной вселенной взаимодействует с механизмами обработки событий, рассмотрите классику из репертуара GoF: шаблон Observer.
Используемые почти везде, даже на стороне клиента через JavaScript, наблюдатели являются ярким примером концепции IoC в действии; есть сильно отделенный субъект, сосредоточенный на выполнении нескольких узких задач без загрязнения окружающего контекста, в то время как один или несколько внешних наблюдателей отвечают за реализацию логики, необходимой для обработки событий, инициируемых субъектом. То, как обрабатывать события и даже обрабатывать новые, полностью зависит от наблюдателей, а не от субъекта.
Примером может быть хороший способ сделать мой предыдущий лепет немного более понятным. Итак, допустим, мы реализовали примитивную модель предметной области, которая определяет отношение один-ко-многим между сообщениями в блоге и комментариями. В этом случае мы будем намеренно амбициозны и дадим модели возможность запустить электронную почту, чтобы уведомить системного администратора о добавлении нового комментария в сообщение.
Честно говоря, реализация такой функции без обращения к 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; 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()
где происходит реальная инверсия управления. Метод просто запускает «вытягивающее» обновление для всех зарегистрированных наблюдателей при добавлении комментария. Это означает, что вся логика, необходимая для отправки уведомления по электронной почте, делегируется одному или нескольким внешним наблюдателям, таким образом снимая грязную работу с 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, где многие разработчики склонны интуитивно ассоциировать концепцию только с простым старым Dependency Injection, Inversion of Control — это простая, но убийственная методология программирования, которая, при правильной реализации, является фантастическим способом создания развязанные, ортогональные системы, компоненты которых легко тестируются изолированно.
Если вы используете Dependency Injection в своих приложениях (верно?), То вы должны чувствовать, что инстинкты вашего кодера довольно хорошо удовлетворены, поскольку вы уже используете преимущества, которые предоставляет Inversion of Control. Однако, как я пытался продемонстрировать ранее, существует множество ситуаций, в которых этот подход подходит не только для правильного управления зависимостями классов. Event-Driven Design, безусловно, хороший пример.
Изображение через Fotolia