#php #oop #design-patterns
#php #ооп #шаблоны проектирования
Вопрос:
Мне сказали, что синглтоны сложно тестировать.
- http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/
- http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/
Мне сказали, что статические методы / объекты тоже никуда не годятся.
Таким образом, в основном единственным решением, похоже, является внедрение зависимостей.
Но … Я действительно не могу привыкнуть к DI, возьмите этот пример:
В моем фреймворке у меня есть класс, который управляет SQL. Этот класс (и многие другие мои фреймворки) использует одноэлементный регистратор для регистрации сообщений (и многие другие помощники).
С DI мой код превратился бы в:
global $logger; //> consider i have been instanciated it at the start of my fw
$query = new PreparedQuery($logger);
$query->prepare() etc.
Теперь это не кажется таким уж плохим. Но рассмотрим страницу, которая требует много запросов, я считаю, что довольно избыточно каждый раз писать $logger
в конструкторе, особенно если учесть, что PreparedQuery требовалось много других зависимостей в конструкторе.
Единственное решение избежать одноэлементности, которое я нашел, — это использовать метод (или просто простую функцию) в основном приложении, в котором хранятся все ссылки на эти вспомогательные объекты (Service Locator / Container), но это не решает проблему сокрытия зависимостей
Итак, по вашему опыту, кроме DI, какой хороший шаблон использовать?
Решение:
Для всех интересующихся создатель PHPUnit объясняет, как решить проблему с одноэлементными объектами (и как решить проблему тестирования статических методов с PHP 5.3)
- (одноэлементный) http://sebastian-bergmann.de/archives/882-Testing-Code-That-Uses-Singletons.html
- (статический) http://sebastian-bergmann.de/archives/883-Stubbing-and-Mocking-Static-Methods.html
Довольно интересное чтение, если вы спросите меня.
Ответ №1:
Пожалуйста, не используйте global
.
Вам нужно передать $logger в конструкторах или вместо этого передать Service Container (также известный как Objects manager, Service Locator, Resources Manager).
Вариант из Symfony Framework http://symfony.com/doc/current/book/service_container.html
Вы можете создать свой собственный диспетчер объектов, и его методы не должны быть статическими.
Комментарии:
1. Так что, по сути, это единственное решение… Спасибо за ваш ответ, но я знал это (прочитайте последний абзац моего вопроса :))
2. одной функции будет недостаточно, и контейнер-сервис может быть сложным (некоторые методы всегда будут возвращать новые, некоторые вернут существующий экземпляр, некоторые вернут клоны, некоторые методы будут принимать аргументы …).
3. В проекте я использую диспетчер объектов, который действительно является простой функцией и работает отлично. Если вы хотите, я опубликую код
4. Я полагаю, нет причин спорить, просто хочу помочь. Надеюсь, что страница документации будет полезной.
5. это очень полезно, и я благодарю вас … но в моей фреймворке у меня нет контроллера, который включен во весь мой код .. поэтому я не могу использовать
$this->container->get('object')
, как это делает sf
Ответ №2:
Ну, в этом случае я бы вместо этого создал builder (или factory). Итак, ваша фабрика внедрила бы зависимость для вас. Таким образом, вы также можете избежать своих глобальных:
class PreparedQueryFactory {
protected $logger = null;
public function __construct($loggger) {
$this->logger = $logger;
}
public function create() {
return new PreparedQuery($this->logger);
}
}
Таким образом, вы делаете один раз:
$factory = new PreparedQueryFactory($logger);
Тогда в любое время, когда вам нужен новый запрос, просто вызовите:
$query = $factory->create();
Итак, это очень простой пример. Но вы могли бы добавить всевозможную сложную логику, если вам нужно. Но суть в том, что, избегая new
в своем коде, вы также избегаете управления зависимостями. Таким образом, вместо этого вы можете передавать фабрики по мере необходимости.
Преимущество заключается в том, что все это на 100% тестируемо, поскольку все внедряется везде (в отличие от использования глобальных).
Вы также можете использовать реестр (иначе известный как Service Container или контейнер DI), но убедитесь, что вы вводите реестр в.
Комментарии:
1. вы знаете, что при этом мне все еще нужно использовать global ..
global $factory
. Таким образом, для каждого объекта, который использует helper (singleton) Мне нужно будет объявить другой объект * Factory. (а если у вас в фреймворке сотни объектов, это нежизнеспособно)2. @yes: вам не нужен глобальный. Если вы абстрагируетесь от этой концепции, все будет внедрено. И вам не нужна фабрика для каждого класса в вашем фреймворке, вам нужна фабрика для каждого типа класса, экземпляр которого необходимо создавать на лету. Фактически, именно так работает контейнер-сервис, он просто абстрагирует создание фабрики от некоторых механизмов настройки (XML или Yaml или аннотации и т.д.).
3. @yes123 Если вы прочтете еще несколько статей Миско Хевери, вы увидите, что он также использует фабрики для создания объектов такого рода, хотя DI предпочтительнее там, где это применимо.
4. @koen: Я немного поискал, но ничего не нашел, не могли бы вы опубликовать ссылку? @irc: Не думаю, что я понял ваше последнее замечание. Когда вы говорите «вам нужна фабрика для каждого типа класса, экземпляр которого необходимо создавать на лету», это нормально, но если у вас много подобных классов, вам придется реализовать все остальные фабрики для этого
5. @yes123 misko.hevery.com/2009/03/30/collaborator-vs-the-factory или misko.hevery.com/2008/07/08/how-to-think-about-the-new-operator/…
Ответ №3:
Ведение журнала обычно является тем примером, когда статические синглтоны в порядке. Вам в любом случае не нужно издеваться над вашим протоколированием, не так ли?
Комментарии:
1. @yes123 это довольно широкое утверждение, с которым я не уверен, что согласен, даже если один парень написал об этом… но мне придется согласиться с @OZ_ answer
2. @yes123: Они совсем не плохи, но ими очень-очень часто злоупотребляют, что приводит к низкой репутации. Однако в некоторых случаях они полезны (и не плохи ;))
3. Это правда. Миско Хевери проводит различие между синглетонами с большими S и синглетонами с маленькими s. Последнее предназначено только для того, чтобы убедиться, что существует один экземпляр, первое предназначено для того, чтобы вы могли вызвать один экземпляр там, где это необходимо. Первое плохо.
4. @dynamic Если вы читаете эту статью того же автора: googletesting.blogspot.com/2008/08 / … вы заметите, как он упоминает, что ведение журнала является одним из немногих случаев, когда одноэлементный шаблон не является проблемой.
Ответ №4:
Приведенные выше ответы дают вам несколько идей. Я представлю еще одно: реализовать архитектуру плагина. Регистратор становится плагином, который вы можете включать / отключать / изменять, когда захотите.
Упрощенный пример:
class Logger implements Observer {
public function notify($tellMeWhatHappened) {
// oh really? let me do xyz
}
}
class Query implements Observable {
private $observers = array();
public function addObserver(Observer $observer) {
$this->observers[] = $observer;
}
public function foo() {
// great code
foreach ($this->observers as $observer) { $observer->notify('did not work'); }
}
}
Это удаляет регистратор из конструктора. Это то, что я предпочитаю, если это не важно для функционирования объекта.
Ответ №5:
В моем понимании выступлений Миско Хевери о DI и new
операторе проблема в том, что вы не зашли достаточно далеко в реализации DI.
Что он всегда говорит, так это то, что вы не должны смешивать бизнес-логику с построением объектов. Однако в двух строках вашего примера первая ( $query = new PreparedQuery($logger);
) создает объект, а затем вторая ( $query->prepare(/* ... */);
) — бизнес-логику.
Очевидно, что цель этого кода — подготовить запрос, и вместо того, чтобы беспокоиться о том, как создать PreparedQuery
, он должен просто запросить его в конструкторе класса. Или, если ему нужно иметь возможность выдавать множество подготовленных запросов, он должен запросить прототип (который он будет клонировать всякий раз, когда ему понадобится новый) или объект factory. Дело в том, что тот факт, что в PreparedQuery есть logger, не имеет значения, и о нем следует позаботиться где-то в другом месте.
Принцип «запрашивать то, что вам нужно» в конструкторе, в принципе, легко понять, хотя я все еще пытаюсь разобраться для себя, что это означает на практике в различных ситуациях, и как реализовать его вплоть до вершины («основной метод» или эквивалент). Тем не менее, я думаю, что этот принцип говорит об общей проблеме, с которой вы столкнулись. Этот new
оператор не должен находиться там, где он есть в первую очередь.