Pull to refresh

Comments 35

Про Pimple было уже, как минимум, в контексте Silex + упоминалось в описании Inversion. Кроме последнего еще стоит вспомнить Chernozem.
Или я чего-то не понимаю, или получение сервисов через container['service'] — это паттерн под названием Service Locator, но никак не DI.
Мы можем инъецировать объекты и параметры во время конструирования сервиса.

В том же примере:

// Объявление сервисов
$container['session_storage'] = function ($c) {
  return new $c['session_storage_class']($c['cookie_name']);
};
$container['session'] = function ($c) {
  return new Session($c['session_storage']);
};


сервис session_storage передается в виде аргумента в конструктор класса Session.
Ну вы это вручную делаете на этапе конфигурации. То есть классический Service Locator. DI-контейнер же должен сам разбираться, кому какие аргументы конструктора вбить.
Как раз таки нет. Возьмем DiC Symfony — мы так же в конфигурации сервиса указываем каким аргументом что подавать.

Для того что бы использовать Pimple полноценно как DiC, а не как Service Locator, просто необходимо и контроллеры приложения обернуть как сервис — появится возможность передавать как аргументы другие сервисы, или назначать их через setter-ы.

$container['user_controller'] = $container->share(function ($c) {
    return new UserController(
        $c['session_storage']
    );
});
Ну вот если посмотреть на DI из мира языков с поддержкой reflection, то можно заметить, что они ощутимо иначе работают.

Предположим, у нас есть вот такой набор классов:
class FooImpl : IFoo
{
    Foo(IBar dependency){}
}
class BarImpl : IBar
{
}
class Target
{
    Target(IFoo dependency)
}


В случае с DI мы делаем примерно так
//Тут привязки в произвольном порядке
container.Bind<IFoo>().To<FooImpl>();
container.Bind<IBar>().To<BarImpl>().InSingletonScope();
//Просим контейнер создать экземпляр класса, имеющего зависимости.
container.Resolve<Target>();

Теперь посмотрим на код с использованием паттерна Service Locator:
locator.Set<IBar>(new BarImpl());
locator.Set<IFoo>(()=> new FooImpl(locator.Get<IBar>());
new Target(locator.Get<IFoo>());

Очень похож на ваш, правда?
У нас появилась необходимость вручную обращаться к локатору за зависимостями при конструировании объектов, а так же необходимость знать, какие зависимости нужны каждому сервису. Это отличительные черты паттерна Service Locator (которому для работы не нужна поддержка вывода информации о типах со стороны языка), из чего я делаю вывод, что в вашем коде используется именно он.
Речь идет о PHP, и у данного языка нет возможности реализовать приведенный вами пример. Service Locator — один из способов реализации DI.

Согласен, в моем последнем примере $cointainer сам себе играет в роли Service Locator. Конфигурируемые же им объекты получают зависимости либо через конструктор, либо через специально отведенный для этого метод при создании, и в остальном коде нам нет необходимости обращаться к Pimple как с Service Locator.
Ну я не спорю с тем, что применяется именно DI (при «деревянном» использовании локатора классы сами его пинают из конструктора, а локатор представляет из себя статический класс/лежит в глобальной переменной), просто указанный контейнер не является DI-контейнером в полной мере.

Я глянул доку по PHP, там вроде есть какой-то Reflection. Его точно никак нельзя применить для реализации полноценного DI-контейнера?
Вполне можно узнать какого типа (класса, интерфейса) аргументы у метода-конструктора через рефлексию и создать по этим названиям интерфейсов объекты. Я реализовывал такой DI на php.
Да, про невозможность реализовать ваш пример на PHP — был не прав. Через Reflection API такое возможно.

Все же я считаю что это только еще один способ реализации DI, и не стал бы их делить на полноценные и неполноценные :)
Просто в данном случае необходимые зависимости конфигурируются в самом классе. Удобнее.
В моём понимании «полноценный DI-контейнер» — это когда мы говорим контейнеру какому интерфейсу что соответствует, просим создать нужный объект, а со всем остальным он думает без нашего участия. А тут надо во-первых дублировать кучу кода (когда вам надо что-то заинжектить в несколько разных классов), во-вторых помнить, где какие параметры у конструкторов. Неудобно, многословно, тратит время.
Конфигурацию контейнера никто не отменял, и вам всеравно придется описывать какому интерфейсу что соответствует. Делать это в отдельных файлах конфигураций или же задавать через api предоставляемое компонентом — дело личное.

К слову, разве основным отличием di от service locator не в том, что при использовании service locator мы завязываемся на конкретную имплементацию сервиса? в этом плане pimple все же реализует паттерн dependency injection за счет возможности переопределять сервисы (класс использующий сервис все-равно должен быть завязан только на интерфейс). Хотя я Фаулера не читал, могу и ошибаться. Поправьте если что.
что при использовании service locator мы завязываемся на конкретную имплементацию сервиса
Нет, не завязываемся, лишь на «интерфейс», который в php достигается утиной типизацией. Строго говоря в статье Pimple используется для Dependency Injection (зависимости всё же приходят через конструктор, а не заполняются в нём через $locator['service']), но сам по себе реализует именно Service Locator, так как представляет собой банальный реестр и не способен самостоятельно создавать инстансы.
Есть в пхп по крайне мере один нормальный DI — Dice
По вашим критериям даже спринговый контейнер не является «полноценным». То, что вы описываете — лишь доп фича контейнера, которую несложно реализовать, в том числе в php. Она не может являться базовой так как не покрывает всех вариантов связей (несколько реализаций интерфейса, коллекции и т.п.)
Ну не знаю, Windsor, Autofac и Ninject это в состоянии делать, с другими DI-контейнерами, я, увы, не работал.
Вопрос не в том умеют или нет, вопрос в том, что эта фича не является фундаментальной для DI-контейнера. Его задача из декларативной конфигурации уметь создавать объект и инжектить разные типы зависимостей. Данный контейнер вполне в состоянии это делать.
А Spring я привел просто как самый классический пример реализации контейнера.
Я не вижу тут декларативной конфигурации, я вижу тут кучу new и запихиваний в контейнер делегатов (или как в php зовут «указатель на функцию»).
Декларативная она по тому, что вы говорите как создать объект, когда он понадобится. Я не вижу принципиальной разницы между
$container['session'] = function ($c) {
  return new Session($c['sessionStorage']);
};

