0,0
рейтинг
18 июня 2013 в 10:22

Разработка → Проблема инициализации объектов в ООП приложениях на PHP. Поиск решения при помощи шаблонов Registry, Factory Method, Service Locator и Dependency Injection

PHP*
Так уж повелось, что программисты закрепляют удачные решения в виде шаблонов проектирования. По шаблонам существует множество литературы. Классикой безусловно считается книга Банды четырех «Design Patterns» by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides" и еще, пожалуй, «Patterns of Enterprise Application Architecture» by Martin Fowler. Лучшее из того, что я читал с примерами на PHP – это «PHP Objects, Patterns and Practice» by Matt Zandstra. Так уж получилось, что вся эта литература достаточно сложна для людей, которые только начали осваивать ООП. Поэтому у меня появилась идея изложить некоторые паттерны, которые я считаю наиболее полезными, в сильно упрощенном виде. Другими словами, эта статья – моя первая попытка интерпретировать шаблоны проектирования в KISS стиле.
Сегодня речь пойдет о том, какие проблемы могут возникнуть с инициализацией объектов в ООП приложении и о том, как можно использовать некоторые популярные шаблоны проектирования для решения этих проблем.

Пример


Современное ООП приложение работает с десятками, сотнями, а иногда и тысячами объектов. Что же, давайте внимательно посмотрим на то, каким образом происходит инициализация этих объектов в наших приложениях. Инициализация объектов – это единственный аспект, который нас интересует в данной статье, поэтому я решил опустить всю «лишнюю» реализацию.
Допустим, мы создали супер-пупер полезный класс, который умеет отправлять GET запрос на определенный URI и возвращать HTML из ответа сервера. Чтобы наш класс не казался чересчур простым, пусть он также проверяет результат и бросает исключение в случае «неправильного» ответа сервера.

class Grabber
{
    public function get($url) {/** returns HTML code or throws an exception */}
}


Создадим еще один класс, объекты которого будут отвечать за фильтрацию полученного HTML. Метод filter принимает в качестве аргументов HTML код и CSS селектор, а возвращает он пусть массив найденных элементов по заданному селектору.

class HtmlExtractor
{
    public function filter($html, $selector) {/** returns array of filtered elements */}
}


Теперь, представим, что нам нужно получить результаты поиска в Google по заданным ключевым словам. Для этого введем еще один класс, который будет использовать класс Grabber для отправки запроса, а для извлечения необходимого контента класс HtmlExtractor. Так же он будет содержать логику построения URI, селектор для фильтрации полученного HTML и обработку полученных результатов.

class GoogleFinder
{
    private $grabber;
    private $filter;

    public function __construct()
    {
        $this->grabber = new Grabber();
        $this->filter = new HtmlExtractor();
    }

    public function find($searchString) { /** returns array of founded results */}
}


Вы заметили, что инициализация объектов Grabber и HtmlExtractor находится в конструкторе класса GoogleFinder? Давайте подумаем, насколько это удачное решение.
Конечно же, хардкодить создание объектов в конструкторе не лучшая идея. И вот почему. Во-первых, мы не сможем легко подменить класс Grabber в тестовой среде, чтобы избежать отправки реального запроса. Справедливости ради, стоит сказать, что это можно сделать при помощи Reflection API. Т.е. техническая возможность существует, но это далеко не самый удобный и очевидный способ.
Во-вторых, та же проблема возникнет, если мы захотим повторно использовать логику GoogleFinder c другими реализациями Grabber и HtmlExtractor. Создание зависимостей жестко прописано в конструкторе класса. И в самом лучшем случае у нас получится унаследовать GoogleFinder и переопределить его конструктор. Да и то, только если область видимости свойств grabber и filter будет protected или public.
И последний момент, каждый раз при создании нового объекта GoogleFinder в памяти будет создаваться новая пара объектов-зависимостей, хотя мы вполне можем использовать один объект типа Grabber и один объект типа HtmlExtractor в нескольких объектах типа GoogleFinder.
Я думаю, что вы уже поняли, что инициализацию зависимостей нужно вынести за пределы класса. Мы можем потребовать, чтобы в конструктор класса GoogleFinder передавались уже подготовленные зависимости.

