[Из песочницы] Что не так с DI абстракцией ASP.NET Core?
Статья из блога IOC библиотеки Simple Injector
Автор Steve
Буду рад, если укажете на ошибки и неточности перевода.
Последние несколько лет Microsoft занималась разработкой новой версии платформы. NET: .NET Core. .NET Core — это полный редизайн существующей платформы .NET, нацеленный на настоящую кроссплатформенность и совместимость с облачными технологиями. Мы внимательно следили за развитием .NET Core и выпускали совместимые с платформой версии Simple Injector, начиная с RC1. С выпуском Simple Injector v3.2 мы официально поддерживаем .NET Core.
Как вы могли заметить, Microsoft добавила свою собственную DI библиотеку в качестве одного из основных компонентов фреймворка. Кто-то может воскликнуть «наконец-то!». Отсутствие такого компонента породило множество опенсорсных DI библиотек для .NET. И Simple Injector, очевидно, один из них.
Не поймите меня неправильно, мы аплодируем Microsoft за продвижение DI в качестве основной практики в .NET, и это, вероятно, приведет к появлению еще большего количества разработчиков, практикующих DI, что в свою очередь положительно скажется на нашей отрасли. Проблема, однако, начинается с абстракции, которую Microsoft определила на вершине своего встроенного DI контейнера. По сравнению с предыдущими Resolve абстракциями (IDependencyResolver и IServiceProvider), новая абстракция добавляет Register API поверх IServiceCollection. Суть этой абстракции для Microsoft в том, что другие (более функционально богатые) DI библиотеки могут подключаться в платформу, в то время как разработчики приложений, сторонних инструментов и фреймворков используют стандартизированную абстракцию для регистрации зависимостей. Это дает разработчикам приложений стандарт для интеграции DI библиотек на их выбор.
На первый взгляд может показаться, что иметь такую абстракцию — хорошая мысль. Вообще говоря, в нашей отрасли программного обеспечения мало проблем, которые не могут быть решены путем добавления (дополнительных) уровней абстракции. Хотя в данном случае рассуждения Microsoft ошибочны. Эксперты DI предупреждали их об этой проблеме с самого начала, но безуспешно. Mark Seemann довольно точно описал проблемы с этим подходом в целом здесь, где, на мой взгляд, основные моменты его рассуждений это:
- Такой подход тянет в направлении наименьшего общего знаменателя
- Такой подход подавляет инновации
- Такой подход добавляет ад версионирования
- Становится сложнее работать, не используя DI контейнер
- Если разработкой адаптеров будут заниматься члены open-source сообщества, у этих адаптеров может быть разный уровень качества и они могут не быть совместимы с последней версией Conforming Container(прим.пер. имеется в виду шаблон, описанный здесь)
Это реальные проблемы, стоящие перед нами сегодня в новой DI абстракции в .NET Core. DI контейнеры часто имеют очень уникальные и несовместимые особенности, когда речь заходит об их registration API. Simple Injector, например, очень тщательно спроектирован в области обнаружения многочисленных ошибок конфигурации. Один из самых ярких примеров (а их гораздо больше) — его диагностические способности. Это одна из особенностей, которые в корне несовместимы с ожиданиями, которые будут у пользователей DI абстракции. А что же будут ожидать пользователи от новой абстракции?
Пользователей DI абстракции можно разделить на три группы: разработчики фреймворков, внешних библиотек и самих приложений; особенно разработчики фреймворков и внешних библиотек, которые сейчас задумываются над добавлением регистрации своих зависимостей через общую абстракцию. Так как для этих двух групп разработчиков практически невозможно проверить их код со всеми доступными адаптерами они будут тестировать свой код с помощью встроенного контейнера. И пока эти разработчики используют встроенный контейнер они будут (и, вероятно, должны) неявно ожидать стандартного поведения от встроенного контейнера — не важно какой адаптер используется. Другими словами, это встроенный контейнер определяет и контракт, и поведение абстракции. Каждый созданный адаптер должен быть точным надмножеством встроенного контейнера. Отклонение от нормы не допускается, так как это нарушило бы работу внешних библиотек, которые зависят от поведения по умолчанию встроенного контейнера.
Диагностика и верификация в Simple Injector — одни из многих возможностей, позволяющих вести разработку намного продуктивнее. Они позволяют находить проблемы, которые могли бы быть обнаружены намного позже в процессе разработки, если бы вы использовали другие DI библиотеки. Но выполнение диагностики и приложения и сторонних компонент вызовет проблемы — очень маловероятно, что сторонние компоненты будут автоматически «играть по правилам» с диагностикой Simple Injector. Велика вероятность, что они будут регистрировать зависимости таким образом, при котором Simple Injector будет считать их подозрительными, даже если они (надеюсь) хорошо протестировали регистрацию в особых случаях со стандартным контейнером. Гипотетическому адаптеру для Simple Injector было бы невозможно различить регистрации сторонних зависимостей и зависимостей приложения. Отключение диагностики полностью уберет один из самых важных предохранительных механизмов, в то время как сохранение диагностики приведет к ложным срабатываниям со стороны сторонних компонентов, а эти ложные срабатывания придется подавлять разработчикам приложения. Поскольку регистрация сторонних компонент в большинстве своем скрыта от разработчиков приложений, работа с всеми этими вопросами может оказаться сложной, разочаровывающей и иногда даже невозможной. Можно утверждать — хорошо, что Simple Injector находит проблемы со сторонними инструментами. Но если вы захотите обратиться к разработчикам сторонних библиотек и попытаетесь объяснить им «проблему», то вероятно они переведут стрелки на нас, ведь «очевидно» что мы разработали «несовместимый» адаптер.
Диагностические способности в Simple Injector — одни из многих несовместимостей, с которыми мы столкнулись, когда писали адаптер для .NET Core DI абстракции. Другие несовместимости:
- Способ которым Simple Injector явно отделяет регистрацию коллекций от отображения one-to-one
- Способ которым Simple Injector обрабатывает open-generic регистрацию.
Чтобы сделать полностью совместимый адаптер для Simple Injector потребуется удалить много известных возможностей фреймворка, тем самым изменяя существующее поведение библиотеки и превращая ее во что то, что нарушает принципы, которыми мы руководствовались при разработке. Непривлекательное решение. Мало того, что оно приведет к появлению ломающих совместимость изменений, но оно так же пропадут возможности и поведение, за которые Simple Injector и любили разработчики. В этом смысле наличие адаптера — это «душение инноваций», как описывал Mark. В Simple Injector мы сделали много инноваций, а адаптер сделает Simple Injector практически бесполезным для его пользователей. Адаптер так же ограничит нас от внесения дальнейших улучшений и новшеств. Кто-то может посчитать философию Simple Injector радикальной, но мы думаем иначе. Мы разработали его таким образом, который, как мы считаем, наилучшим образом подойдет для наших пользователей. И кол-во скачиваний NuGet пакета указывает, что многие разработчики согласны с нами. Соответствие определенному адаптеру будет мешать нам и дальше удовлетворять наших пользователей.
Хотя видение Simple Injector может отклоняться от нормы больше, чем большинство других контейнеров, сам акт определения общей абстракции для будущих DI библиотек — даже более радикальная или инновационная точка зрения, которая «душит инновации» будущих библиотек. Только представьте себе один из других контейнеров, внедряющих такие же проверки, которые обеспечивает Simple Injector? Такая особенность не может быть введена без нарушения контракта DI абстракции. Сам факт наличия такого адаптера может блокировать прогресс в нашей отрасли.
Этим объяснением, я надеюсь я так же прояснил, что Microsoft DI абстракция даже не «наименьший общий знаменатель», потому что «наименьший общий знаменатель» подразумевает совместимость со всеми DI библиотеками. Как я высказался здесь, есть шанс, что ни одна из существующих DI библиотек не совместима полностью с этой абстракцией. Частично это сводится к тому, что, хотя встроенный контейнер определяет контракт абстракции, проект с тестами этой абстракции испытывает недостаток в солидном количестве тестовых примеров, которые бы полностью определили точное поведение во всех сценариях. До сих пор все реализации адаптера были попыткой угадать и надеяться на лучшее — на то, что реализация адаптера практически синхронизирована с поведением встроенного контейнера. Разработчики Autofac к примеру, только что поняли, что у них есть некоторые довольно серьезные проблемы с совместимостью и в итоге пришли к тем же самым выводам что и мы.
Это не было бы так плохо, если библиотека DI Microsoft была богата возможностями и содержала бы такие функции, как верификация и диагностика из Simple Injector. Тогда мы все могли бы использовали одну и тот же полнофункциональную DI библиотеку. К сожалению, реализация далека не так функционально богата, а сама Microsoft описала их реализацию как
Минималистичный DI контейнер, полезный в тех случаях, когда вам не нужны какие-либо дополнительные возможности для инъекций
Что еще хуже, с тех пор как встроенный контейнер определяет контракт абстракции, добавление новых возможностей во встроенный контейнер сломает все существующие адаптеры! Любой сторонний разработчик, использующий абстракцию, будет тестировать (свою библиотеку) только с помощью встроенной библиотеки (.NET Core’s DI). А когда библиотека стороннего разработчика начинает зависеть от какой-то функции, добавленной во встроенный контейнер, и который при этом еще не поддерживается адаптером, то что-то сломается и пострадает разработчик приложения. Это один из аспектов ада версионирования, который Mark Seemann обсуждает в своем блоге. Будем надеяться, что, по крайней мере, Microsoft будет увеличивать основной номер версии (major version number) каждый раз, когда они будут вносить изменения. Мало того, что их текущая реализация «минималистична», она никогда не сможет развиваться в полностью пригодный многофункциональный DI контейнер, потому что они загнали себя в угол: каждое будущее изменение — это изменение, ломающие совместимость, от которого всем будет плохо.
Лучшее решение — избегать использование абстракции и ее адаптеров полностью. Как Mark Seemann довольно точно объяснил здесь и здесь, библиотекам и фреймворкам, возможно, не нужно использовать DI контейнер вообще. К сожалению, сам факт определения абстракции намного усложняет попытку избежать ее использования. Определяя абстракцию и активно продвигая ее использование, Microsoft приводит тысячи сторонних разработчиков библиотек и фреймворков к тому, чтобы они перестать думать об определении правильной абстракции для библиотеки и фреймворка (статьи Mark Seemann ясно описывают это). Разработчики больше не думают об этом, потому что Microsoft заставляет их верить, что весь мир нуждается в одной общей абстракции. Мы уже видели, как новые фабричные интерфейсы для MVC вступали в игру очень поздно (например, как IViewComponentActivator абстракции до начала RC2). И если мы видим, что команда MVC доводит такие ошибки до столь позднего этапа цикла разработки, то что мы можем ожидать от всех тех разработчиков, которые начинают разрабатывать на новой платформе .NET?
Заключение
Определение DI абстракции — болезненная ошибка Microsoft, которая будет преследовать нас на протяжении многих последующих лет. Она уже подавляет инновации, порождает ад версионирования и расстраивает многих разработчиков. Абстракция несовместима со многими библиотеками DI и, вопреки рекомендации экспертов, Microsoft решила сохранить ее, деля мир на несовместимые и частично совместимые контейнеры, что приводит к бесконечным сообщениям о проблемах адаптеров, реализующих DI абстракцию и сторонними библиотеками, которые используют эту абстракцию.
На наш взгляд, как разработчик приложения, вы должны воздерживаться от использования адаптера и в следующей статье я расскажу более подробно, как подойти к этому и почему, даже без несовместимого контейнера, это надежный путь вперед.
Будьте на связи.
Комментарии (7)
19 июля 2016 в 23:30
–3↑
↓
По мне так, введение в эту сферу какого -то стандарта и его продвижение — это огромный плюс по сравнению с тем что умрут все остальные опенсурс реализации. Так хоть не будет зоопарка стандартов.19 июля 2016 в 23:37
+1↑
↓
Только это не стандарт, а реализация. И что делать, если она не покрывает мои потребности?
19 июля 2016 в 23:40 (комментарий был изменён)
+2↑
↓
так опен сурс же, делайте пул реквесты и да прибудет!
И весь дот нет фреймворк состоит из реализаций и ничего, никто не пилит своей реализации вывода на консоль или работы http, разве только по крайней нужде.19 июля 2016 в 23:43
0↑
↓
Угу, и будет реализация, имеющая объединение всех фич — иными словами, зоопарк.
19 июля 2016 в 23:48
0↑
↓
Нет, зоопарк это когда у вас куча реализаций одного и того же, а когда все действительно нужные фичи в одной, то это просто хорошая стандартная реализация.19 июля 2016 в 23:49
–1↑
↓
Кто будет определять «действительно нужные фичи»?
Куча реализаций одного и того же — это не зоопарк, а свобода выбора.
20 июля 2016 в 00:36
0↑
↓
Стандартный контейнер определяет необходимый для .NET Core минимум способов регистрации зависимости: transient, singleton, scoped. Их реализует любой контейнер. Нужно ли было усложнить интерфейс добавления зависимостей и сделать его более богатым и совместимым со всеми фичами Simple Injector, Ninject, Castle Winsdor, Autofac и т.д?
Я считаю, что нет, это не принесло бы значительной пользы, но сильно усложнило бы написание адаптеров для сторонних библиотек. Это хорошая практика дизайна системы — делать точки расширения с простым и очевидным интерфейсом, без сложных внутренних инвариантов. Такие интерфейсы легко понять и реализовать.
Понять, запомнить и реализовать все инварианты регистрации любого из современных контейнеров очень и очень сложно, что привело бы к кучке реализаций, совместимых между собой на 90%, и порождающих такие баги, что обычные ошибки регистрации будут потом казаться ясными, как предупреждения компилятора.
В кои-то веки Майкрософт предпочла простоту универсальности.