<component id="session" type="Session">
  <sessionStorage>${sessionStorage}</emailFormatter>
</component>

Есть у такой реализации через замыкания (они так в пхп называются) свои минусы, но в целом для легковесного контейнера вполне применимо.
Если вы поменяете набор параметров конструктора, то придётся снова лезть в конфигурацию, иначе всё развалится, причём о том, что всё развалилось, вы не узнаете до момента, когда попробуете создать этот объект (если у вас есть интеграционные тесты, они это выловят, но всё же). Вся прелесть нормального DI-контейнера в том, что он такие вещи разруливает и достаточно сообразителен, чтобы создать экземпляр любого нужного класса самостоятельно.
В Zend Framework 2 пытались сделать полноценный DI с Reflection. Получилось настолько медленно, что сами создатели рекоммендуют использовать его только для экспериментов.
Это легко решается «кэшированием», что, например, делает Symfony2 DI под названием «скомпилированный контейнер»
А что мешало кэш соорудить? Если один раз собрать необходимую информацию, а потом из него брать, должно летать, нет?
Посмотрите реализацию DI для Magento 2 github.com/magento/magento2/tree/master/lib/Magento/ObjectManager
Он соответствует вашим критериям DI, а именно в конфигурации DI просто говорится какому интерфейсу соответствует какая реализация, а клиентский код напрямую с DI контейнером вообще не работает, просто указывает все сови зависимости в конструкторе. DI контейнер же, узнает о всех зависимостях через декларацию конструкторов и создает объекты либо на лету, либо по разанее «скомпилированной» декларации. Как-то так github.com/magento/magento2/blob/master/lib/Magento/ObjectManager/Factory/Factory.php
В php есть и рефлексии и тайп хинтинг для объектов, так что можно реализовать без проблем. Другой вопрос зачем. Суть pimple в том, что бы быть максимально простым, влазить в минимум строк кода так сказать. Добавление рефлексий при отсутствии системы кеширования сделают его крайне медлительным и увеличат время инициализации сервисов. Добавление рефлекций и кеширование этого всего превратит pimple в symfony/dependency-injection или любую другую здоровую имплементацию.
Компонент должен быть простым в использовании, а не в реализации, нет?
Строго говоря, и DI, и SL — это способы реализации более общего паттерна IoC, Inversion of Control. Pimple, как тут уже сказали, ближе к SL, с минимальной обвязкой для создания «провайдеров» в виде анонимных функций. Зависимости, как видно в статье, всё равно придётся разрешать вручную. Полноценный DI же разрешает зависимости автоматически, как правило, с учётом полифорфизма подтипов (как раз те привязки интерфейсов к реализациям). Не могу сказать, насколько это реализуемо в PHP, поскольку я его не знаю, но в языках вроде Java для этого приходится использовать отражение. Языки с более продвинутой системой типов дают дополнительные средства, вроде self-type annotations в Scala.
В PHP тоже есть Reflection и он с успехом используется для написания DI контейнеров. Например, в ZF2, Symfony 2, Magento 2
Не спорю, что упоминалось, и были показаны некоторые возможности, но считаю что Pimple достоин отдельной статьи.
Мне одному кажется, что в DI зависимости нужно привязывать к интерфейсу, т.е. интерфейс -> класс реализация, а не к строковому значению, и инстанцировать объекты через конструктор, указывая в нем названия интерфейсов в параметрах.
Можете пояснить? В конструкторе/сеттере обычно прописывается интерфейс (тайп хинтинг поддерживается с версии 5,3), да. То есть нету смысла в di если вы все-равно привязываетесь к конкретной реализации. Или вы о чем-то другом?
Смысл в DI есть, потому что привязки интерфейса к реализации централизуются в одном месте — конфигурации DI-контейнера, где их очень легко и просто менять, а не размазываются по всему коду.
Реализуете один интерфейс и классы, которые реализуют его. Далее в настройках указываете, какой класс для интерфейса использовать. Этот класс легко можно сменить, не меняя названия интерфейса во всех местах в коде. Это и есть основная идея DI. Просто в PHP есть только такие средства это организовать. И кстати, по-моему тайп-хинтинг для классов поддерживался в PHP еще до 5.3.
Это то я понимаю, просто не сразу понял о каких строковых значениях идет речь.
Проблема в том, что у вас не всегда 1 к 1 связь, поэтому в самом общем случае вам придется вводить уникальные айди. А вещи типа @Autowired делаются уже поверх для упрощения.
Sign up to leave a comment.

Articles