“Дружня” бібліотека введення залежностей (DI)


230

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

Питання полягає в тому, який найкращий спосіб спроектувати бібліотеку так:

  • DI Agnostic - Хоча додавання базової «підтримки» для однієї або двох загальних бібліотек DI (StructureMap, Ninject тощо) видається розумним, я хочу, щоб споживачі могли використовувати бібліотеку з будь-якими рамками DI.
  • Користувач не для DI - якщо споживач бібліотеки не використовує DI, бібліотека все ще повинна бути максимально простою у використанні, зменшуючи кількість роботи, яку повинен виконати користувач, щоб створити всі ці "неважливі" залежності просто для того, щоб дістатися "справжні" класи, якими вони хочуть користуватися.

Моє теперішнє мислення полягає у наданні декількох "модулів реєстрації DI" для загальних бібліотек DI (наприклад, реєстр StructureMap, модуль Ninject), а також набір або класи Factory, які не є DI і містять з'єднання з цими кількома фабриками.

Думки?


Нова посилання на недіючу
M.Hassan

Відповіді:


360

Це насправді просто зробити, коли зрозумієш, що DI - це закономірності та принципи , а не технології.

Щоб розробити API в агностично-контейнерному режимі DI, дотримуйтесь цих загальних принципів:

Програма на інтерфейс, а не на реалізацію

Цей принцип насправді є цитатою (хоча з пам’яті) від « Шаблони дизайну» , але це завжди має бути вашою реальною метою . DI - це лише засіб для досягнення цієї мети .

Застосовуйте Голлівудський принцип

Голлівудський принцип в термінах DI говорить: Не дзвоніть контейнеру DI, він зателефонує вам .

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

Використовуйте інжекцію конструктора

Коли вам потрібна залежність, запитайте про неї статично через конструктор:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

Зверніть увагу, як клас обслуговування гарантує інваріантам. Щойно створений екземпляр, залежність буде гарантовано доступною через поєднання Застереження та readonlyключового слова.

Використовуйте абстрактний завод, якщо вам потрібен короткочасний об’єкт

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

Дивіться це для отримання додаткової інформації.

Складайте лише в останній відповідальний момент

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

Детальніше тут:

Спростіть за допомогою фасаду

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

Щоб забезпечити гнучкий фасад з високим ступенем розкриття, ви можете розглянути можливість створення плавних будівельників. Щось на зразок цього:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

Це дозволить користувачеві створити Foo за замовчуванням, написавши

var foo = new MyFacade().CreateFoo();

Однак було б дуже зрозуміло, що можна поставити власну залежність, і ви могли б написати

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

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


FWIW, довго після написання цієї відповіді, я розширив поняття, розміщені в цій статті, і написав довший допис у блозі про бібліотеки , сприятливі для DI , та супутню публікацію про рамки, зручні для DI .


21
Хоча це чудово звучить на папері, на моєму досвіді, коли у вас багато внутрішніх компонентів взаємодіють складними способами, ви закінчуєте безліч заводів для управління, що ускладнює обслуговування. Крім того, фабрикам доведеться керувати способом життя створених компонентів, коли ви встановите бібліотеку в реальний контейнер, це буде суперечити власному стилю управління контейнером. Заводи та фасади заважають справжнім контейнерам.
Маурісіо Шеффер

4
Мені ще потрібно знайти проект, який би це все зробив .
Маурісіо Шеффер

31
Ну, так ми розробляємо програмне забезпечення в Safewhere, тому ми не ділимося вашим досвідом ...
Марк Семанн

19
Я думаю, що фасади повинні бути закодовані вручну, оскільки вони представляють відомі (і, мабуть, звичайні) комбінації компонентів. Контейнер DI не повинен бути необхідним, тому що все можна підключити вручну (подумайте, Бідний чоловік DI). Нагадаємо, що Фасад - це лише необов'язковий клас зручності для користувачів вашого API. Просунуті користувачі можуть все-таки захотіти обійти Фасад і підключити компоненти на свій смак. Вони можуть захотіти для цього використовувати свій власний DI Contaier, тому я думаю, що було б недобросовісно змушувати конкретний контейнер DI, якщо вони не збираються його використовувати. Можливо, але не бажано
Марк Семанн

8
Це, можливо, найкраща відповідь, яку я коли-небудь бачив на SO.
Нік Ходжес

