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

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

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

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

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

Мысли?

4 ответа

Решение

Это на самом деле просто сделать, когда вы поймете, что 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 гарантирует свои инварианты. Как только экземпляр создан, зависимость гарантированно станет доступной из-за сочетания пункта охраны и 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.

Термин "внедрение зависимостей" не имеет никакого отношения к контейнеру 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.

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

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


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

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

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

Больше подробностей в этом посте.

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

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

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

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

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

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

Другие вопросы по тегам