class GoogleFinder
{
    private $grabber;
    private $filter;

    public function __construct(Grabber $grabber, HtmlExtractor $filter)
    {
        $this->grabber = $grabber;
        $this->filter = $filter;
    }

    public function find($searchString) { /** returns array of founded results */}
}


Если мы хотим предоставить другим разработчикам возможность добавлять и использовать свои реализации Grabber и HtmlExtractor, то стоит подумать о введении интерфейсов для них. В данном случае это не только полезно, но и необходимо. Я считаю, что если в проекте мы используем только одну реализацию и не предполагаем создание новых в будущем, то стоит отказаться от создания интерфейса. Лучше действовать по ситуации и сделать простой рефакторинг, когда в нем появится реальная необходимость.
Теперь у нас есть все нужные классы и мы можем использовать класс GoogleFinder в контроллере.

class Controller
{
    public function action()
    {
        /* Some stuff */

        $finder = new GoogleFinder(new Grabber(), new HtmlExtractor());
        $results = $finder->find('search string');

        /* Do something with results */
    }
}


Подведем промежуточный итог. Мы написали совсем немного кода, и на первый взгляд, не сделали ничего плохого. Но… а что если нам понадобится использовать объект типа GoogleFinder в другом месте? Нам придется продублировать его создание. В нашем примере это всего одна строка и проблема не так заметна. На практике же инициализация объектов может быть достаточно сложной и может занимать до 10 строк, а то и более. Так же возникают другие проблемы типичные для дублирования кода. Если в процессе рефакторинга понадобится изменить имя используемого класса или логику инициализации объектов, то придется вручную поменять все места. Я думаю, вы знаете как это бывает :)
Обычно с хардкодом поступают просто. Дублирующиеся значения, как правило, выносятся в конфигурацию. Это позволяет централизованно изменять значения во всех местах, где они используются.

Шаблон Registry.


Итак, мы решили вынести создание объектов в конфигурацию. Давайте сделаем это.

$registry = new ArrayObject();

$registry['grabber'] = new Grabber();
$registry['filter'] = new HtmlExtractor();
$registry['google_finder'] = new GoogleFinder($registry['grabber'], $registry['filter']);

Нам остается только передать наш ArrayObject в контроллер и проблема решена.

class Controller
{
    private $registry;

    public function __construct(ArrayObject $registry)
    {
        $this->registry = $registry;
    }

    public function action()
    {
        /* Some stuff */

        $results = $this->registry['google_finder']->find('search string');

        /* Do something with results */
    }
}


Можно дальше развить идею Registry. Унаследовать ArrayObject, инкапсулировать создание объектов внутри нового класса, запретить добавлять новые объекты после инициализации и т.д. Но на мой взгляд приведенный код в полной мере дает понять, что из себя представляет шаблон Registry. Этот шаблон не относится к порождающим, но он в некоторой степени позволяет решить наши проблемы. Registry – это всего лишь контейнер, в котором мы можем хранить объекты и передавать их внутри приложения. Чтобы объекты стали доступными, нам необходимо их предварительно создать и зарегистрировать в этом контейнере. Давайте разберем достоинства и недостатки этого подхода.
На первый взгляд, мы добились своей цели. Мы перестали хардкодить имена классов и создаем объекты в одном месте. Мы создаем объекты в единственном экземпляре, что гарантирует их повторное использование. Если изменится логика создания объектов, то отредактировать нужно будет только одно место в приложении. Как бонус мы получили, возможность централизованно управлять объектами в Registry. Мы легко можем получить список всех доступных объектов, и провести с ними какие-нибудь манипуляции. Давайте теперь посмотрим, что нас может не устроить в этом шаблоне.
Во-первых, мы должны создать объект перед тем как зарегистрировать его в Registry. Соответственно, высока вероятность создания «ненужных объектов», т.е. тех которые будут создаваться в памяти, но не будут использоваться в приложении. Да, мы можем добавлять объекты в Registry динамически, т.е. создавать только те объекты, которые нужны для обработки конкретного запроса. Так или иначе контролировать это нам придется вручную. Соответственно, со временем поддерживать это станет очень тяжело.
Во-вторых, у нас появилась новая зависимость у контроллера. Да, мы можем получать объекты через статический метод в Registry, чтобы не передавать Registry в конструктор. Но на мой взгляд, не стоит этого делать. Статические методы, это даже более жесткая связь, чем создание зависимостей внутри объекта, и сложности в тестировании (вот неплохая статья на эту тему).
В-третьих, интерфейс контроллера ничего не говорит нам о том, какие объекты в нем используются. Мы можем получить в контроллере любой объект доступный в Registry. Нам тяжело будет сказать, какие именно объекты использует контроллер, пока мы не проверим весь его исходный код.

