Dependency Inject (DI) «дружественная» библиотека


230

Я размышляю над дизайном библиотеки C #, которая будет иметь несколько различных функций высокого уровня. Конечно, эти высокоуровневые функции будут реализованы с использованием принципов проектирования классов SOLID в максимально возможной степени. Таким образом, вероятно, будут классы, предназначенные для непосредственного использования потребителями на регулярной основе, и «поддерживающие классы», которые являются зависимостями этих более распространенных классов «конечного пользователя».

Вопрос в том, как лучше всего спроектировать библиотеку:

  • DI Agnostic - Хотя добавление базовой «поддержки» для одной или двух общих библиотек DI (StructureMap, Ninject и т. Д.) Кажется разумным, я хочу, чтобы потребители могли использовать библиотеку с любой структурой DI.
  • Можно использовать без DI. Если пользователь библиотеки не использует DI, библиотека должна быть максимально простой в использовании, сокращая объем работы, которую пользователь должен выполнить для создания всех этих «неважных» зависимостей просто для того, чтобы добраться до «настоящие» классы, которые они хотят использовать.

В настоящее время я думаю о том, чтобы предоставить несколько «модулей регистрации DI» для общих библиотек DI (например, реестр StructureMap, модуль Ninject), а также набор или классы Factory, которые не являются DI и содержат связь с этими несколькими фабриками.

Мысли?


Новая ссылка на неработающую ссылку на статью (SOLID): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Ответы:


360

Это на самом деле просто сделать, когда вы поймете, что DI - это шаблоны и принципы , а не технологии.

Чтобы разработать API-интерфейс, не зависящий от контейнера, следуйте этим общим принципам:

Программа для интерфейса, а не реализация

Этот принцип на самом деле является цитатой (из памяти) из Design Patterns , но он всегда должен быть вашей настоящей целью . DI это просто средство для достижения этой цели .

Применить принцип Голливуда

Голливудский принцип в терминах DI гласит: не вызывайте DI Container, он позвонит вам .

Никогда не запрашивайте зависимость напрямую, вызывая контейнер из своего кода. Спросите об этом неявно с помощью Constructor Injection .

Используйте конструктор инъекций

Когда вам нужна зависимость, запросите ее статически через конструктор:

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

Обратите внимание, как класс Service гарантирует свои инварианты. Как только экземпляр создан, зависимость гарантированно станет доступной из-за сочетания пункта Guard и readonlyключевого слова.

Используйте Abstract Factory, если вам нужен недолговечный объект

Зависимости, внедренные с помощью Constructor Injection, обычно бывают долгоживущими, но иногда вам нужен недолговечный объект или для построения зависимости на основе значения, известного только во время выполнения.

Смотрите это для получения дополнительной информации.

Сочинять только в последний ответственный момент

Держите предметы развязанными до самого конца. Как правило, вы можете подождать и подключить все в точке входа приложения. Это называется корнем композиции .

Подробнее здесь:

Упростите, используя Фасад

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

Чтобы обеспечить гибкий Фасад с высокой степенью обнаружения, вы можете рассмотреть возможность использования Fluent Builders. Что-то вроде этого:

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-Friendly Frameworks .


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

4
Я еще не нашел проект, который делает все это.
Маурисио Шеффер

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

19
Я думаю, что фасады должны быть закодированы вручную, потому что они представляют известные (и предположительно распространенные) комбинации компонентов. Контейнер DI не должен быть необходим, потому что все можно подключить вручную (подумайте, DI Бедного Человека). Напомним, что Facade - это просто дополнительный удобный класс для пользователей вашего 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 to SQL, Linq to Entities и NHibernate, все это делается за кулисами, обычно посредством внедрения свойств. Некоторые каркасные классы фактически принимают IList<T>аргумент конструктора, например BindingList<T>, который используется для нескольких функций привязки данных.

  • Linq to 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

