Если синглтоны - это плохо, то почему сервисный контейнер хорош?

91

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

Но во фреймворке может быть много объектов, которые нужно создать только один раз и вызывать из любого места (регистратор, база данных и т. Д.).

Чтобы решить эту проблему, мне посоветовали использовать так называемый «диспетчер объектов» (или контейнер служб, например, symfony), который хранит внутри все ссылки на службы (регистратор и т.

Но почему поставщик услуг не так плох, как чистый синглтон?

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

PS. Я знаю, что чтобы не скрывать зависимости, я должен использовать DI (как заявил Миско)

Добавить

Я бы добавил: в наши дни синглтоны не такое уж зло, - объяснил создатель PHPUnit:

DI + Singleton решает проблему:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

это довольно умно, даже если это не решает всех проблем.

Есть ли какое-либо хорошее приемлемое решение для доступа к этим вспомогательным объектам, кроме DI и Service Container ?

динамичный
источник
2
@yes Ваша редакция делает ложные предположения. Себастьян никоим образом не предполагает, что фрагмент кода делает использование синглонов меньшей проблемой. Это всего лишь один из способов сделать код, который иначе было бы невозможно протестировать более тестируемым. Но это все еще проблемный код. Фактически, он прямо отмечает: «Просто потому, что ты можешь, это не значит, что ты должен». Правильным решением будет вообще не использовать синглтоны.
Гордон
3
@yes следуют принципу SOLID.
Гордон
19
Я оспариваю утверждение, что синглтоны - это плохо. Их можно использовать не по назначению, но и любой инструмент тоже . С помощью скальпеля можно спасти жизнь или положить ей конец. Бензопила может расчистить лес, чтобы предотвратить лесные пожары, или может отрубить значительную часть вашей руки, если вы не знаете, что делаете. Научитесь использовать свои инструменты с умом и не относитесь к советам как к Евангелию - так кроется бездумный ум.
paxdiablo
4
@paxdiablo , но они являются плохими. Синглтоны нарушают SRP, OCP и DIP. Они привносят в ваше приложение глобальное состояние и тесную связь, заставляя ваш API лгать о своих зависимостях. Все это отрицательно скажется на ремонтопригодности, читаемости и тестируемости вашего кода. Могут быть редкие случаи, когда эти недостатки перевешивают небольшие преимущества, но я бы сказал, что в 99% вам не нужен синглтон. Особенно в PHP, где синглтоны в любом случае уникальны только для запроса, и собрать графы коллабораторов из Builder очень просто.
Гордон
5
Нет, не думаю. Инструмент - это средство для выполнения функции, обычно каким-то образом упрощая ее, хотя некоторые (emacs?) Имеют редкую особенность - усложнять ее :-) В этом синглтон ничем не отличается от сбалансированного дерева или компилятора. . Если вам нужно обеспечить только одну копию объекта, это сделает синглтон. Можно спорить о том, хорошо ли он это делает, но я не думаю, что вы можете утверждать, что он этого не делает вообще. И могут быть способы получше, например, бензопила будет быстрее ручной пилы или гвоздь против молотка. Это не делает ручную пилу / молоток менее инструментом.
paxdiablo

Ответы:

76

Service Locator - это, так сказать, меньшее из двух зол. «Меньшее», сводящееся к этим четырем отличиям ( по крайней мере, я не могу сейчас вспомнить другие ):

Принцип единой ответственности

Сервисный контейнер не нарушает принцип единой ответственности, как это делает синглтон. Синглтоны смешивают создание объектов и бизнес-логику, в то время как Service Container строго отвечает за управление жизненными циклами объектов вашего приложения. В этом отношении Service Container лучше.

Связь

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

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

Скрытые зависимости

Однако проблема сокрытия зависимостей существует очень часто. Когда вы просто вводите локатор в свои классы потребления, вы не узнаете никаких зависимостей. Но в отличие от синглтона SL обычно создает экземпляры всех необходимых зависимостей за кулисами. Таким образом, когда вы загружаете Сервис, вы не в конечном итоге, как Миско Хевери в примере с CreditCard , например, вам не нужно создавать экземпляры всех зависимостей вручную.

Получение зависимостей из экземпляра также нарушает Закон Деметры , который гласит, что вы не должны копаться в соавторах. Экземпляр должен разговаривать только со своими непосредственными сотрудниками. Это проблема как синглтона, так и с ServiceLocator.

Глобальное состояние

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

Также см. Фаулер « Локатор сервисов против внедрения зависимостей» для более подробного обсуждения.


Заметка о вашем обновлении и связанная статья Себастьяна Бергманна о тестировании кода, использующего синглтоны : Себастьян никоим образом не предполагает, что предлагаемый обходной путь делает использование синглтонов менее проблематичным. Это всего лишь один из способов сделать код, который иначе было бы невозможно протестировать более тестируемым. Но это все еще проблемный код. Фактически, он прямо отмечает: «Просто потому, что ты можешь, это не значит, что ты должен».

