Правильный DI или анти-паттерн локатора сервисов?

Я разрабатываю SDK и использую DI (я думаю неправильно) для своих классов и их зависимостей. Кажется, я перепутал DI и сервисный локатор. Мои проблемы:

  • Зная, где построить мой DI-контейнер. У меня нет среды выполнения/точки входа для использования в качестве составной корень - и не должен, согласно этому сообщению.
  • Сроки службы. То, как я создаю свой DI-контейнер, вызывает создание нескольких одноэлементных объектов, и каждый класс имеет свой собственный DI-контейнер - я определенно хочу этого избежать.

Кажется, это DI через Builder эквивалентен анти-шаблону локатора сервисов? то, что мне нужно, но на него нет ответа.

Я определяю свой контейнер с помощью .NET DI:

public class ServiceProvider
{
        private Microsoft.Extensions.DependencyInjection.ServiceProvider serviceProvider;
        protected readonly IServiceCollection serviceCollection = new ServiceCollection();
        public ServiceProvider()
        {
            serviceCollection.AddScoped<IScopedService, ScopedService>();
            serviceCollection.AddSingleton<ISingletonService, SingletonService>();
        }
}

Теперь, когда ScopedService зависит от SingletonService, это делается с помощью правильного внедрения конструктора:

public ScopedService(ISingletonService singletonService)
{
    _singletonService = singletonService
}

Но классы потребления (конечного пользователя) выполняются через локатор службы, например.

private IScopedService _scopedService;
private IScopedService ScopedService
{
    get
    {
        return _scopedService ?? _scopedService = serviceProvider.GetService<IScopedService>();
    }
}

Все мои потребляющие классы наследуются от базового класса, который, я считаю, создает новый экземпляр ServiceProvider каждый раз, когда создается экземпляр класса.

    public abstract class ComponentBase
    {
        protected internal ServiceProvider serviceProvider = new ServiceProvider();
    }

Проблема: создается несколько объектов Singleton

            var singleton1 = consumingClass.serviceProvider.GetService<ISingletonService>();
            var singleton2 = consumingClass.serviceProvider.GetService<ISingletonService>();

Assert.AreEqual(singleton1, singleton2) // Test failure

Я ожидаю, что службы Singleton будут одним и тем же экземпляром. Должен ли я сделать ServiceProvider в базовом классе статическим? Это решило бы проблему, но также, похоже, еще больше склоняется к анти-паттерну локатора сервисов.

Стоит ли переходить на фабричный шаблон? Где я регистрирую все потребляющие классы в поставщике услуг, и только фабрика знает о контейнере DI? Вы бы тогда получили класс, вызвав геттера на фабрике?

В то время как IMO бывают случаи, когда что-то, напоминающее локатор службы, является меньшим из двух зол, прямое внедрение контейнера в класс определенно является анти-шаблоном, потому что зависимость становится неясной. Класс может запросить что-нибудь из этого контейнера. Он создает неопределенную, открытую зависимость. Вот статья (мой), который показывает пример, где что-то вроде локатора сервисов может быть полезным и как свести к минимуму его зло.

Scott Hannen 20.06.2019 19:51

Я бы написал, что каждый класс зависит от того, от чего он должен зависеть. Если вы это сделаете, почти всегда есть способ настроить ваш контейнер для предоставления этих зависимостей. Обычно это легко. Если это сложно, то это проблема, которую нужно решить. Ваши классы будут написаны правильно. Решайте проблему там, где она есть, а не позволяя ей просачиваться в ваши классы.

Scott Hannen 20.06.2019 19:57

Спасибо @scotthannen, это очень полезная статья.

John Curran 21.06.2019 05:21

Пожалуйста, не делайте больше работы для других людей, искажая свои посты. Размещая на Stack Overflow, вы предоставляете SO безотзывное право в соответствии с Лицензия CC BY-SA 3.0 распространять этот контент. В соответствии с политикой SO любой вандализм будет пресекаться. Если вы хотите узнать больше об удалении сообщения, прочитайте дополнительную информацию на странице Как работает удаление?.

iBug 21.06.2019 05:26
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
983
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я бы посоветовал не иметь свой собственный IServiceProvider в SDK, подобный тому, который был указан в связанной статье.

Вместо этого предоставьте точку расширения для вашего SDK, чтобы подключиться к DI потребителя.

public static MySDKServiceCollectionExtensions {
    public static IServiceCollection AddMySDK(this IServiceCollection services) {
        services.AddScoped<IScopedService, ScopedService>();
        services.AddSingleton<ISingletonService, SingletonService>();

        //...additional services.

        return services
    }
}

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

public abstract class ComponentBase {        
    protected ISingletonService singleton;

    public ComponentBase(ISingletonService singleton) {
        this.singleton = singleton;
    }
}

Наличие IServiceProvider в вашем SDK рассматривается как проблема реализации, которая не должна иметь значения для вашего SDK.

A DI Container should only be referenced from the Composition Root. All other modules should have no reference to the container.

Потребляющий проект/приложение (конечный пользователь) добавит ваш SDK в корень своей композиции.

//...

IServiceCollection serviceCollection = new ServiceCollection();

//...Add SDK
serviceCollection.AddMySDK();

//Add my services.
serviceCollection.AddScoped<IConsumer, Consumer>()

//...

и использовать его соответственно по мере необходимости

public class Consumer: IConsumer {
    protected internal IScopedService service;

    protected Consumer(IScopedService service) {
        this.service = service
    }

    private IScopedService ScopedService {
        get {
            return service;
        }
    }

    //...
}

Мне очень нравится этот ответ. Метод расширения для добавления моих сервисов в сервисы конечных пользователей очень удобен. Однако у меня есть вопрос: таким образом, мне нужно будет регистрировать мои конечные пользовательские/потребляющие классы в качестве служб в ServiceCollection? И получить с var component = services.GetService<IConsumer>();?

John Curran 20.06.2019 23:27

@JohnCurran да, если вы хотите, чтобы контейнер выполнял всю тяжелую работу по разрешению всего.

Nkosi 20.06.2019 23:28

@JohnCurran, но это не должно тебя беспокоить. Они зарегистрируют свои классы по желанию

Nkosi 20.06.2019 23:33

Другие вопросы по теме