40

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

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Ви пишете це так:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Тобто ви робите дві речі, коли пишете свій код:

  1. Покладайтеся на інтерфейси, а не на класи, коли ви думаєте, що реалізацію, можливо, потрібно буде змінити;

  2. Замість того, щоб створювати екземпляри цих інтерфейсів всередині класу, передайте їх як аргументи конструктора (як альтернативу, вони можуть бути віднесені до загальнодоступних властивостей; перший - конструктор впорскування , другий - введення властивостей ).

Ніщо з цього не передбачає існування будь-якої бібліотеки DI, і це насправді не ускладнює написання коду без нього.

Якщо ви шукаєте приклад цього, не дивіться далі, ніж сама .NET Framework:

  • List<T>знаряддя IList<T>. Якщо ви проектуєте свій клас для використання IList<T>(або IEnumerable<T>), ви можете скористатися такими поняттями, як ледача завантаження, як Linq до SQL, Linq to Entities і NHibernate - все це відбувається за лаштунками, як правило, шляхом введення властивостей. Деякі рамкові класи фактично приймають IList<T>аргумент конструктора, наприклад BindingList<T>, який використовується для декількох функцій прив'язки даних.

  • Linq до SQL і EF повністю побудовані навколо IDbConnectionвідповідних інтерфейсів, які можна передавати за допомогою загальнодоступних конструкторів. Однак їх не потрібно використовувати; конструктори за замовчуванням працюють чудово, коли рядок з'єднання десь знаходиться у файлі конфігурації.

  • Якщо ви коли-небудь працюєте над компонентами WinForms, ви працюєте з "послугами", як-от INameCreationServiceабо IExtenderProviderService. Ви навіть не знаєте , що на насправді то , що конкретні класи є . .NET фактично має власний контейнер IoC IContainer, який використовується для цього, і Componentклас має GetServiceметод, який є фактичним локатором обслуговування. Звичайно, ніщо не заважає вам використовувати будь-який або всі ці інтерфейси без того IContainerчи іншого конкретного локатора. Самі сервіси лише слабко поєднуються з контейнером.

  • Контракти у WCF побудовані повністю навколо інтерфейсів. Фактичний конкретний клас обслуговування зазвичай посилається на ім'я у файлі конфігурації, який по суті є DI. Багато людей цього не усвідомлюють, але цілком можливо замінити цю конфігураційну систему іншим контейнером IoC. Можливо, цікавіше, що поведінка служби - це всі випадки, IServiceBehaviorякі можна додати пізніше. Знову ж таки, ви можете легко перенести це в контейнер IoC і змусити його вибрати відповідну поведінку, але ця функція може бути використана без жодної.

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

Якщо ви хочете розробити свою бібліотеку з підтримкою DI для максимальної зручності використання, то найкращим пропозицією є, мабуть, поставити власну реалізацію IoC за замовчуванням, використовуючи легкий контейнер. IContainerє чудовим вибором для цього, оскільки це частина самої .NET Framework.


2
Справжня абстракція для контейнера - це IServiceProvider, а не IContainer.
Маурісіо Шеффер

2
@Mauricio: Ви, звичайно, маєте рацію, але спробуйте пояснити тому, хто ніколи не використовував систему LWC, чому IContainerнасправді контейнер не є одним пунктом. ;)
Aaronaught

Аарон, просто цікаво, чому ти використовуєш приватний набір; замість того, щоб просто вказати поля як лише прочитані?
jr3

@Jreeter: Ти маєш на увазі, чому вони не private readonlyполя? Це чудово, якщо вони використовуються лише класом декларування, але OP вказав, що це код на рівні рамки / бібліотеки, що передбачає підкласифікацію. У таких випадках, як правило, потрібні важливі залежності, що піддаються підкласам. Я міг би написати private readonlyполе і властивість з явними getters / setters, але ... витрачений простір на прикладі та більше коду, який потрібно підтримувати на практиці, без реальної вигоди.
Aaronaught

Дякую за уточнення! Я припускав, що лише діти будуть доступні для читання полів.
jr3

5

EDIT 2015 : час минув, зараз я розумію, що вся ця справа була величезною помилкою. IoC контейнери жахливі, а DI - дуже поганий спосіб боротьби з побічними ефектами. Ефективно слід уникати всіх відповідей тут (і самого питання). Просто будьте в курсі побічних ефектів, відокремте їх від чистого коду, а все інше або стає на місце, або є неактуальним і непотрібним.

Оригінальна відповідь наступна:


Мені довелося зіткнутися з цим самим рішенням під час розробки SolrNet . Я почав з мети бути DI-дружньою та контейнерно-агностичною, але, коли я додавав все більше внутрішніх компонентів, внутрішні заводи швидко ставали некерованими, а отримана бібліотека - негнучка.