Factory Method


В Registry нас больше всего не устраивает то, что объект необходимо предварительно инициализировать, чтобы он стал доступным. Вместо инициализации объекта в конфигурации, мы можем выделить логику создания объектов в другой класс, у которого можно будет «попросить» построить необходимый нам объект. Классы, которые отвечают за создание объектов называют фабриками. А шаблон проектирования называется Factory Method. Давайте посмотрим на пример фабрики.

class Factory
{
    public function getGoogleFinder()
    {
        return new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor());
    }

    private function getGrabber()
    {
        return new Grabber();
    }

    private function getHtmlExtractor()
    {
        return new HtmlFiletr();
    }
}


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

class Factory
{
    private $finder;

    public function getGoogleFinder()
    {
        if (null === $this->finder) {
            $this->finder = new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor());
        }

        return $this->finder;
    }
}


Мы можем параметризировать метод фабрики и делегировать инициализацию другим фабрикам в зависимости от входящего параметра. Это уже будет шаблон Abstract Factory.
Если появится необходимость разбить приложение на модули, мы можем потребовать, чтобы каждый модуль предоставлял свои фабрики. Мы можем и дальше развивать тему фабрик, но думаю что суть этого шаблона понятна. Давайте посмотрим как мы будем использовать фабрику в контроллере.

class Controller
{
    private $factory;

    public function __construct(Factory $factory)
    {
        $this->factory = $factory;
    }

    public function action()
    {
        /* Some stuff */

        $results = $this->factory->getGoogleFinder()->find('search string');

        /* Do something with results */
    }
}


К преимуществам данного подхода, отнесем его простоту. Наши объекты создаются явно, и Ваша IDE легко приведет Вас к месту, в котором это происходит. Мы также решили проблему Registry и объекты в памяти будут создаваться только тогда, когда мы «попросим» фабрику об этом. Но мы пока не решили, как поставлять контроллерам нужные фабрики. Тут есть несколько вариантов. Можно использовать статические методы. Можно предоставить контроллерам самим создавать нужные фабрики и свести на нет все наши попытки избавиться от копипаста. Можно создать фабрику фабрик и передавать в контроллер только ее. Но получение объектов в контроллере станет немного сложнее, да и нужно будет управлять зависимостями между фабриками. Кроме того не совсем понятно, что делать, если мы хотим использовать модули в нашем приложении, как регистрировать фабрики модулей, как управлять связями между фабриками из разных модулей. В общем, мы лишились главного преимущества фабрики – явного создания объектов. И пока все еще не решили проблему «неявного» интерфейса контроллера.

Service Locator


Шаблон Service Locator позволяет решит недостаток разрозненности фабрик и управлять созданием объектов автоматически и централизованно. Если подумать, мы можем ввести дополнительный слой абстракции, который будет отвечать за создание объектов в нашем приложении и управлять связями между этими объектами. Для того чтобы этот слой смог создавать объекты для нас, мы должны будем наделить его знаниями, как это делать.
Термины шаблона Service Locator:
  • Сервис (Service) — готовый объект, который можно получить из контейнера.
  • Описание сервиса (Service Definition) – логика инициализации сервиса.
  • Контейнер (Service Container) – центральный объект который хранит все описания и умеет по ним создавать сервисы.