Гордон
источник
1
Особенно здесь должна быть обеспечена возможность проверки. Вы не можете имитировать вызовы статических методов. Однако вы можете имитировать службы, которые были введены через конструктор или сеттер.
Дэвид
44

Шаблон локатора сервисов - это анти-шаблон. Это не решает проблему выявления зависимостей (вы не можете сказать, глядя на определение класса, каковы его зависимости, потому что они не внедряются, вместо этого они выдергиваются из локатора службы).

Итак, ваш вопрос: чем хороши локаторы сервисов? Мой ответ: это не так.

Избегайте, избегайте, избегайте.

Джейсон
источник
6
Похоже, вы ничего не знаете об интерфейсах. Класс просто описывает необходимый интерфейс в подписи конструктора - и это все, что ему нужно знать. Пройденный Service Locator должен реализовывать интерфейс, вот и все. И если IDE проверит реализацию интерфейса, контролировать любые изменения будет довольно просто.
OZ_
4
@ yes123: Люди, которые так говорят, ошибаются, и они ошибаются, потому что SL - это антипаттерн. Ваш вопрос: "Почему SL хороши?" Мой ответ: это не так.
Джейсон
5
Я не буду спорить, является ли SL анит-шаблоном или нет, но я скажу, что это намного меньшее зло по сравнению с синглтоном и глобальными объектами. Вы не можете протестировать класс, который зависит от синглтона, но вы определенно можете протестировать класс, который зависит от SL (вы можете испортить дизайн SL до такой степени, что он не работает) ... Так что это стоит отмечая ...
ircmaxell
3
@Jason вам нужно передать объект, который реализует интерфейс - и это только то, что вам нужно знать. Вы ограничиваетесь только определением конструктора классов и хотите записать в конструктор все классы (не интерфейсы) - это глупая идея. Все, что вам нужно, это интерфейс. Вы можете успешно протестировать этот класс с помощью моков, вы можете легко изменить поведение, не меняя код, нет никаких дополнительных зависимостей и связывания - это все (в целом), что мы хотим иметь в Dependency Injection.
OZ_
2
Конечно, я просто объединю базу данных, регистратор, диск, шаблон, кэш и пользователя в один объект «Вход», и конечно будет легче определить, на какие зависимости опирается мой объект, чем если бы я использовал контейнер.
Mahn
4

Контейнер службы скрывает зависимости, как это делает шаблон Singleton. Вы можете предложить вместо этого использовать контейнеры для внедрения зависимостей, поскольку у него есть все преимущества контейнера службы, но нет (насколько мне известно) недостатков, которые есть у контейнера службы.

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

Это хороший вопрос для SO, объясняющий разницу между ними: в чем разница между шаблонами внедрения зависимостей и локатора служб?

Риккристи
источник
«DIC внедряет для вас соответствующие зависимости» Разве это не происходит и с синглтоном?
динамический
5
@ yes123 - Если вы используете синглтон, вы не будете его вводить, в большинстве случаев вы просто получите к нему глобальный доступ (в этом суть синглтона). Я полагаю, если вы скажете, что если вы введете синглтон, он не скроет зависимости, но это как бы побеждает исходную цель шаблона синглтона - вы спросите себя, если мне не нужен глобальный доступ к этому классу, почему мне нужно сделать его синглтоном?
rickchristie
2

Потому что вы можете легко заменить объекты в Service Container с помощью
1) наследования (класс диспетчера объектов может быть унаследован, а методы могут быть переопределены)
2) изменения конфигурации (в случае с Symfony)

И синглтоны плохи не только из-за высокой связи, но и потому, что они _ одиночные _ton. Это неправильная архитектура почти для всех видов объектов.

С «чистым» DI (в конструкторах) вы заплатите очень большую цену - все объекты должны быть созданы до того, как будут переданы в конструктор. Это будет означать больше используемой памяти и меньшую производительность. Кроме того, не всегда объект можно просто создать и передать в конструктор - можно создать цепочку зависимостей ... Мой английский недостаточно хорош, чтобы обсуждать это полностью, читайте об этом в документации Symfony.

OZ_
источник
0

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

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

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

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

Моя идея синглтона - вы не можете добавить один и тот же объект внутри родителя дважды. Например

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

оставит вас с одним экземпляром и обеими переменными, указывающими на него.

Наконец, если вы хотите использовать объектно-ориентированную разработку, работайте с объектами, а не с классами.

романиньш
источник
1
так что ваш метод - передать $api var вокруг вашей структуры? Я не совсем понял, что вы имеете в виду. Также, если вызов add('Logger')возвращает тот же экземпляр, в основном у вас есть сервисный помощник
динамический
Да, это верно. Я называю их «системным контроллером», и они предназначены для улучшения функциональности API. Подобным образом добавление «Auditable» контроллера к модели дважды будет работать точно так же - создать только один экземпляр и только один набор полей аудита.
romaninsh