Является ли DI единственным решением для одноэлементных и / или статических объектов?

#php #oop #design-patterns

#php #ооп #шаблоны проектирования

Вопрос:

Мне сказали, что синглтоны сложно тестировать.

Мне сказали, что статические методы / объекты тоже никуда не годятся.

Таким образом, в основном единственным решением, похоже, является внедрение зависимостей.

Но … Я действительно не могу привыкнуть к 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)

Довольно интересное чтение, если вы спросите меня.

Ответ №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 оператор не должен находиться там, где он есть в первую очередь.