Должен ли я использовать Dependency Injection или статические фабрики?

81

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

  • Фабрики затрудняют тестирование и не позволяют легко менять реализации. Они также не делают зависимости очевидными (например, вы изучаете метод, не обращая внимания на тот факт, что он вызывает метод, который вызывает метод, который вызывает метод, использующий базу данных).
  • Внедрение зависимостей массово расширяет списки аргументов конструктора и размазывает некоторые аспекты по всему вашему коду. Типичная ситуация, когда конструкторы более половины классов выглядят так(....., LoggingProvider l, DbSessionProvider db, ExceptionFactory d, UserSession sess, Descriptions d)

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

Как мне решить такую ​​проблему ??

RokL
источник
2
Если фабрика может решить все ваши проблемы, возможно, вы могли бы просто внедрить фабрику в ваши объекты и получить из них LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession.
Джорджио
1
Слишком много «входов» в метод, независимо от того, были ли они переданы или введены, являются большей проблемой самой разработки методов. Куда бы вы ни пошли, возможно, вы захотите немного уменьшить размер ваших методов (что легче сделать, когда вы сделаете инъекцию)
Bill K
Решением здесь не должно быть уменьшение аргументов. Вместо этого создайте абстракции, которые создают объект более высокого уровня, который выполняет всю работу в этом объекте и приносит вам пользу.
Алекс

Ответы:

74

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

Например, вы можете ввести новый тип, SessionEnvironmentинкапсулирующий a DBSessionProvider, the UserSessionи загруженный Descriptions. Однако, чтобы знать, какие абстракции имеют смысл больше всего, нужно знать детали вашей программы.

Подобный вопрос уже задавался здесь на SO .

Док Браун
источник
9
+1: я думаю, что группировка аргументов конструктора в классы - очень хорошая идея. Это также заставляет вас организовать эти аргументы в более значимые структуры.
Джорджио
5
Но если в результате получаются не значимые структуры, то вы просто скрываете сложность, которая нарушает SRP. В этом случае следует выполнить рефакторинг класса.
Данидакар
1
@ Джорджио Я не согласен с общим утверждением, что «группировка аргументов конструктора в классы - очень хорошая идея». Если вы квалифицируете это как «в этом сценарии», то это другое.
тымтам
19

Внедрение зависимостей массово расширяет списки аргументов конструктора и размазывает некоторые аспекты по всему вашему коду.

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

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

