Чи слід використовувати залежно-впорскувальні або статичні заводи?


81

При розробці системи у мене часто виникає проблема використання купою модулів (ведення журналів, доступ до бази даних тощо), які використовуються іншими модулями. Питання в тому, як я можу запропонувати ці компоненти іншим компонентам. Дві відповіді здаються можливими введенням залежності або з використанням заводської схеми. Однак обидва здаються неправильними:

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

Ось типова ситуація, з якою у мене виникає проблема: у мене є класи виключень, які використовують описи помилок, завантажені з бази даних, використовуючи запит із параметром мови мови користувача, що знаходиться в об'єкті сеансу користувача. Отже, щоб створити новий виняток, мені потрібен опис, який вимагає сесії бази даних та сеансу користувача. Тож я приречений перетягувати всі ці об’єкти на всі мої методи лише на випадок, якщо мені може знадобитися кинути виняток.

Як вирішити таку проблему ??


2
Якщо фабрика може вирішити всі ваші проблеми, можливо, ви можете просто ввести завод у свої об’єкти і отримати LoggingProvider, DbSessionProvider, ExceptionFactory, UserSession від цього.
Джорджіо

1
Занадто багато "входів" до методу, будь то передача чи ін'єкція, є більшою проблемою самого проектування методів. Що б ви не ходили з вами, можливо, ви хочете трохи зменшити розмір своїх методів (що простіше зробити, коли ви зробите ін’єкцію на місці)
Білл К

Тут рішення не повинно бути зменшенням аргументів. Замість цього будуйте абстракції, які будують об'єкт вищого рівня, який виконує всю роботу в об'єкті і дає вам користь.
Олексій

Відповіді:


74

Використовуйте введення залежності, але щоразу, коли списки аргументів конструктора стають занадто великими, перефактуруйте його за допомогою служби Facade . Ідея полягає в тому, щоб згрупувати деякі аргументи конструктора разом, запровадивши нову абстракцію.

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

Подібне запитання вже було задано тут на SO .


9
+1: Я думаю, що групування аргументів конструктора по класах - дуже гарна ідея. Це також змушує вас організувати ці аргументи в більш значущі структури.
Джорджіо

5
Але якщо результат не є змістовними структурами, то ви просто приховуєте складність, яка порушує SRP. У цьому випадку слід проводити рефакторинг класів.
danidacar

1
@Giorgio Я не згоден із загальним твердженням, що "групування аргументів конструктора в класи - дуже гарна ідея". Якщо ви кваліфікуєте це за "за цим сценарієм", то це інакше.
tymtam

19

Ін'єкційна залежність масово набухає списки аргументів конструктора, і це змазує деякі аспекти у вашому коді.

З цього не здається, що ви зрозуміли, що DI належним чином - ідея полягає в тому, щоб перевернути шаблон обліку об'єктів всередині фабрики.

Ваша конкретна проблема здається більш загальною проблемою OOP. Чому об'єкти не можуть просто викидати нормальні винятки, що не читаються людиною, під час їх виконання, а потім мати щось перед остаточним спробу / ловити, що сприймає це виняток, і в цей момент використовує інформацію сеансу для викидання нового, кращого винятку ?

Іншим підходом було б мати фабрику винятків, яка передається об'єктам через їх конструктори. Замість того, щоб кидати новий виняток, клас може кинути метод заводу (напр throw PrettyExceptionFactory.createException(data).

Майте на увазі, що ваші об'єкти, крім заводських об'єктів, ніколи не повинні використовувати newоператора. Винятки, як правило, є окремим випадком, але у вашому випадку вони можуть бути винятком!


1
Я десь читав, що коли ваш список параметрів стає занадто довгим, це не тому, що ви використовуєте ін'єкцію залежності, а тому, що вам потрібно більше введення залежності.
Джорджіо

Це може бути однією з причин - як правило, залежно від мови, ваш конструктор повинен мати не більше 6-8 аргументів, і не більше 3-4 з них повинні бути самими об'єктами, якщо тільки конкретний шаблон (як, наприклад, Builderшаблон) диктує це. Якщо ви передаєте параметри своєму конструктору через те, що ваш об'єкт інстанціює інші об'єкти, це явний випадок для IoC.
Джонатан Річ

12

Ви вже перерахували недоліки статичної заводської структури, але я не зовсім згоден з недоліками схеми введення залежності:

Ця ін'єкційна залежність вимагає, щоб ви писали код для кожної залежності - це не помилка, а особливість: вона змушує задуматися про те, чи справді вам потрібні ці залежності, тим самим сприяючи втраті зв'язку. У вашому прикладі:

