Почему Service Locator это зло и что использовать вместо?

Прочитал много статей про то, что Service Locator - это антипаттерн. Но он используется в PHP повсеместно, в частности, и в Yii2, и в Laravel он лежит в основе всего.

Помогите с 2 вопросами.
1. Правильно ли я понял, в чем проблема Service Locator?

Как я понял, проблема в том, что когда мы получаем объект как-то так:
$db = $serviceLocator->get('db');
То фактически там может быть что угодно, так как на уровне языка мы никак не защищены при таком вызове, там может быть любой объект, реализующий любой интерфейс и т. д. И это плохо, так как мы, например, у себя в каком-нибудь пакете для какого-нибудь фреймворка вызываем сервис 'mailer', ожидая получить там экземпляр одного мейлера, а наш пакет поставили на другое приложение, где используется другой мейлер и все перестало работать. Вроде как мы должны как-то явно проверять на входе, что мы сейчас получим объект такого-то класса или реализующий такой-то интерфейс.

2. Что тогда нужно использовать вместо Service Locator? При этом чтобы по части конфигурации было так же все централизовано и складно, как это обычно во фреймворках с Service Locator. Например, у нас есть сервисы db, mailer, logger и многие другие. И конфиги их лежат в config/app.php централизованно. Как в одну строчку получить сервис db и использовать его, если не через Service Locator?
  • Вопрос задан
  • 2052 просмотра
Решения вопроса 1
FanatPHP
@FanatPHP
Чебуратор тега РНР
Все эти страшные слова - они на самом деле всегда про одно и то же - про связность. Когда ты хардкодишь внутри класса вызов какого-то конкретного сервиса - ты намертво к нему привязываешься. И чтобы поменять сервис на другой, ты будешь вынужден поменять код класса. Окей, поменял. И тут же в другом месте, где этот же класс использовался, что-то сломалось! И что теперь? Делать два класса, которые различаются одной строчкой? Нет конечно. А как тогда использовать один и тот же класс для обработки разных входящих данных (или одних и тех же данных, но разными способами)? Сделать его поведение изменяемым. То есть сделать изменяемыми те инструменты, которыми он пользуется - т.е. его зависимости.

Поэтому все зависимости обычно передаются через конструктор (и поэтому и называются инъекция зависимостей.)

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

Но тут надо понимать, что всё это работает только при правильном применении ООП. А точнее просто при применении ООП. Потому что 98% "ООП" кода, который пишется на РНР - это голимая процедурщина, даже если она обёрнута в классы и методы. Если у тебя метод класса представляет из себя стену кода, которую ты тупо перенёс из файла, инклюдившегося в любимое похапешное спагетти - то это не ООП. Это та же процедурщина, вид сбоку. И смысл использования dependency injection ты с ним не почуствуешь. Будешь конечно применять, но в качестве карго культа - потому что тебе это на тостере написали.
А вот когда твой код начнет становиться действительно объектным - тогда стразу станет понятнее.


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

Соотвтственно, ответ на вопрос "что использовать?" очень простой:
- при ручном создании экземпляра объекта, все зависимости передавать в него через конструктор, а не получать "из воздуха" в коде.
- при автоматическом создании экземпляра объекта, использовать dependency injection container

В этим смысле очень полезно освоить Симфони - строгий фрейворк, в котором нет сервис локатора и в котором запрещено пользоваться контейнером напрямую.
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
dmitriylanets
@dmitriylanets
веб-разработчик
Основные минусы:
- В результате, между классом и его клиентом существует формальный или неформальный «контракт», который выражается в виде предусловий (требований к клиенту) и постусловий (гарантий выполнения работы). Однако если класс принимает экземпляр сервис локатора, или, хуже того, использует глобальный локатор, то этот контракт, а точнее требования, которые нужно выполнить клиенту класса, становятся неясными
- Когда наш класс использует сервис локатор, то стабильность класса становится неопределенной. Наш класс, теоретически, может использовать что угодно, поэтому изменение любого класса (или интерфейса) в нашем проекте может затронуть произвольное количество классов и модулей
- Самое страшное в Сервис Локаторе то, что он дает видимость хорошего дизайна. У нас никто не знает о конкретных классах, все завязаны на интерфейсы, все «нормально» тестируется и «расширяется». Но когда вы попробуете использовать ваш код в другом контексте или когда кто-то попробует использовать его повторно, вы с ужасом поймете, что у вас есть дикая «логическая» связанность, о которой вы и не подозревали
- Для меня ясность и понятность интерфейса класса важнее количества параметров конструктора. Я не исключаю, что бывают случаи, когда сервис локатор является наименьшим злом, но я бы в любом случае постарался свести его использования к минимуму.
Ответ написан
Комментировать
kraso4niy
@kraso4niy
fullstack
1. Вы всё верно поняли. Сложность в том что внутри может оказаться не та структура которую вы ожидайте (ожидалась)

Но это не значит что это плохо. Эту проблему можно решить валидацией. В вашем примере достаточно проверять на instanceOf. Тогда код станет правильный. А в symfony сделали это ещё лучше и добавили строгость к типам для сервис локатора через подписку на сервисы.

Что бы понять когда применять сервис локатор, а когда инъекцию пройдите по ссылке https://symfony.com/doc/current/service_container/... и почитайте.

Там есть пояснение:
Sometimes, a service needs access to several other services without being sure that all of them will actually be used. In those cases, you may want the instantiation of the services to be lazy. However, that’s not possible using the explicit dependency injection since services are not all meant to be lazy


Можно пояснить на примере: допустим у вас есть класс с методом, и внутри метода только в некоторых случаях вам понадобится сделать запись в БД (допустим обращение к сервису в виде get->('db')).

Например:
if($userData = $request->get('user_data')) {
    $db = $serviceLocator->get('db')->insert($userData);
} else {
   die("user data empty");
}


Если делать через инъекцию, то $db всегда будет инициализирована, например если проводить инъекцию через конструктор или метод, то объект вашего класса всегда будет при любых условиях запускать код инициализации $db и тратить на это ресурсы. Пример через инъекцию (тут тип инъекции через аргумент метода, аналогично можно сделать через конструктор или сеттер)

class MyClass {
   // $db будет инициализрован, но используется только в 1 случае для if
  // если $db тяжёлый сервис и инициализируется долго это проблема!
   function httpResponse(Request $request, Database $db) {
      
       if($userData = $request->get('user_data')) {
          $db->insert($userData);
       } else {
          die("user data empty");
       }
   }
}


А в случае сервис локатора $db будет инициализирован только в условии когда придёт user_data. Таким образом если у вас контроллер использует 15 сервисов (но это bad code!), а одновременно нужен только 1 из них, лучше использовать сервис локатор.

class MyClass {
   // В таком случае рекомендуется воспользоваться сервис локатором! 
   function httpResponse(Request $request, Database $db, Servive1 $s1, Service2 $s2) {
      
       if($userData = $request->get('user_data')) {
          $db->insert($userData);
          if ($db->lastId()) {
               $s1->makeSuccess();
               if($s1->isSuccess()) {
                   $s2->commit();
               }
          }
       } else {
          $s2->rollback();
          die("user data empty");
       }
   }
}


Отсюда следует почему сервис локатор считается антипаттерном. Если ваш класс использует 15 сервисов, и из них все используются выборочно, то вам вероятно следует изменить архитектурный подход, провести рефакторинг, провести декомпозицию класса.

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

P.S:
Использовать service locator можно и в этом нет ничего страшного, особенно когда есть понимание что это такое и для чего он нужен.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы