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

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

вопрос в том, что является лучшим способом разработки библиотека такова:

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

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

мысли?

4 ответов


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

чтобы спроектировать API в контейнере 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; }
    }
}

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

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

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

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

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

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

более детально здесь:

упростить использование фасада

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

для того чтобы обеспечить гибкий фасад с высокой степенью обнаруживаемости, вы смогли рассматривать обеспечить беглых Строителей. Что-то вроде этого:--8-->

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-дружественные рамки.


термин "инъекция зависимостей" не имеет ничего общего с контейнером 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, все делают за кулисами, обычно через инъекцию свойств. Некоторые классы framework фактически принимают IList<T> в качестве аргумента конструктора, например BindingList<T>, который используется для нескольких функций привязки данных.

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

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

  • контракты в WCF построены полностью вокруг интерфейсов. Фактический конкретный класс обслуживания обычно ссылается по имени в файле конфигурации, который по существу является DI. Многие люди не понимают этого, но вполне возможно поменять эту систему конфигурации с другим контейнером IoC. Возможно, более интересно, что поведение службы-это все экземпляры IServiceBehavior который можно добавить позже. Опять же, вы можете легко подключить это к контейнеру IoC и выбрать соответствующее поведение, но функция полностью используется без одного.

и так далее и так далее. Вы найдете DI повсюду в .NET, просто обычно это делается так легко, что вы даже не думаете об этом как DI.

если вы хотите создать свою библиотеку с поддержкой DI для максимального удобства использования, лучшим предложением, вероятно, является предоставление собственной реализации IoC по умолчанию с использованием легкого контейнера. IContainer - отличный выбор для этого, потому что это часть самой .NET Framework.


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

оригинальный ответ следующее:


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

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

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

подробнее в этом блог пост.

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

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

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


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

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

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