Ось типова ситуація, з якою у мене виникає проблема: у мене є класи виключень, які використовують описи помилок, завантажені з бази даних, використовуючи запит із параметром мови мови користувача, що знаходиться в об'єкті сеансу користувача. Отже, щоб створити новий виняток, мені потрібен опис, який вимагає сесії бази даних та сеансу користувача. Тож я приречений перетягувати всі ці об’єкти на всі мої методи лише на випадок, якщо мені може знадобитися кинути виняток.

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

Очевидно, що локалізація повідомлень залежить від того, хто переглядає ці повідомлення, тобто це відповідальність шару презентації. Тому я б кинув звичайні винятки з службового бізнесу, які мають ідентифікатор повідомлення, який потім можна шукати в обробці винятків рівня презентаційного рівня в будь-якому джерелі повідомлення, яке воно використовується.

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

Взагалі кажучи, я би використовував статичні заводи лише для речей, до яких потрібно повсюдний доступ, і які гарантовано будуть доступні в усіх середовищах, до яких ми могли б хотіти запустити код (наприклад, журнал реєстрації). Для всього іншого я б використовував звичайну стару ін'єкцію залежності.


Це. Я не бачу необхідності натискати на БД, щоб кинути виняток.
Калет

1

Використовуйте ін'єкційну залежність. Використання статичних заводів - це робота Service Locatorантипаттера. Дивіться семінарну роботу від Мартіна Фаулера тут - http://martinfowler.com/articles/injection.html

Якщо ваші конструкторські аргументи стають занадто великими, і ви не використовуєте контейнер DI будь-якими способами, пишіть власні фабрики для отримання екземплярів, дозволяючи його налаштувати, або XML, або прив’язуючи реалізацію до інтерфейсу.


5
Локатор послуг не є антипатерном - сам Фоулер посилається на нього у вказаній вами URL. У той час як шаблоном "Локатор послуг" можна зловживати (так само, як зловживають Singletons - щоб відстежувати глобальний стан), це дуже корисна модель.
Джонатан Річ

1
Цікаво знати. Я завжди чув, що це називається анти-зразком.
Сем

4
Це антипатерн, лише якщо локатор служби використовується для зберігання глобального стану. Локатор служби повинен бути об'єктом без громадянства після встановлення даних, бажано незмінним.
Джонатан Річ

XML не є безпечним. Я б вважав це планом z після того, як кожен інший підхід провалився
Річард Тінгл

1

Я б підходив також із введенням залежності. Пам’ятайте, що DI здійснюється не тільки через конструктори, але і через налаштування властивостей. Наприклад, реєстратор може бути введений як властивість.

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

На крок далі ви можете піти - це орієнтований на аспекти Programmnig, який реалізований у багатьох основних рамках. Це може дозволити вам перехопити (або «порадити» використовувати термінологію AspectJ) конструктор класу та ввести відповідні властивості, можливо, надавши спеціальний атрибут.


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>();
}

Я чув аргументи pro (відсутність конструкції конструктора), я чув аргументи con (використання конструктора дозволяє більше DI автоматизації).

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

Щодо технічного рівня, мені це не байдуже. Будь-який варіант вимагає стільки ж зусиль, щоб набрати. А оскільки ви використовуєте DI, ви зазвичай не будете викликати конструктор вручну. Але помилка інтерфейсу Visual Studio UI робить мені користь не роздуття аргументу конструктора.


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

Випадки використання для цього рідкісні, але вони існують.


ОП запитує про статичні заводи. stackoverflow.com/questions/929021 / ...
Basilevs

@Basilevs "Питання в тому, як я можу забезпечити надання цих компонентів іншим компонентам."
Ентоні Рутлідж

1
@Basilevs Все, що я говорив, окрім бічної записки, стосується і статичних заводів. Я не впевнений, що ви конкретно намагаєтесь вказати. На що посилається посилання?
Flater

Чи може бути такий випадок використання ін'єкції фабрики таким випадком, коли один розробив абстрактний клас запиту HTTP та стратегізував п’ять інших поліморфних дочірніх класів, один для GET, POST, PUT, PATCH та DELETE? Ви не можете знати, який метод HTTP буде використовуватися щоразу при спробі інтеграції зазначеної ієрархії класів з маршрутизатором типу MVC (який частково може покладатися на клас запиту HTTP. PSR-7 HTTP-інтерфейс повідомлення некрасивий.
Anthony Rutledge

@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).


Корова без коментарів не корисна.
Ентоні Ратлідж
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.