Любой модуль может зарегистрировать свои описания сервисов. Чтобы получить какой-то сервис из конейнера мы должны будем запросить его по ключу. Существует масса вариантов реализации Service Locator, в простейшем варианте мы можем использовать ArrayObject в качестве контейнера и замыкания, в качестве описания сервисов.

class ServiceContainer extends ArrayObject
{
    public function get($key)
    {
        if (is_callable($this[$key])) {
            return call_user_func($this[$key]);
        }

        throw new \RuntimeException("Can not find service definition under the key [ $key ]");
    }
}


Тогда регистрация Definitions будет выглядеть так:

$container = new ServiceContainer();

$container['grabber'] = function () {
    return new Grabber();
};

$container['html_filter'] = function () {
    return new HtmlExtractor();
};

$container['google_finder'] = function() use ($container) {
    return new GoogleFinder($container->get('grabber'), $container->get('html_filter'));
};


А использование, в контроллере так:

class Controller
{
    private $container;

    public function __construct(ServiceContainer $container)
    {
        $this->container = $container;
    }

    public function action()
    {
        /* Some stuff */

        $results = $this->container->get('google_finder')->find('search string');

        /* Do something with results */
    }
}


Service Container может быть очень простым, а может быть очень сложным. Например, Symfony Service Container предоставляет массу возможностей: параметры (parameters), области видимости сервисов (scopes), поиск сервисов по тегам (tags), псевдонимы (aliases), закрытые сервисы (private services), возможность внести изменения в контейнер после добавления всех сервисов (compiller passes) и еще много чего. DIExtraBundle еще больше расширяет возможности стандартной реализации.
Но, вернемся к нашему примеру. Как видим, Service Locator не только решает все те проблемы, что и предыдущие шаблоны, но и позволяет легко использовать модули с собственными определениями сервисов.
Кроме того, на уровне фреймворка мы получили дополнительный уровень абстракции. А именно, изменяя метод ServiceContainer::get мы сможем, например, подменить объект на прокси. А область применения прокси-объектов ограниченна лишь фантазией разработчика. Тут можно и AOP парадигму реализовать, и LazyLoading и т.д.
Но, большинство разработчиков, все таки считают Service Locator анти-паттерном. Потому что, в теории, мы можем иметь сколько угодно т.н. Container Aware классов (т.е. таких классов, которые содержат в себе ссылку на контейнер). Например, наш Controller, внутри которого мы можем получить любой сервис.
Давайте, посмотрим, почему это плохо.
Во-первых, опять же тестирование. Вместо того, чтобы создавать моки только для используемых классов в тестах придется делать мок всему контейнеру или использовать реальный контейнер. Первый вариант не устраивает, т.к. приходится писать много ненужного кода в тестах, второй, т.к. он противоречит принципам модульного тестирования, и может привести к дополнительным издержкам на поддержку тестов.
Во-вторых, нам будет трудно рефакторить. Изменив любой сервис (или ServiceDefinition) в контейнере, мы будем вынуждены проверить также все зависимые сервисы. И эта задача не решается при помощи IDE. Отыскать такие места по всему приложению будет не так-то и просто. Кроме зависимых сервисов, нужно будет еще проверить все места, где отрефакторенный сервис получается из контейнера.
Ну и третья причина в том, что бесконтрольное дергание сервисов из контейнера рано или поздно приведет к каше в коде и излишней путанице. Это сложно объяснить, просто Вам нужно будет тратить все больше и больше времени, чтобы понять как работает тот или иной сервис, иными словами полностью понять что делает или как работает класс можно будет только прочитав весь его исходный код.

Dependency Injection


Что же можно еще предпринять, чтобы ограничить использование контейнера в приложении? Можно передать в фреймворк управление созданием всех пользовательских объектов, включая контроллеры. Иными словами, пользовательский код не должен вызывать метод get у контейнера. В нашем примере мы cможем добавить в контейнер Definition для контроллера:

