Dependency Inject (DI) "дружественная" библиотека
я обдумываю дизайн библиотеки C#, которая будет иметь несколько различных функций высокого уровня. Конечно, эти функции высокого уровня будут реализованы с помощью SOLID принципы проектирования класса как можно больше. Таким образом, вероятно, будут классы, предназначенные для потребителей, которые будут использоваться непосредственно на регулярной основе, и "классы поддержки", которые являются зависимостями этих более распространенных классов "конечного пользователя".
вопрос в том, каков наилучший способ проектирования библиотека такова:
- DI Agnostic-хотя добавление базовой "поддержки" для одной или двух общих библиотек DI (StructureMap, Ninject и т. д.) кажется разумным, я хочу, чтобы потребители могли использовать библиотеку с любой структурой DI.
- non-DI usable - если потребитель библиотеки не использует никакой DI, библиотека все равно должна быть максимально проста в использовании, уменьшая объем работы, которую пользователь должен сделать, чтобы создать все эти" неважные " зависимости только для того, чтобы добраться до "реальные" классы они хотят использовать.
мое текущее мышление состоит в том, чтобы предоставить несколько "модулей регистрации DI" для общих библиотек DI (например, реестр StructureMap, модуль Ninject) и набор или фабричные классы, которые не являются DI и содержат связь с этими несколькими фабриками.
мысли?
4 ответа:
Это на самом деле просто сделать, как только вы поймете, что Ди-о закономерности и принципы, а не технологии.
чтобы спроектировать 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.
более детально здесь:
- где я должен сделать инъекцию с Ninject 2+ (и как я могу организовать мои модули?)
- дизайн-где должны быть зарегистрированы объекты при использовании Windsor
упростить использование фасада
если вы чувствуете, что полученный API становится слишком сложным для начинающих пользователей, вы всегда можете предоставить несколько фасад классы, которые инкапсулируют общие зависимости сочетания.
чтобы обеспечить гибкий фасад с высокой степенью открытости, вы можете рассмотреть возможность предоставления беглых Строителей. Что-то вроде этого:
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; } }
то есть, вы сделать две вещи, когда вы пишете свой код:
полагайтесь на интерфейсы вместо классов всякий раз, когда вы думаете, что реализация может потребоваться быть измененным;
вместо того, чтобы создавать экземпляры этих интерфейсов внутри класса, передайте их в качестве аргументов конструктора (альтернативно, они могут быть назначены общедоступным свойствам; первый -конструктор инъекций, последний инъекции собственность).
ничто из этого не предполагает существования какой-либо библиотеки DI, и это действительно не делает код более сложным для написания без один.
если вы ищете пример этого, не смотрите дальше, чем сама платформа .NET Framework:
List<T>
осуществляетIList<T>
. Если вы создаете свой класс для использованияIList<T>
(илиIEnumerable<T>
), вы можете воспользоваться такими понятиями, как lazy-loading, как Linq to SQL, Linq to Entities и NHibernate, которые все делают за кулисами, обычно через инъекцию свойств. Некоторые классы фреймворка фактически принимают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. Этот контейнер используется во всей этой структуре через интерфейс, который предоставляет известный сервис. Чтобы получить бетонный контейнер, он имеет встроенный локатор поставщика услуг. Элемент Виндзор объекта указывает этот локатор поставщика услуг на Windsor, что делает его выбранным поставщиком услуг.
итог: нет идеального решения. Как и любое дизайнерское решение, этот вопрос требует баланса между гибкостью, ремонтопригодностью и удобством.
то, что я бы сделал, это создать свою библиотеку в агностическом контейнере DI, чтобы максимально ограничить зависимость от контейнера. Это позволяет поменять на DI контейнер для другого, если это необходимо.
затем предоставьте слой над логикой DI пользователям библиотеки, чтобы они могли использовать любую структуру, которую вы выбрали через свой интерфейс. Таким образом, они все еще могут использовать функции DI, которые вы предоставили, и они могут использовать любую другую структуру для своих собственных цели.
разрешение пользователям библиотеки подключать свои собственные рамки DI кажется мне немного неправильным, поскольку это значительно увеличивает объем обслуживания. Это также становится более плагинов среды, чем прямой DI.