Pull to refresh

Идея небольшого дополнительного механизма для контейнера внедрения зависимостей

Level of difficultyMedium
Reading time3 min
Views3.2K

Хочу поделиться соображениями для решения расхожей проблемы контейнера внедрения зависимостей. В сишарпе это называется коллекцией сервисов, но я буду использовать общее для всех название.

Расхожая проблема

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

Предположим, у нас есть заказчик, которому мы сделали три сервиса в одном приложении, которые обошлись нам, например, в 150 контроллеров по 50 контроллеров каждый сервис. У нас есть интерфейс ILogger и его файловая имплементация. Мы соответственно регистрируем это в контейнере зависимостей.

И вот заказчик однажды хочет трафик логов направить:

  • по одному сервису (50 контроллеров) куда-то в RabbitMq

  • по другому (ещё 50 контроллеров) куда-то в PostgresSQL

  • по третьему (ещё 50 контроллеров) файл

Мы заводим ещё две имплементации логера...

Но вот теперь самое главное — у нас нет возможности указать, какой логер где использовать.

Есть разные способы с этим справиться. Все они так или иначе сводятся к тому, что нам нужно пройтись по всем 150-ти контроллерам, заменить ожидание зависимостей и дописать ещё логику их получения. Часто, например, для этого регистрируют коллекцию имплементаций, получают и обходят её на местах, выбирая нужную имплементацию из неё.

Майкрософт предложили в качестве решения именованную (keyed) регистрацию сервисов:

builder.Services.AddSingleton<ILogger, FileLogger>("file");
builder.Services.AddSingleton<ILogger, RabbitMqLogger>("rabbitmq");
builder.Services.AddSingleton<ILogger, PostgresSqlLogger>("postgres");
...

// Использование в конструкторе с атрибутом для указания ключа/имени
public Some1Controller([FromKeyedServices("file")] ILogger logger) { ... }
public Some2Controller([FromKeyedServices("rabbitmq")] ILogger logger) { ... }
public Some3Controller([FromKeyedServices("postgres")] ILogger logger) { ... }

Но будем честны — это не решает проблему. Проще сразу поставить в зависимости конкретные типы. Ибо всё равно нельзя избежать редактирования 150-ти контроллеров. А именованные зависимости ещё и создают видимость какой-то правильности, хотя по существу являются нарушением инверсии зависимостей и требуют точно такой же конкретики, только каким-то очень извилистым образом.

И это хорошо, если у нас 150, а не 1500 контроллеров.

Идея решения

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

У Майкрософт есть интерфейс провайдера System.IServiceProvider. Но он для этого не годится, и нельзя просто так его ни исправить, ни заменить, так как он используется не только в контейнере внедрения зависимостей, а связан много с чем ещё.

Но если предположить, что проблема интерфейса провайдера как-то решена, то в логику резолвинга зависимостей добавился бы ещё один шаг: при получении зависимости, если у неё есть имплементация этого провайдера, то нужно делегировать резолвинг ему, передав что именно запрашивается и кто именно запрашивает.

Таким образом можно было бы, создав один провайдер, в нём написать логику резолвинга зависимостей, обрабатывая переданный в него тип предъявителя запроса, как критерий резолвинга.

Далее, например, по пространству имён, или по имени предъявителя, или просто по заранее заготовленному перечню соответствий типов (который можно унести хоть за пределы приложения, например в базу), или ещё каким-либо образом определять, кому какую зависимость отдавать.

В итоге все 150 контроллеров останутся нетронутыми, а вся логика вариативности резолвинга зависимостей сосредоточена в одном месте — в провайдере.

Такая проблема существует, например, и в Laravel (php). Правда в нём уже есть возможность передачи критерия при запросе зависимости, но при автовайринге это не используется, поэтому такие проблемы не редко решают подобным же образом — дописывая логику в конструкторах.

Пул-реквеста конкретной реализации для контейнера от Майкрософт у меня нет. Я пробовал разобраться — это не просто. Но я предложил новый интерфейс наследуя старый. Это мне показался наименее проблематичный путь. Но поломки обратной совместимости не избежать. Своё предложение в таком же не очень конкретном виде я написал в ишу на гитхабе в проекте dotnet/runtime, надеясь на просто ознакомление и интерес к варианту решения проблемы. Поэтому же я решил публично поделиться соображениями и здесь.

Tags:
Hubs:
Total votes 5: ↑4 and ↓1+5
Comments37

Articles