Внедрение зависимостей и шаблон проектирования синглтонов

89

Как определить, когда использовать внедрение зависимостей или одноэлементный шаблон. Я читал на многих веб-сайтах, где говорится: «Используйте инъекцию зависимостей по шаблону singleton». Но я не уверен, что полностью согласен с ними. Для моих малых или средних проектов я определенно вижу простое использование одноэлементного шаблона.

Например Регистратор. Я мог бы использовать Logger.GetInstance().Log(...) Но, вместо этого, зачем мне вставлять каждый создаваемый мной класс с экземпляром регистратора ?.

Сисадмин
источник

Ответы:

66

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

Посмотрите эту презентацию об объектно-ориентированном дизайне для тестирования, и вы поймете, почему синглтоны - это плохо.

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

Имейте в виду, что объект может быть де-факто одноэлементным, но все же получаться через внедрение зависимостей, а не через Singleton.getInstance().

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

Божо
источник
89

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

В одноэлементном шаблоне непропорционально большое внимание уделяется простоте доступа к объектам. Он полностью избегает контекста, требуя, чтобы каждый потребитель использовал объект в области видимости домена приложения, не оставляя никаких вариантов для различных реализаций. Он встраивает знания об инфраструктуре в ваши классы (призыв к GetInstance()), добавляя ровно нулевую выразительную силу. Это фактически снижает вашу выразительную силу, потому что вы не можете изменить реализацию, используемую одним классом, не изменив ее для всех . Вы просто не можете добавить разовые функции.

Кроме того, когда класс Fooзависит от Logger.GetInstance(), Fooон эффективно скрывает свои зависимости от потребителей. Это означает, что вы не сможете полностью понять Fooили использовать его с уверенностью, если не прочитаете его источник и не обнаружите тот факт, от которого он зависит Logger. Если у вас нет исходного кода, это ограничивает ваше понимание и эффективное использование кода, от которого вы зависите.

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

Брайан Уоттс
источник
9
@BryanWatts при всем том, что синглтоны все еще намного быстрее и менее подвержены ошибкам в нормальном приложении среднего масштаба. Обычно у меня нет более одной возможной реализации для (настраиваемого) Регистратора, поэтому зачем мне **: 1. Создавать для этого интерфейс и изменять его каждый раз, когда мне нужно добавлять / изменять публичные члены 2. Поддерживать конфигурация DI 3. Скрыть факт наличия единственного объекта этого типа во всей системе 4. Привязать себя к строгой функциональности, вызванной преждевременным разделением проблем.
Ури Абрамсон
@UriAbramson: Похоже, вы уже приняли решение о компромиссах, которые предпочитаете, поэтому я не буду пытаться вас убеждать.
Брайан Уоттс
@BryanWatts Это не тот случай, возможно, мой комментарий был слишком резким, но на самом деле мне очень интересно услышать, что вы можете сказать по поводу того, что я поднял ...
Ури Абрамсон
2
@UriAbramson: Достаточно честно. Согласны ли вы хотя бы с тем, что замена реализаций на изоляцию во время тестирования важна?
Брайан Уоттс
6
Использование синглтонов и внедрения зависимостей не исключают друг друга. Синглтон может реализовывать интерфейс, поэтому может использоваться для удовлетворения зависимости от другого класса. Тот факт, что это синглтон, не заставляет каждого потребителя получать ссылку через его метод / свойство GetInstance.
Оливер
18

Другие очень хорошо объяснили проблему синглтонов в целом. Я просто хотел бы добавить примечание о конкретном случае Logger. Я согласен с вами, что обычно не проблема получить доступ к Регистратору (или корневому регистратору, если быть точным) как синглтону, через статический getInstance()или getRootLogger()метод. (если вы не хотите видеть, что регистрируется тестируемым вами классом - но по своему опыту я вряд ли могу вспомнить такие случаи, когда это было необходимо. Опять же, для других это может быть более насущной проблемой).

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

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

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

Петер Торок
источник
1
Даже тогда синглтон может быть проблемой. Если вы понимаете, что вашему регистратору в некоторых случаях нужен какой-то дополнительный параметр, вы можете либо создать новый метод (и позволить регистратору стать более уродливым), либо сломать всех потребителей регистратора, даже если им все равно.
kyoryu
4
@kyoryu, я говорю о "обычном" случае, который ИМХО подразумевает использование (де-факто) стандартной структуры ведения журнала. (Которые обычно настраиваются через файлы свойств / XML, кстати.) Конечно, есть исключения - как всегда. Если я знаю, что мое приложение является исключительным в этом отношении, я действительно не буду использовать синглтон. Но излишне изобретательные высказывания «когда-нибудь это может быть полезно» - почти всегда напрасная трата усилий.
Péter Török
Если вы уже используете DI, это не требует особых усилий. (Кстати, я с вами не возражаю, я за). Многим регистраторам требуется некоторая информация о "категории" или что-то в этом роде, и добавление дополнительных параметров может вызвать боль. Скрытие его за интерфейсом может помочь сохранить в чистоте потребляющий код и упростить переход на другую платформу ведения журнала.
kyoryu
1
@kyoryu, извините за ошибочное предположение. Я видел отрицательный голос и комментарий, поэтому я соединил точки - как оказалось, неправильно :-( Я никогда не испытывал необходимости переключаться на другую структуру ведения журнала, но опять же, я понимаю, что это может быть законным участие в некоторых проектах.
Петер Торок,
5
Поправьте меня, если я ошибаюсь, но singleton будет для LogFactory, а не для регистратора. Кроме того, LogFactory, скорее всего, будет фасадом журналирования apache или Slf4j. Так что переключение реализаций журналирования в любом случае безболезненно. Разве реальная проблема с использованием DI для внедрения LogFactory не в том, что теперь вам нужно перейти в applicationContext, чтобы создать каждый экземпляр в своем приложении?
HDave
9

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

  • Трудно проверить. Имеется в виду, как мне убедиться, что регистратор работает правильно.
  • Трудно протестировать. Это означает, что если я тестирую код, который использует регистратор, но это не является целью моего теста, мне все равно нужно убедиться, что мой тестовый env поддерживает регистратор
  • Иногда вам нужен не синглтон, а больше гибкости

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

Скотт Вайнштейн
источник
На самом деле, нет. Речь идет об общем изменяемом состоянии и статических зависимостях, которые в конечном итоге усложняют задачу. Тестирование - это просто очевидный и часто самый болезненный пример.
kyoryu
2

Примерно единственный раз, когда вы должны когда-либо использовать Singleton вместо Dependency Injection, - это если Singleton представляет неизменяемое значение, такое как List.Empty или подобное (при условии неизменяемых списков).

Внутренняя проверка для синглтона должна быть такой: «Было бы хорошо, если бы это была глобальная переменная, а не синглтон?» В противном случае вы используете шаблон Singleton, чтобы скрыть глобальную переменную, и вам следует рассмотреть другой подход.

Kyoryu
источник
1

Только что прочитал статью о Monostate - это отличная альтернатива Singleton, но у нее есть некоторые странные свойства:

class Mono{
    public static $db;
    public function setDb($db){
       self::$db = $db;
    }

}

class Mapper extends Mono{
    //mapping procedure
    return $Entity;

    public function save($Entity);//requires database connection to be set
}

class Entity{
public function save(){
    $Mapper = new Mapper();
    $Mapper->save($this);//has same static reference to database class     
}

$Mapper = new Mapper();
$Mapper->setDb($db);

$User = $Mapper->find(1);
$User->save();

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

Sunwukung
источник