$container['google_finder'] = function() use ($container) {
    return new Controller(Grabber $grabber);
};


И избавиться от контейнера в контроллере:

class Controller
{
    private $finder;

    public function __construct(GoogleFinder $finder)
    {
        $this->finder = $finder;
    }

    public function action()
    {
        /* Some stuff */

        $results = $this->finder->find('search string');

        /* Do something with results */
    }
}


Такой вот подход (когда доступ к Service Container не предоставляется клиентским классам) называют Dependency Injection. Но и этот шаблон имеет как преимущества, так и недостатки. Пока у нас соблюдается принцип единственной ответственности, то код выглядит очень красиво. В-первую очередь, мы избавились от контейнера в клиентских классах, благодаря чему их код стал намного понятнее и проще. Мы легко можем протестировать контроллер, подменив необходимые зависимости. Мы можем создавать и тестировать каждый класс независимо от других (в том числе и классы контроллеров) используя TDD или BDD подход. При создании тестов мы сможем абстрагироваться от контейнера, и позже добавить Definition, когда нам понадобится использовать конкретные экземпляры. Все это сделает наш код проще и понятнее, а тестирование прозрачнее.
Но, необходимо упомянуть и об обратной стороне медали. Дело в том, что контроллеры – это весьма специфичные классы. Начнем с того, что контроллер, как правило, содержит в себе набор экшенов, значит, нарушает принцип единственной ответственности. В результате у класса контроллера может появиться намного больше зависимостей, чем необходимо для выполнения конкретного экшена. Использование отложенной инициализации (объект инстанцианируется в момент первого использования, а до этого используется легковесный прокси) в какой-то мере решает вопрос с производительностью. Но с точки зрения архитектуры создавать множество зависимостей у контроллера тоже не совсем правильно. Кроме того тестирование контроллеров, как правило излишняя операция. Все, конечно, зависит от того как тестирование организовано в Вашем приложении и от того как вы сами к этому относитесь.
Из предыдущего абзаца Вы поняли, что использование Dependency Injection не избавляет полностью от проблем с архитектурой. Поэтому, подумайте как Вам будет удобнее, хранить в контроллерах ссылку на контейнер или нет. Тут нет единственно правильного решения. Я считаю что оба подхода хороши до тех пор, пока код контроллера остается простым. Но, однозначно, не стоит создавать Conatiner Aware сервисы помимо контроллеров.

Выводы


Ну вот и пришло время подбить все сказанное. А сказано было немало… :)
Итак, чтобы структурировать работу по созданию объектов мы можем использовать следующие паттерны:
  • Registry: Шаблон имеет явные недостатки, самый основной из которых, это необходимость создавать объекты перед тем как положить их в общий контейнер. Очевидно, что мы получим скорее больше проблем, чем выгоды от его использования. Это явно не лучшее применение шаблона.
  • Factory Method: Основное достоинство паттерна: объекты создаются явно. Основной недостаток: контроллеры должны либо сами беспокоиться о создании фабрик, что не решает проблему хардкода имен классов полностью, либо фреймворк должен отвечать за снабжение контроллеров всеми необходимыми фабриками, что будет уже не так очевидно. Отсутствует возможность централизованно управлять процессом создания объектов.
  • Service Locator: Более «продвинутый» способ управлять созданием объектов. Дополнительный уровень абстракции может быть использован, чтобы автоматизировать типичные задачи встречающиеся при создании объектов. Например:
    class ServiceContainer extends ArrayObject
    {
        public function get($key)
        {
            if (is_callable($this[$key])) {
                $obj = call_user_func($this[$key]);
    
                if ($obj instanceof RequestAwareInterface) {
                    $obj->setRequest($this->get('request'));
                }
    
                return $obj;
            }
    
            throw new \RuntimeException("Can not find service definition under the key [ $key ]");
        }
    }
    

    Недостаток Service Locator в том, что публичный API классов перестает быть информативным. Необходимо прочитать весь код класса, чтобы понять, какие сервисы в нем используются. Класс, который содержит ссылку на контейнер сложнее протестировать.
  • Dependency Injection: По сути мы можем использовать тот же Service Container, что и для предыдущего паттерна. Разница в том, как этот контейнер используется. Если мы будем избегать создания классов зависимых от контейнера, мы получим четкий и явный API классов.
