Що не так з DI абстракцією ASP.NET Core?

Кілька місяців тому, коли ASP.NET Core був ще в RC1, я робив перші незручні спроби перевести свій тестовий проект з MVC 5 на ASP.NET Core. На той момент у проекті вже використовувалася IOC бібліотека Simple Injector, і з цієї причини я хотів продовжувати використовувати цю бібліотеку, благо була підтримка з rc1. Я стежив за виходом нових версій цієї бібліотеки і відносно недавно натрапив на досить цікаву, на мій погляд, статтю, розміщену в тематичному блозі Simple Injector. Хоч стаття і спирається на відповідну бібліотеку, але основна її цінність у піднятті більш загальної проблеми - нової 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 і IServceProvider), нова абстракція додає Register API поверх IServceCollection. Суть цієї абстракції для 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 абстракцію і сторонніми бібліотеками, які використовують цю абстракцію.

На наш погляд, як розробник програми, ви повинні утримуватися від використання адаптера і в наступній статті я розповім більш детально, як підійти до цього і чому, навіть без несумісного контейнера, це надійний шлях вперед.

Будьте на зв'язку.