Другой подход заключается в создании фабрики исключений, которая передается объектам через их конструкторы. Вместо того , чтобы бросать новое исключение, класс может бросить на метод завода (например throw PrettyExceptionFactory.createException(data).

Имейте в виду, что ваши объекты, кроме ваших заводских объектов, никогда не должны использовать newоператор. Исключения - это, как правило, один особый случай, но в вашем случае они могут быть исключением!

Джонатан Рич
источник
1
Я где-то читал, что когда ваш список параметров становится слишком длинным, это происходит не потому, что вы используете внедрение зависимостей, а потому, что вам нужно больше внедрения зависимостей.
Джорджио
Это может быть одной из причин - обычно, в зависимости от языка, ваш конструктор должен иметь не более 6-8 аргументов, и не более 3-4 из них должны быть самими объектами, если только не конкретный шаблон (например, Builderшаблон) диктует это. Если вы передаете параметры в конструктор, потому что ваш объект создает экземпляры других объектов, это очевидный случай для IoC.
Джонатан Рич
12

Вы уже достаточно хорошо перечислили недостатки статического шаблона фабрики, но я не совсем согласен с недостатками шаблона внедрения зависимостей:

Это внедрение зависимостей требует от вас написания кода для каждой зависимости, это не ошибка, а особенность: она заставляет вас задуматься о том, действительно ли вам нужны эти зависимости, тем самым способствуя слабой связи. В вашем примере:

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

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

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

Таким образом, вы можете удалить 3 ненужных зависимости (UserSession, ExceptionFactory и, вероятно, описания), что сделает ваш код более простым и универсальным.

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

меритон - забастовка
источник
Этот. Я не вижу необходимости нажимать на БД, чтобы вызвать исключение.
Caleth
1

Используйте внедрение зависимостей. Использование статических фабрик - это занятие Service Locatorантипаттерна. Смотрите оригинальную работу Мартина Фаулера здесь - http://martinfowler.com/articles/injection.html

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

Сэм
источник
5
Сервисный локатор не является антипаттерном - сам Фаулер ссылается на него в размещенном вами URL. Хотя паттерном сервисного локатора можно злоупотреблять (так же, как злоупотребляют синглетонами - абстрагировать глобальное состояние), это очень полезный паттерн.
Джонатан Рич
1
Интересно знать. Я всегда слышал, что это называется анти паттерном.
Сэм
4
Это только антипаттерн, если локатор службы используется для хранения глобального состояния. Локатор службы должен быть объектом без состояния после создания экземпляра и предпочтительно неизменным.
Джонатан Рич
XML не является безопасным типом. Я бы посчитал это планом z после того, как любой другой подход потерпел неудачу
Ричард Тингл
1

Я бы тоже пошел с инъекцией зависимостей. Помните, что DI выполняется не только через конструкторы, но и через установщики свойств. Например, регистратор может быть введен как свойство.

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

Еще одним шагом, который вы можете сделать, является Aspect-Oriented Programmnig, которая реализована во многих основных средах. Это может позволить вам перехватить (или «посоветовать» использовать терминологию AspectJ) конструктор класса и внедрить соответствующие свойства, возможно, с помощью специального атрибута.

Tallmaris
источник
4
Я избегаю DI через сеттеры, потому что он вводит окно времени, в течение которого объект не находится в полностью инициализированном состоянии (между вызовом конструктора и сеттера). Или другими словами, он вводит порядок вызова метода (должен вызывать X перед Y), которого я избегаю, если это вообще возможно.
RokL
1
DI через установщики свойств идеально подходит для необязательных зависимостей. Ведение журнала является хорошим примером. Если вам нужна регистрация, то установите свойство Logger, если нет, то не устанавливайте его.
Престон
1

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

Я не совсем согласен. По крайней мере, не в целом.

Простая фабрика:

public IFoo GetIFoo()
{
    return new Foo();
}

Простая инъекция:

myDependencyInjector.Bind<IFoo>().To<Foo>();

Оба фрагмента служат одной и той же цели, они устанавливают связь между IFooи Foo. Все остальное - просто синтаксис.

Переход Fooк ADifferentFooтакому же примеру кода требует столько же усилий.
Я слышал, что люди утверждают, что внедрение зависимости позволяет использовать разные привязки, но один и тот же аргумент может быть сделан о создании разных фабрик. Выбор правильной привязки так же сложен, как и выбор правильной фабрики.

Фабричные методы позволяют, например, использовать Fooв некоторых местах и ADifferentFooв других местах. Кто-то может назвать это хорошим (полезно, если оно вам нужно), кто-то может назвать это плохим (вы могли бы сделать недоделанную работу по замене всего).
Однако не так сложно избежать этой неоднозначности, если придерживаться одного метода, который возвращает, IFooчтобы у вас всегда был один источник. Если вы не хотите стрелять себе в ногу, не держите заряженный пистолет или не направляйте его на ногу.


Внедрение зависимостей массово расширяет списки аргументов конструктора и размазывает некоторые аспекты по всему вашему коду.

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

public MyService()
{
    _myFoo = DependencyFramework.Get<IFoo>();
}

Я слышал аргументы "за" (без раздувания конструктора), я слышал аргументы "против" (использование конструктора позволяет больше автоматизации DI).

Лично, хотя я уступил нашему старшему, который хочет использовать аргументы конструктора, я заметил проблему с выпадающим списком в VS (вверху справа, чтобы просмотреть методы текущего класса), когда имена методов исчезают, когда один из них сигнатур метода длиннее, чем мой экран (=> раздутый конструктор).

На техническом уровне мне все равно. Любой вариант требует примерно столько же усилий, чтобы набрать. А поскольку вы используете DI, вы все равно обычно не будете вызывать конструктор вручную. Но ошибка пользовательского интерфейса Visual Studio заставляет меня предпочитать не раздувать аргумент конструктора.


Как примечание, введение зависимости и фабрики не являются взаимоисключающими . У меня были случаи, когда вместо вставки зависимости я вставлял фабрику, которая генерирует зависимости (к счастью, NInject позволяет вам использовать, Func<IFoo>поэтому вам не нужно создавать фактический класс фабрики).

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

Flater
источник
ОП спрашивает о статических фабриках. stackoverflow.com/questions/929021/…
Basilevs
@Basilevs «Вопрос в том, как мне предоставить эти компоненты другим компонентам».
Энтони Ратледж,
1
@Basilevs Все, что я сказал, кроме примечания, относится и к статическим фабрикам. Я не уверен, что вы пытаетесь конкретно указать. Какая ссылка в ссылке?
Флатер
Может ли один такой случай использования внедрения фабрики быть таким, когда кто-то разработал абстрактный класс HTTP-запроса и разработал стратегию пяти другим полиморфным дочерним классам, один для GET, POST, PUT, PATCH и DELETE? Вы не можете знать, какой метод HTTP будет использоваться каждый раз при попытке интегрировать указанную иерархию классов с маршрутизатором типа MVC (который может частично полагаться на класс HTTP-запроса. Интерфейс HTTP-сообщения PSR-7 ужасен.
Энтони Ратледж,
@AnthonyRutledge: фабрики DI могут означать две вещи (1) Класс, который предоставляет несколько методов. Я думаю, это то, о чем ты говоришь. Тем не менее, это не совсем относится к фабрикам. Будь то класс бизнес-логики с несколькими открытыми методами или фабрика с несколькими открытыми методами, вопрос семантики и не имеет технической разницы. (2) Особый случай использования DI для фабрик заключается в том, что не фабричная зависимость создается один раз (во время внедрения), тогда как фабричная версия может использоваться для создания реальной зависимости на более поздней стадии (и, возможно, несколько раз).
Флейтер
0

В этом фиктивном примере фабричный класс используется во время выполнения, чтобы определить, какой тип объекта входящего HTTP-запроса создавать, на основе метода HTTP-запроса. Сама фабрика внедряется с экземпляром контейнера внедрения зависимости. Это позволяет фабрике определять время выполнения и позволяет контейнеру внедрения зависимостей обрабатывать зависимости. Каждый входящий объект HTTP-запроса имеет как минимум четыре зависимости (суперглобальные и другие объекты).

<?php
namespace TFWD\Factories;

/**
 * A class responsible for instantiating
 * InboundHttpRequest objects (PHP 7.x)
 * 
 * @author Anthony E. Rutledge
 * @version 2.0
 */
class InboundHttpRequestFactory 
{
    private const GET = 'GET';
    private const POST = 'POST';
    private const PUT = 'PUT';
    private const PATCH = 'PATCH';
    private const DELETE = 'DELETE';

    private static $di;
    private static $method;

    // public function __construct(Injector $di, Validator $httpRequestValidator)
    // {
    //    $this->di = $di;
    //    $this->method = $httpRequestValidator->getMethod();
    // }

    public static function setInjector(Injector $di)
    {
        self::$di = $di;
    }    

    public static setMethod(string $method)
    {
        self::$method = $method;
    }

    public static function getRequest()
    {
        if (self::$method == self::GET) {
            return self::$di->get('InboundGetHttpRequest');
        } elseif ((self::$method == self::POST) && empty($_FILES)) {
            return self::$di->get('InboundPostHttpRequest');
        } elseif (self::$method == self::POST) {
            return self::$di->get('InboundFilePostHttpRequest');
        } elseif (self::$method == self::PUT) {
            return self::$di->get('InboundPutHttpRequest');
        } elseif (self::$method == self::PATCH) {
            return self::$di->get('InboundPatchHttpRequest');
        } elseif (self::$method == self::DELETE) {
            return self::$di->get('InboundDeleteHttpRequest');
        } else {
            throw new \RuntimeException("Unexpected HTTP request. Invalid request.");
        }
    }
}

Клиентский код для установки типа MVC в централизованном виде index.phpможет выглядеть следующим образом (проверки пропущены).

InboundHttpRequestFactory::setInjector($di);
InboundHttpRequestFactory::setMethod($httpRequestValidator->getMethod());
$di->set('InboundHttpRequest', InboundHttpRequestFactory::getRequest());
$router = $di->get('Router');  // The Router class depends on InboundHttpRequest objects.
$router->dispatch(); 

Кроме того, вы можете удалить статическую природу (и ключевое слово) фабрики и позволить инжектору зависимостей управлять всем этим (следовательно, закомментированным конструктором). Однако вам придется изменить некоторые (не константы) ссылок selfна члены класса ( ) на члены экземпляра ( $this).

Энтони Ратледж
источник
Downvotes без комментариев не полезны.
Энтони Ратледж,