Я размышляю над дизайном библиотеки C #, которая будет иметь несколько различных функций высокого уровня. Конечно, эти высокоуровневые функции будут реализованы с использованием принципов проектирования классов SOLID в максимально возможной степени. Таким образом, вероятно, будут классы, предназначенные для непосредственного использования потребителями на регулярной основе, и «поддерживающие классы», которые являются зависимостями этих более распространенных классов «конечного пользователя».
Вопрос в том, как лучше всего спроектировать библиотеку:
- DI Agnostic - Хотя добавление базовой «поддержки» для одной или двух общих библиотек DI (StructureMap, Ninject и т. Д.) Кажется разумным, я хочу, чтобы потребители могли использовать библиотеку с любой структурой DI.
- Можно использовать без DI. Если пользователь библиотеки не использует DI, библиотека должна быть максимально простой в использовании, сокращая объем работы, которую пользователь должен выполнить для создания всех этих «неважных» зависимостей просто для того, чтобы добраться до «настоящие» классы, которые они хотят использовать.
В настоящее время я думаю о том, чтобы предоставить несколько «модулей регистрации DI» для общих библиотек DI (например, реестр StructureMap, модуль Ninject), а также набор или классы Factory, которые не являются DI и содержат связь с этими несколькими фабриками.
Мысли?
Ответы:
Это на самом деле просто сделать, когда вы поймете, что DI - это шаблоны и принципы , а не технологии.
Чтобы разработать API-интерфейс, не зависящий от контейнера, следуйте этим общим принципам:
Программа для интерфейса, а не реализация
Этот принцип на самом деле является цитатой (из памяти) из Design Patterns , но он всегда должен быть вашей настоящей целью . DI это просто средство для достижения этой цели .
Применить принцип Голливуда
Голливудский принцип в терминах DI гласит: не вызывайте DI Container, он позвонит вам .
Никогда не запрашивайте зависимость напрямую, вызывая контейнер из своего кода. Спросите об этом неявно с помощью Constructor Injection .
Используйте конструктор инъекций
Когда вам нужна зависимость, запросите ее статически через конструктор:
Обратите внимание, как класс Service гарантирует свои инварианты. Как только экземпляр создан, зависимость гарантированно станет доступной из-за сочетания пункта Guard и
readonly
ключевого слова.Используйте Abstract Factory, если вам нужен недолговечный объект
Зависимости, внедренные с помощью Constructor Injection, обычно бывают долгоживущими, но иногда вам нужен недолговечный объект или для построения зависимости на основе значения, известного только во время выполнения.
Смотрите это для получения дополнительной информации.
Сочинять только в последний ответственный момент
Держите предметы развязанными до самого конца. Как правило, вы можете подождать и подключить все в точке входа приложения. Это называется корнем композиции .
Подробнее здесь:
Упростите, используя Фасад
Если вы чувствуете, что полученный API становится слишком сложным для начинающих пользователей, вы всегда можете предоставить несколько классов Facade, которые инкапсулируют общие комбинации зависимостей.
Чтобы обеспечить гибкий Фасад с высокой степенью обнаружения, вы можете рассмотреть возможность использования Fluent Builders. Что-то вроде этого:
Это позволило бы пользователю создать Foo по умолчанию, написав
Однако было бы очень легко обнаружить, что можно предоставить пользовательскую зависимость, и вы могли бы написать
Если вы представите, что класс MyFacade инкапсулирует множество различных зависимостей, я надеюсь, что ясно, как он будет обеспечивать правильные значения по умолчанию, в то же время делая расширяемость обнаруживаемой.
FWIW, долгое время после написания этого ответа, я расширил концепции и написал более длинный пост в блоге о библиотеках , дружественных к DI , и сопутствующий пост о DI-Friendly Frameworks .
источник
Термин «внедрение зависимостей» не имеет никакого отношения к контейнеру IoC, даже если вы склонны видеть, что они упоминаются вместе. Это просто означает, что вместо написания вашего кода вот так:
Вы пишете это так:
То есть, когда вы пишете код, вы делаете две вещи:
Положитесь на интерфейсы, а не на классы, когда вы думаете, что реализация может потребоваться изменить;
Вместо того, чтобы создавать экземпляры этих интерфейсов внутри класса, передавайте их как аргументы конструктора (в качестве альтернативы они могут быть назначены общедоступным свойствам; первый - внедрение конструктора , второй - внедрение свойства ).
Ничто из этого не предполагает существования какой-либо библиотеки 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 имеет свой собственный контейнер IoCIContainer
, который используется для этого, а уComponent
класса естьGetService
метод, который является фактическим локатором службы. Конечно, ничто не мешает вам использовать любой или все эти интерфейсы безIContainer
определенного локатора. Сами сервисы слабо связаны с контейнером.Контракты в WCF построены исключительно на интерфейсах. На конкретный класс обслуживания обычно ссылаются по имени в файле конфигурации, который по сути является DI. Многие люди не понимают этого, но вполне возможно заменить эту систему конфигурации на другой контейнер IoC. Возможно, более интересно то, что поведение службы - это все примеры,
IServiceBehavior
которые можно добавить позже. Опять же, вы можете легко подключить это к контейнеру IoC и выбрать подходящее поведение, но эта функция полностью применима без нее.И так далее. Вы найдете DI повсюду в .NET, просто обычно это делается настолько плавно, что вы даже не думаете о нем как о DI.
Если вы хотите спроектировать библиотеку с поддержкой DI для максимального удобства использования, то, вероятно, лучше всего предложить собственную реализацию IoC по умолчанию с использованием облегченного контейнера.
IContainer
это отличный выбор для этого, потому что он является частью самого .NET Framework.источник
IContainer
самом деле это не контейнер в одном абзаце. ;)private readonly
поля? Это замечательно, если они используются только декларирующим классом, но OP указал, что это был код уровня фреймворка / библиотеки, что подразумевает подклассы. В таких случаях вы обычно хотите, чтобы важные зависимости подвергались подклассам. Я мог бы написатьprivate readonly
поле и свойство с явными получателями / установщиками, но ... впустую потраченное место в примере и больше кода, чтобы поддерживать его на практике, без реальной выгоды.РЕДАКТИРОВАТЬ 2015 : время прошло, теперь я понимаю, что все это было огромной ошибкой. Контейнеры IoC ужасны, а DI - очень плохой способ справиться с побочными эффектами. По сути, все ответы здесь (и сам вопрос) следует избегать. Просто знайте о побочных эффектах, отделяйте их от чистого кода, и все остальное или становится на свои места или не имеет значения и ненужной сложности
Исходный ответ следует:
При разработке SolrNet мне пришлось столкнуться с тем же решением . Я начал с цели быть DI-дружественным и независимым от контейнеров, но по мере того, как я добавлял все больше и больше внутренних компонентов, внутренние фабрики быстро становились неуправляемыми, и получающаяся библиотека была негибкой.
В итоге я написал свой собственный очень простой встроенный контейнер IoC, в то же время предоставив средство Windsor и модуль Ninject . Интеграция библиотеки с другими контейнерами - это просто вопрос правильного подключения компонентов, поэтому я мог бы легко интегрировать ее с Autofac, Unity, StructureMap, что угодно.
Недостатком этого является то, что я потерял способность просто
new
обслуживать. Я также взял зависимость от CommonServiceLocator, которого я мог бы избежать (я мог бы изменить его в будущем), чтобы упростить реализацию встроенного контейнера.Более подробная информация в этом блоге .
MassTransit, похоже, полагается на нечто подобное. Он имеет интерфейс IObjectBuilder, который на самом деле является IServiceLocator от CommonServiceLocator с парой дополнительных методов, затем он реализует это для каждого контейнера, то есть NinjectObjectBuilder, и обычного модуля / средства, то есть MassTransitModule . Затем он полагается на IObjectBuilder для создания экземпляра того, что ему нужно. Конечно, это правильный подход, но лично мне он не очень нравится, так как он фактически слишком часто обходит контейнер, используя его в качестве локатора службы.
MonoRail также реализует свой собственный контейнер , который реализует старый добрый IServiceProvider . Этот контейнер используется во всей этой структуре через интерфейс, который предоставляет хорошо известные сервисы . Чтобы получить конкретный контейнер, он имеет встроенный локатор поставщика услуг . Средство Windsor указывает этот локатор поставщика услуг на Windsor, делая его выбранным поставщиком услуг.
Итог: идеального решения не существует. Как и при любом проектном решении, эта проблема требует баланса между гибкостью, ремонтопригодностью и удобством.
источник
ServiceLocator
. Если вы применяете функциональные методы, вы в конечном итоге получаете эквивалент замыканий, которые являются классами с внедрением зависимостей (где зависимости - это закрытые переменные). Как еще? (Я действительно любопытно, потому что я не люблю DI либо.)Что бы я сделал, это спроектировал бы мою библиотеку в независимом от DI контейнере, чтобы максимально ограничить зависимость от контейнера. Это позволяет при необходимости заменять контейнер DI на другой.
Затем представьте слой над логикой DI пользователям библиотеки, чтобы они могли использовать любую среду, выбранную вами через интерфейс. Таким образом, они по-прежнему могут использовать предоставленную вами функциональность DI и могут свободно использовать любую другую среду для своих собственных целей.
Позволить пользователям библиотеки подключать свою собственную структуру DI мне кажется немного неправильным, поскольку это значительно увеличивает объем обслуживания. Это также становится скорее средой плагина, чем прямым DI.
источник