Как стать автором
Обновить

Комментарии 30

Т.е., шаблон уменьшает возможности переиспользования кода некоторого класса по двум причинам: во-первых, из-за лишней зависимости класса от самого Локатора
то, а) мы делаем его независимым от наличия Локатора (например, в тестовой среде),

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


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

Это несколько неправда. Современные DI-контейнеры прекрасно умеют инициализацию по требованию.


В таком варианте использования паттерн Service Locator вызывает во мне положительные эмоции и не вызывает отрицательных.

Ну то есть то, что чтобы запустить объект в тестах, вам надо посмотреть (по коду класса), какие зависимости надо ему положить в локатор — это не вызывает у вас отрицательных эмоций?


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


(я уж молчу про всякие мелочи типа потокобезопасности или однократной инициализации зависимостей, предполагаю, что вы их просто опустили для простоты, а не забыли)

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

public function __construct($container = null)

В PHP так можно.


Это несколько неправда. Современные DI-контейнеры прекрасно умеют инициализацию по требованию.

Значит это всё-таки несколько правда и старые DI-контейнеры плохо умеют инициализацию по требованию. И приведите, пожалуйста, пример современного DI-контейнера, который "прекрасно умеет инициализацию по требованию". Желательно на PHP или JS — это мои активные языки на данный момент.


Ну то есть то, что чтобы запустить объект в тестах, вам надо посмотреть (по коду класса), какие зависимости надо ему положить в локатор — это не вызывает у вас отрицательных эмоций?

Зачем вообще локатор/контейнер при тестах? Инжектите зависимости через setter'ы (или конструктор). И да, чтобы тестировать класс нужно залезть в его код. И нет, не обязательно лезть в код, если у вас есть описание публичных интерфейсов класса и ожидаемое поведение.


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

А это оборотная сторона медали. За всё нужно чем-то платить. Кстати, бойлерплейтить по-новому нужно только те зависимости, которые должны pull'аться в рабочем режиме. Остальные нужно бойлерплейтить по-старому.


(я уж молчу про всякие мелочи типа потокобезопасности или однократной инициализации зависимостей, предполагаю, что вы их просто опустили для простоты, а не забыли)

Нет, не забыл. Я про них даже не думал. В PHP в web-приложениях вообще один поток. Но можно сделать и много. Но обычно не делают. А что, pull-способ использования контейнера менее потокобезопасен, чем push-способ? И это, что за зверь такой "однократная инициализация зависимостей"? Каким образом вы предполагаете в конструктор заталкивать зависимости многократно?

Зачем вообще локатор/контейнер при тестах? Инжектите зависимости через setter'ы (или конструктор). И да, чтобы тестировать класс нужно залезть в его код. И нет, не обязательно лезть в код, если у вас есть описание публичных интерфейсов класса и ожидаемое поведение.

А если мы интеграционный тест пишем и нам не очень хочется писать кастомную логику инициализации всего дерева требуемых объектов?


В PHP так можно.

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

А если мы интеграционный тест пишем и нам не очень хочется писать кастомную логику инициализации всего дерева требуемых объектов?

Тогда не пишите кастомную логику, используйте обычную. Это конкретно коллега lair хотел запустить конкретный объект в конкретных тестах.


Код с тайп-хинтами лучше анализируется статически, предотвращает ошибки еще до запуска и тем более деплоя.

Полностью согласен. Если класс предполагается использовать в рамках проекта, где интерфейс IContainer определён, то лучше его использовать. Но если очень сильно хочется отвязаться от этого интерфейса, то PHP это позволяет.

В PHP так можно.

А вы говорите про паттерн, или про его реализацию в конкретном языке?


И приведите, пожалуйста, пример современного DI-контейнера, который "прекрасно умеет инициализацию по требованию".

Autofac. Но вообще — любой, который умеет регистрировать и отдавать Func<...>.


Инжектите зависимости через setter'ы (или конструктор).

Угу. Какие сеттеры надо вызвать, а какие — не обязательно?


А это оборотная сторона медали. За всё нужно чем-то платить.

А говорите, нет отрицательных эмоций.


Я про них даже не думал. В PHP в web-приложениях вообще один поток.

… но при этом вы упоминаете консольные приложения.