Я закінчив писати власний дуже простий вбудований контейнер IoC , надаючи також об'єкт Windsor та модуль Ninject . Інтеграція бібліотеки з іншими контейнерами - це лише питання належного підключення компонентів, тому я міг легко інтегрувати її в Autofac, Unity, StructureMap і все, що завгодно.

Мінус цього полягає в тому, що я втратив здатність просто newзбільшити послугу. Я також взяв залежність від CommonServiceLocator, якої я міг би уникнути (я міг би його рефактор в майбутньому), щоб зробити вбудований контейнер простішим у виконанні.

Детальніше у цій публікації в блозі .

MassTransit, здається, покладається на щось подібне. Він має інтерфейс IObjectBuilder, який є насправді IServiceLocator CommonServiceLocator із ще кількома методами, потім він реалізує це для кожного контейнера, тобто NinjectObjectBuilder та звичайний модуль / об'єкт, тобто MassTransitModule . Тоді він покладається на IObjectBuilder, щоб інстанціювати те, що йому потрібно. Це, звичайно, правильний підхід, але особисто мені це не дуже подобається, оскільки він насправді занадто сильно проходить навколо контейнера, використовуючи його як сервіс-локатор.

MonoRail також реалізує власний контейнер , який реалізує старий хороший IServiceProvider . Цей контейнер використовується у всьому цьому рамках через інтерфейс, який відкриває відомі сервіси . Щоб отримати бетонний контейнер, він має вбудований локатор постачальника послуг . Об'єкт Windsor вказує цей сервіс провайдер локатор Віндзор, що робить його обраний постачальник послуг.

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


12
Хоча я поважаю вашу думку, я вважаю ваші надмірно узагальнені "всі інші відповіді тут застарілими і слід уникати" вкрай наївно. Якщо ми з того часу дізналися що-небудь, саме те, що введення залежності - це відповідь на мовні обмеження. Не розвиваючись і не відмовляючись від наших сучасних мов, здається, нам буде потрібно DI, щоб вирівняти наші архітектури та досягти модульності. IoC як загальна концепція є лише наслідком цих цілей і, безумовно, бажаною. IoC контейнери абсолютно не потрібні ні для IoC, ні для DI, і їх корисність залежить від складених нами модульних композицій.
TNE

@tne Ваша згадка про те, що я є "надзвичайно наївною", є непривітальним рекламним гоміном. Я писав складні програми без DI чи IoC в C #, VB.NET, Java (мови, які ви вважаєте, що "потрібні" IoC, мабуть, судячи з вашого опису), це точно не стосується мовних обмежень. Йдеться про концептуальні обмеження. Я пропоную вам дізнатися про функціональне програмування, а не вдаватися до атомів ad-hominem та поганих аргументів.
Маурісіо Шеффер

18
Я згадав, що визнав вашу заяву наївною; це нічого не говорить про вас особисто і все стосується моєї думки. Будь ласка, не відчувайте образи від випадкового незнайомця. Я маю на увазі, що я не знаю всіх відповідних функціональних підходів програмування; ти цього не знаєш, і ти помилився. Я знав, що ти там добре обізнаний (швидко переглянув твій блог), тому мені стало прикро, що, здавалося б, знаючий хлопець скаже такі речі, як «нам не потрібен IoC». IoC відбувається буквально всюди у функціональному програмуванні (..)
Tne

4
і в програмному забезпеченні високого рівня в цілому. Насправді функціональні мови (та багато інших стилів) насправді доводять, що вони вирішують IoC без DI, я думаю, що ми обидва з цим згодні, і це все, що я хотів сказати. Якщо вам вдалося без DI на тих мовах, які ви згадали, як це зробили? Я припускаю, що не використовую ServiceLocator. Якщо ви застосовуєте функціональні методи, ви отримуєте еквівалент закриттів, які є класами, які вводять залежність (де залежність є змінними закритого типу). Як інакше? (Я дійсно цікаво, тому що я не люблю DI небудь.)
Tne

1
Контейнери IoC - це глобальні змінні словники, що забирають параметричність як інструмент міркування, тому цього слід уникати. IoC (концепція) як у "не дзвони нам, ми зателефонуємо тобі", застосовується технічно кожного разу, коли ти передаєш функцію як значення. Замість DI, моделюйте свій домен ADT, тоді пишіть перекладачі (cf Free).
Маурісіо Шеффер

1

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

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

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

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