Это не все, что я хотел бы рассказать о проблеме создания объектов в PHP приложениях. Есть еще паттерн Prototype, мы не рассмотрели использование Reflection API, оставили в стороне проблему ленивой загрузки сервисов да и еще много других нюансов. Статья получилась не маленькая, потому закругляюсь :)
Я хотел показать, что Dependency Injection и другие паттерны не так уж и сложны, как принято считать.
Если говорить о Dependency Injection, то существуют и KISS реализации этого паттерна, например Pimple, который занимает всего пару сотен строк вместе с комментариями. И есть такие монстры, как Symfony Dependency Injection Component. Базовый принцип один и тот же, а вот набор предоставляемых возможностей несопоставим.
Что же, надеюсь было интересно и вы не зря потратили время на чтение статьи.

Have fun!

P.S. Большое спасибо, всем кто нашел время и откликнулся на мою просьбу о помощи. Мне важно было знать Ваше мнение ;)
Артем Колесников @tyomo4ka
карма
35,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (31)

  • +1
    Хорошая статья, согласен с вами в рекомендации книг, Зандстра опирался на «Банду четырех», постоянно подчеркивая это в своей книге.
    Вы уже упоминали о компонентах Symfony, хотелось бы добавить, что Symfony это набор всех паттернов, которые только есть и можно круто прокачать свой скилл, просто изучив исходники с помощью своей любимой IDE.
    • 0
      Да и книга хорошая. Помню когда увидел ее на книжном рынке, сразу же купил. Она очень доступна для понимания.
  • НЛО прилетело и опубликовало эту надпись здесь
    • +2
      Dependency Injection — явная передача объектов в методы класса, только так. Либо в конструктор (идеальный вариант), либо в init-методы (initServiceA()).

      Здесь не ошибка, а скорее путаница определений, свойственная многим статьям/мануалам. Под заголовком Dependency Injection в статье описан Dependency Injection Container (DIC). Очень часто, когда пишут о DIС, «Container» опускают. На самом деле DI можно использовать без контейнера, как описано в примере в начале:

      public function __construct(Grabber $grabber, HtmlExtractor $filter)
      

      Очень хорошо это поясняет Anthony Ferrara в обучающем видео:
      www.youtube.com/watch?v=IKD2-MAkXyQ
      • +1
        Спасибо, за отзывы. В статье я попытался объяснить больше «практическую проблему», т.е. почему не стоит дубилровать код создания объектов, почему стоит выносить инциализацию объектов за пределы класса, и каким образом можно организовать работу по созданию объектов внутри приложения. Насколько я понимаю, BoneFletcher прав, слово Container обычно опускают, когда говорят о DI. И, если не ошибаюсь, тот же Фаулер об этом писал. Вообще, в терминологии тяжело запутаться :) Так что я не стану спорить…
  • 0
    Однозначно, в закладки. Спасибо за хорошую публикацию
    • +3
      Спасибо, за отзыв. Приятно знать, что не зря старался :)
  • 0
    Можно параметры объекта описывать в конфиге:
    $configGF = [
        'filter' => 'MyMock',
    ];
    
    $finder = new GoogleFinder($config);
    


    А уже GoogleFinder в своём конструкторе смотрит в конфиг и для $this->finder создаёт экземпляр класса MyMock. А для тех, что не определены, использует классы по умолчанию ($this->grabber = new Grabber()).
    • +1
      Да, конечно. Я просто хотел показать самую простую реализацию :)
    • +1
      Пару раз использовал подобный прием, но почему-то конструкция типа $this->filter = new $configGF['filter]; (равно как $function_name()) мне жутко не нравится. Вы часто используете?
      • +1
        Не в конструкции дело. Конструкции можно любые сделать, да и PHP итак не самый красивый язык в плане конструкций.

        Я о самом принципе.
        Есть класс, экземпляры которого могут иметь настраиваемое поведение (реализации вложенных подсистем лишь один из видов настройки).
        Отдельно пишем класс по возможности обобщённо, отдельно конфиги к нему. А объект сам по своему конфигу себя инициализирует, как хочет.

        Не надо вызывать потом отдельно методы, подсовывать ему незаметно какие-то объекты в поля. Сразу после конструктора у нас завершённый, неизменяемый, готовый к работе объект.
  • 0
    Есть вопрос. Даже два.
    1. В статье, как пример реализации паттерна Service locator, написан ServiceContainer, он хранит все пользовательские объекты. Так почему же он называется ServiceContainer? В нем не храняться классы-сервисы, которые как раз и вводят слой сервисов. Он просто используется для разруливания зависимостей под капотом.
    2. DIC — это реализация паттерна Service locator?
    • +1
      1) Тут слой сервисов немного не причем :) Идея Service Locator в том чтобы отвязать логику инициализации объекта от логики его использования. Ты просто как бы говоришь контейнеру, дай мне этот объект. И он создает его со всеми зависимостями, которые тебе нужны или берет уже существующий объект, если он был инициализирован раньше. Не знаю, почему назвали именно Service Locator, как-то видимо так исторически сложилось :)

      2) Не совсем так. Вы можете использовать и там и там одну реализацию контейнера. Когда вы создаете классы, которые будут сами обращаться к контейнеру и получать у него нужные сервисы – это Service Locator. Когда фреймворк берет на себя все обязанности по созданию объектов и у Вас нет клиентских классв, которые имеют зависимость на контейнер – это Dependency Injection. Т.е. в случае с Web приложением. Фреймворк принимает запрос, сопоставляет его сервису (вы должны будете в конфигруации роутов указать ключ по которому сервис доступен а контейнере), получает сервис из контейнера и передает в него запрос. Как-то так.
      • +1
        Вот и я о том же, сервисы (слой сервисов) тут ни при чем. Т.е. ServiceContainer хранит на самом деле не сервисы, а произвольные пользовательские объекты? Если да, то в чем тогда отличие от DIC? DIC тоже хранит пользовательские объекты, и автоматом по перому требованию инициализирует зависимости.

        Начал читать второй ваш абзац, и понимать разницу между DI и Service locator. Dependecy Injection — это паттерн, при котором необходимые зависимости содержаться как свойства зависимого класса, а Service locator — при котором зависимый класс является ServiceLocatorAware и получает зависимости у контейнера напрамую.

        Все верно?
        • +1
          Да, верно. DI контейнер предполагает ни один клиентский класс, ничего не знает о том, что какой-то контейнер вообще существует. В одном месте фреймворк получает один! необходимый сервис и передает управление ему.
  • +1
    Спасибо за интересную статью — как раз сейчас интересуюсь этой темой :)
  • +1
    Спасибо за статью, освежил и структурировал знания :)
    Будут ли еще статьи в таком же ключе?
    • +3
      Я планировал еще статью конкретно по Symfony Dependency Injecton Component. Хочется поделиться некоторыми идеями и получить на них фидбек. Но я достаточно кропотливо отношусь к написанию статей, поэтому не обещаю, что это будет скоро :)
  • +1
    Еще такой вопрос: а если у нас в классе есть метод, в котором нужно использовать не один какой-то конкретный объект, который мы можем передать в аргументе, а нужно создать и использовать несколько объектов определенного класса? Как в таком случае поможет DI?
    • +1
      Как правило, Вам нужно получать один и тот же экземпляр класса из контейнера. Для этого можно сохранять (кэшировать) готовые объекты в свойство контейнера (ключ сервиса => готовый объект). Потом простая проверка, если объект тут, возвращаем, если не тут, создаем и ложим в кэш.

      Иногда бывает нужно каждый раз получать новый экземпляр класса. Для этого Вам всего лишь нужно перестать кэшировать объект (либо просто клонировать первый созданный). В моем коде вы будете получать каждый раз новый объект.
      • +1
        Я немного другое имел в виду. Возьмем такой примитивный пример:

        class BooksService {
        	
        	public function booksMethod()
        	{
        		$book1 = new Book;
        		$book1->someAttr = 'val1';
        		$book1->someMethod();
        
        		// ...
        
        		$book5 = new Book;
        		$book5->someAttr = 'val5';
        		$book5->anotherAttr = 'anotherVal';
        		$book5->someMethod();
        
        		// ...
        	}
        }
        


        Как мне с помощью Dependency injection добиться того, чтобы класс BooksService не зависел жестко от класса Book, и также не был бы Container Aware классом? Такое возможно?
        • +1
          Да, в принципе. Все зависит от того насколько реален пример и почему Вам так надо сделать :) В Вашем случае я вижу 2 решения.

          1) Через контейнер заинжектить 1 Book и клонировать его по необходимости.
          2) Некоторые контейнеры поддерживают постинициализацию. Вы можете создать метод addBook(Book $book) и попросить контейнер, чтобы он вызвал этот метод после создания объекта.

          Вообще, контейнер не предназначен для работы с сущностями. Инжектить что-нибудь в сущности считается плохой практикой, как и инжектить сущности в сервисы. В Вашем случае я бы не рекомендовал использовать контейнер.
          • +1
            Насчет клонирования объекта — смысл понятен)
            А что бы вы порекомендовали в случае сущностей и сервисов? Если не использовать DI контейнер, то оставлять их жестко зависимыми от других классов?
            Где-нибудь есть хорошее описание таких вещей — для каких случаев контейнер предназначен/не предназначен, насколько правильно инжектить в сущности и т.д.?
            • +2
              Вы знаете, этот вопрос достаточно сложный и тут нет однозначных ответов. Все зависит от того каких взглядов на Модель вы придерживаетесь. Если вы сторонник анемичных моделей (в модели только данные, простые хелперы и отношения между сущностями, бизнесс логика в слое сервисов), то не станете мешать в кучу бизнесс логику и сущности, соответственно с вашей точки зрения будет неправильно ложить инициализацию сущностей в контейнер. Если вы сторонник Rich Model, то там будут работать совсем другие принципы и правила.

              Я не стану Вам в принципе давать никаких рекомендаций на этот счет. Но если хотите знать мое мнение, то я считаю, что жесткая связь оправдана в сущностях. Я бы даже запретил использовать интерфейсы для сущностей :) Так или иначе вы работатете не с абстракциями а объектами вполне конкретного типа. Если используется наследования, то можно использовать зависимости на базовый класс. С моей! точки зрения это правильно. Я думаю вы поняли, что я сторонник анемичной модели :)
              • +1
                Если интересуетесь Rich model и Symfony, советую посмотреть этот доклад 2012.symfonycamp.org.ua/speaker-lineup/kirill-chebunin/ :)
                • +1
                  Если честно, сейчас больше интересует подход с сервисными слоями. Я уже применял принцип «всю логику помещать в модель» — модели очень сильно разрастались)
              • +1
                Понятно =) Как бы то ни было, спасибо за информацию)
              • +2
                Все зависит от того каких взглядов на Модель вы придерживаетесь.

                Редкая политкорректность :)
        • +2
          Это можно сделать через фабрику:

          class BooksService
          {
              private $bookFactory;
          
              public function __construct($bookFactory)
              {
                  $this->bookFactory = $bookFactory;
              }
              
              public function booksMethod()
              {
                  $book1 = $this->bookFactory->create();
                  $book5 = $this->bookFactory->create();
              }
          }
          

          Но только если это реально необходимо. Если можно обойтись без абстракций, то лучше делать без них.
  • 0
    Мне кажется, что при написании раздела о DI, Вы имели ввиду:
    return new Controller(GoogleFinder $finder);
    
    • 0
      Скорее так:

      return new Controller($container->get('finder'));
      

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.