Dependency Inject (DI) "дружественная" библиотека - PullRequest
225 голосов
/ 12 января 2010

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

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

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

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

Мысли

Ответы [ 4 ]

355 голосов
/ 12 января 2010

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

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

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

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

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

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

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

Использование конструктора 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-Friendly Libraries и сопутствующий пост о DI-Friendly Frameworks .

38 голосов
/ 12 января 2010

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

4 голосов
/ 12 января 2010

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

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


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

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

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

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

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

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

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

1 голос
/ 12 января 2010

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...