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

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

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

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

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

Мысли?

Ответы

Ответ 1

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

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

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

Этот принцип - фактически цитата из памяти Design Patterns, но она всегда должна быть вашей реальной целью. 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.

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

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

Подробнее см. .

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

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

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

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

Если вы чувствуете, что полученный 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-Friendly Libraries и comanion post о DI-Friendly Frameworks.

Ответ 2

Термин "инъекция зависимостей" конкретно не имеет ничего общего с контейнером 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; }
}

То есть вы делаете две вещи, когда пишете свой код:

  • Полагайтесь на интерфейсы вместо классов всякий раз, когда вы думаете, что реализация может потребоваться изменить;

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

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

Если вы ищете пример этого, посмотрите не дальше, чем сама .NET Framework:

  • List<T> реализует IList<T>. Если вы создаете свой класс для использования IList<T> (или IEnumerable<T>), вы можете воспользоваться такими понятиями, как lazy-load, как 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.

Ответ 3

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

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


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

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

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

Подробнее в этом сообщении в блоге.

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

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

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

Ответ 4

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

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

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