А что, pull-способ использования контейнера менее потокобезопасен, чем push-способ?

Не "pull-способ", а ваша реализация. У вас есть сеттеры (и прочие методы), они (по умолчанию для генеричного кода) не потокобезопасны. "Честный" DI делается в конструкторе, конструктор (по тому же умолчанию) потокобезопасен.


Каким образом вы предполагаете в конструктор заталкивать зависимости многократно?

В конкструктор как раз никак. А вот в ваши свойства их можно запихнуть несколько раз. Это выгодное отличие constructor injection от property injection.

А вы говорите про паттерн, или про его реализацию в конкретном языке?

И. У меня чисто утилитарные намерения.


Autofac. Но вообще — любой, который умеет регистрировать и отдавать Func<...>.

А этот Autofac инициализирует зависимости, внедряемые через конструктор или через свойства/акцессоры? Вот как-то я не вижу, каким образом при создании объекта в языках со статической типизацией можно реализовать инициализацию зависимостей по требованию в этом случае:


public function __construct(IDep1 $dep1, IDep2 $dep2, IDep3 $dep3) {}

Возможно, это особенности .Net. Например, в JS я могу в контейнере использовать Proxy и проинициализировать зависимости $depX при первом обращении к нему, но в PHP, насколько мне известно, такого сделать нельзя. Не могли бы вы привести пример кода (или ссылку на него), где Autofac инициализирует по требованию зависимости конструктора?


Угу. Какие сеттеры надо вызвать, а какие — не обязательно?

Все. Вы же тестируете класс.


А говорите, нет отрицательных эмоций.

Я не испытываю отрицательные эмоции, когда в магазине оплачиваю покупку молока.


… но при этом вы упоминаете консольные приложения.

Консольные приложения могут быть однопоточные.


У вас есть сеттеры (и прочие методы), они (по умолчанию для генеричного кода) не потокобезопасны.

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


Это выгодное отличие constructor injection от property injection.

В таком случае просто не используйте внедрение через setter'ы/свойства в коде приложения — будет вам однократная инициализация зависимостей. Используйте внедрение через setter'ы/свойства только для тестов. Опасаетесь за других разрабов в команде — уберите вообще возможность внедрять что-либо через setter'ы/свойства и тестируйте через инициализацию внедряемого в конструктор контейнера, если для вас это приоритет.


Я не говорю, что все и всегда должны применять описанный мной способ (в том числе вы, и в том числе в .Net). Я лишь хочу увидеть весомые аргументы, почему лично мне не стоит применять этот способ в PHP или в JS. И пока не увидел. В том числе и от вас.

И. У меня чисто утилитарные намерения.

Ну то есть первое условие, при котором "Service Locator анти-шаблоном не является" — это "у меня PHP". Так?


А этот Autofac инициализирует зависимости, внедряемые через конструктор или через свойства/акцессоры?

И так, и так. Предпочтительно через конструктор.


Вот как-то я не вижу, каким образом при создании объекта в языках со статической типизацией можно реализовать инициализацию зависимостей по требованию в этом случае:
public function __construct(IDep1 $dep1, IDep2 $dep2, IDep3 $dep3) {}

Очень просто:


Consumer(Lazy<IDep1> dep1, Lazy<IDep2> dep2, Func<IDep3> dep3Factory)

Все. Вы же тестируете класс.

вообще все? У классов может быть много свойств, и не все из них нужны в конкретном тесте.


Если не хотите, можно и без сеттеров — все зависимости тянуть через контейнер в конструкторе. Будет потокобезопасность и не будет переносимости.
[...]
Опасаетесь за других разрабов в команде — уберите вообще возможность внедрять что-либо через setter'ы/свойства и тестируйте через инициализацию внедряемого в конструктор контейнера, если для вас это приоритет.

Но зачем, если можно просто сделать честный DI, и не вбрасывать контейнер? В чем выигрыш от использования сервис-локатора при таких ограничениях?


почему лично мне не стоит применять этот способ в PHP или в JS

Аргументов, почему лично вам не стоит применять этот способ в PHP или JS, я вам и не дам. Я обсуждаю заявленную в заголовке тему "а анти-паттерн ли service locator". Пока что по всему получается, что как был антипаттерном, так и остался. Просто, как и любой антипаттерн, он иногда все еще имеет свои достоинства и область применения.