Аарон, просто любопытно, почему ты используешь закрытый сет; вместо того, чтобы просто указывать поля как только для чтения?
13

@Jreeter: Вы имеете в виду, почему они не private readonlyполя? Это замечательно, если они используются только декларирующим классом, но OP указал, что это был код уровня фреймворка / библиотеки, что подразумевает подклассы. В таких случаях вы обычно хотите, чтобы важные зависимости подвергались подклассам. Я мог бы написать private readonlyполе и свойство с явными получателями / установщиками, но ... впустую потраченное место в примере и больше кода, чтобы поддерживать его на практике, без реальной выгоды.
Ааронаут

Спасибо за разъяснения! Я предполагал, что поля только для чтения будут доступны ребенку.
3

5

РЕДАКТИРОВАТЬ 2015 : время прошло, теперь я понимаю, что все это было огромной ошибкой. Контейнеры IoC ужасны, а DI - очень плохой способ справиться с побочными эффектами. По сути, все ответы здесь (и сам вопрос) следует избегать. Просто знайте о побочных эффектах, отделяйте их от чистого кода, и все остальное или становится на свои места или не имеет значения и ненужной сложности

Исходный ответ следует:


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

В итоге я написал свой собственный очень простой встроенный контейнер IoC, в то же время предоставив средство Windsor и модуль Ninject . Интеграция библиотеки с другими контейнерами - это просто вопрос правильного подключения компонентов, поэтому я мог бы легко интегрировать ее с Autofac, Unity, StructureMap, что угодно.

Недостатком этого является то, что я потерял способность просто newобслуживать. Я также взял зависимость от CommonServiceLocator, которого я мог бы избежать (я мог бы изменить его в будущем), чтобы упростить реализацию встроенного контейнера.

Более подробная информация в этом блоге .

MassTransit, похоже, полагается на нечто подобное. Он имеет интерфейс IObjectBuilder, который на самом деле является IServiceLocator от CommonServiceLocator с парой дополнительных методов, затем он реализует это для каждого контейнера, то есть NinjectObjectBuilder, и обычного модуля / средства, то есть MassTransitModule . Затем он полагается на IObjectBuilder для создания экземпляра того, что ему нужно. Конечно, это правильный подход, но лично мне он не очень нравится, так как он фактически слишком часто обходит контейнер, используя его в качестве локатора службы.

MonoRail также реализует свой собственный контейнер , который реализует старый добрый IServiceProvider . Этот контейнер используется во всей этой структуре через интерфейс, который предоставляет хорошо известные сервисы . Чтобы получить конкретный контейнер, он имеет встроенный локатор поставщика услуг . Средство Windsor указывает этот локатор поставщика услуг на Windsor, делая его выбранным поставщиком услуг.

Итог: идеального решения не существует. Как и при любом проектном решении, эта проблема требует баланса между гибкостью, ремонтопригодностью и удобством.


12
Хотя я уважаю ваше мнение, я считаю ваши чрезмерно обобщенные «все остальные ответы здесь устаревшими и их следует избегать» крайне наивными. Если мы узнали что-то с тех пор, это то, что внедрение зависимостей является ответом на ограничения языка. Без развития или отказа от наших существующих языков, нам кажется, что нам понадобится DI, чтобы сгладить нашу архитектуру и добиться модульности. IoC как общая концепция является лишь следствием этих целей и определенно желательной. Контейнеры IoC абсолютно не нужны ни для IoC, ни для DI, и их полезность зависит от составов модулей, которые мы создаем.
TNE

@tne Ваше упоминание о том, что я "чрезвычайно наивен", является нежелательным ad-hominem. Я написал сложные приложения без 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, а затем напишите интерпретаторы (см. Free).
Маурисио Шеффер

1

Что бы я сделал, это спроектировал бы мою библиотеку в независимом от DI контейнере, чтобы максимально ограничить зависимость от контейнера. Это позволяет при необходимости заменять контейнер DI на другой.

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.