Так?

Нет.


Lazy<IDep1> dep1

Полагаю, что Lazy — это такой же прокси, как и Ocramius/ProxyManager. Т.е., создание объекта теперь также привязано к контейнеру, как и в моём случае с внедрением самого контейнера (я не говорю, что это плохо — это то, чем приходится платить). Такой вопрос — Lazy также физически генерирует код на диске/в памяти, как и Ocramius/ProxyManager?


У классов может быть много свойств, и не все из них нужны в конкретном тесте.

Тестируйте те, которые нужны.


В чем выигрыш от использования сервис-локатора при таких ограничениях?

Генерация зависимостей по требованию без использования Lazy-прокси. Если Lazy-прокси физически генерирует код на диске — то вот и выигрыш. А если нет — то и выигрыша нет, проксирование лучше. В JS, по крайней мере, подход с прокси точно можно применить, там это на лету делается. А вот в PHP — без кодогенерации не уверен, что можно.


Просто, как и любой антипаттерн, он иногда все еще имеет свои достоинства и область применения.

Вот я и набросал, кмк, область, где Service Locator имеет право на жизнь.

Полагаю, что Lazy — это такой же прокси, как и Ocramius/ProxyManager.

Нет, вы неправильно полагаете. Это стандартный класс .net, не имеющий никакого прокси.


Т.е., создание объекта теперь также привязано к контейнеру

Нет, не привязано.


new Consumer(new Lazy<IDep1>(() => new Dep1()))

Никакого контейнера не нужно, зависимость только на стандартную системную библиотеку и, собственно, потребляемую зависимость.


Такой вопрос — Lazy также физически генерирует код на диске/в памяти, как и Ocramius/ProxyManager?

Нет, зачем? Это обычный класс со ссылкой внутри.


Генерация зависимостей по требованию без использования Lazy-прокси.

А в чем проблема с использованием Lazy<T> или Func<T>?


Или, подождите, дайте я угадаю… в языках, на которые вы ссылаетесь, нет дженериков?

Или, подождите, дайте я угадаю… в языках, на которые вы ссылаетесь, нет дженериков?

Ага.

Такой вопрос — Lazy также физически генерирует код на диске/в памяти, как и Ocramius/ProxyManager?

Судя по документации, нет. Очень похоже, что работает прозрачно, без дополнительной кодогенерации. В таком случае у вас в .Net уже есть контейнер, который решает проблему отложенной инициализации через проксирование зависимостей. Для вас (и остальных пользователей Autofac) описанный мною вариант с Service Locator'ом бесполезен.

И приведите, пожалуйста, пример современного DI-контейнера, который «прекрасно умеет инициализацию по требованию». Желательно на PHP или JS — это мои активные языки на данный момент.

Берем github.com/Ocramius/ProxyManager и готово :)

docs.laminas.dev/laminas-servicemanager/lazy-services (использует proxy manager)
symfony.com/doc/current/service_container/lazy_services.html (использует proxy manager)
php-di.org/doc/lazy-injection.html (использует proxy manager)

В-о-о-т! Это то, что я и искал!!! За это спасибо!


Я правильно понимаю, что этот Proxy генерирует код для проксируемых объектов с сохранением его на диске?


Proxy generation causes I/O operations and uses significant amounts of reflection, so be sure to have generated all of your proxies before deploying your code on a live system, or you may experience poor performance.

Ну то есть то, что чтобы запустить объект в тестах, вам надо посмотреть (по коду класса), какие зависимости надо ему положить в локатор — это не вызывает у вас отрицательных эмоций?

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

Но если перефразировать, проблема Service Layer что нет единого места где указаны сразу все зависимости класса. А это уже сильный аргумент

в начале теста для таких классов инициализируются все сервисы приложения

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

Если мы перепишем наш класс с Локатором в таком виде:
то, а) мы делаем его независимым от наличия Локатора (например, в тестовой среде), б) явным образом выделяем зависимости в setter'ах (также можно аннотировать, документировать, ставить префиксы и решать проблему «неочевидности» зависимостей любым другим доступным способом, вплоть до Ctrl+F по ключу "$locator->get" в коде).


То есть вы на полном серьезе считаете такой код нормальным?

Во-первых, он работает. Во-вторых, он решает проблему (не)создания иерархии зависимостей. В-третьих, если есть лучшее решение, я с удовольствием выкину это. В общем-то для того я и написал данную публикацию. У вас есть лучшее решение?

Во-первых, он работает.

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

У вас есть лучшее решение?

У всей индустрии есть лучшее решение — DI
Ну это аргументация джуна

Вы дальше первого пункта не читали? Там есть ещё "во-вторых". В PHP нет generic'ов и нет проксирования "на лету". У всей IT-индустрии нет решения DI с отложенным внедрением зависимостей без дополнительной кодогенерации для PHP. И попробуйте это опровергнуть :)

У меня есть — вручную написанные прокси на анонимных классах :) Нет решения автоматического отложенного внедрения без кодогенерации.

вручную написанные прокси на анонимных классах :)

(y) :))) Было бы любопытно взглянуть. Есть пример кода?

Мне кажется, современные DI-контейнеры с автовайрингом уже давно закрывают любую потребность в Service Locator, при этом никак не влияют на конечный код сервисов — он остается Plain Old название языка. Конечному коду не нужно знать об интерфейсе Локатора и особенностях использования, этот код можно перетащить куда-либо еще.


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

Хех, есть ровно одно место где мне приходится использовать Service Locator. В ДотНете. Во всяких разных кастомных атрибутах. Например, атрибутах авторизации (которые IAsyncAuthorizationFilter, например).
Используя сервис-локатор — вы повышаете внешнюю связанность (coupling), гвоздями забиваете свою программу на тот клей… Почему условный хэндлер для бизнесухи зависит от контейнера и магических результата вызова его метода get? Почему ваш модуль, в рамках которого живет этот хэндлер, не завязан на абстракции и детали этого же модуля? То есть очень низкая внутренная связь (cohesion)…

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

Не хотелось бы работать, где каждый класс скрывает детали своих зависимостей в $this->container и использует «че хочет»
public function __construct(ILocator $locator)

Погодите, почему вы это считаете что тут сервис локатор?

Сервис локатор — это некий статический класс, который не передаётся как параметр в конструктор, и именно поэтому получается сильная связанность, что и является проблемой.
Как пример — https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/

Есть различные мнения на этот счёт. Например, такое:


SL работает по принципу pull: конструктор "вытягивает" из контейнера свои зависимости.
DI работает по принципу push: контейнер передает в конструктор его зависимости.
Это все теория, в реальном проекте SL будет дергаться везде, в любом методе, на любой чих, без объявления зависимости в поле (мне же только один сервисный метод дернуть, зачем поле заводить).

— Можем же Саймон?
— Запросто.

Ну в общем случае, сервис локатор – это просто "an object that knows how to get hold of all of the services that an application might need". Но да, изначальная идея вроде как не подразумевает передачу его в конструктор. Так что тут, я бы сказал, смешались в кучу ServiceLocator и DI с нарушением ISP.

Кажется, SL имеет право на существование. Но применение его лучше ограничить. В моей практике (на iOS), архитектурный модуль разбивается на две части: Чистую и Грязную (термины Роберта Мартина). Чистая содержит всю логику и обладает свойствами чистой архитетуры (SOLID). Грязная часть занимается конфигурированием чистой (называется <Module>Configurator>) и передачей управления в другой модуль (называется <Module>Router). Вот в конфигуратор и роутер вполне уместно передавать SL. Конфигуратор применя стратегию Pull вытягивает все внешние зависимости и применяя стратегию Push собирает (конфигурирует) архитектурный модуль. Таким образом, вся информация о внешних зависимостях модуля локализуется в конфигураторе. А SL из роутера одного модуля передаётся в конфигуратор другого (который SL передаёт в свой роутер и т.д)

Примеры автора статьи очень похожи на DI самого локатора

public function __construct(ILocator $locator)

В таком виде непонятно чем SL отличается от DI.

Я бы использовал другой пример

public function __construct()
{
  Locator::get(DEP1)->method();
  Locator::get(DEP2)->method();
}

В таком примере проблемы с неявной зависимостью от локатора и отсутствием удобного списка зависимостей класса более очевидны

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории