Определите реализацию во время выполнения, используя внедрение зависимостей - PullRequest
1 голос
/ 03 августа 2020

У меня есть сценарий, в котором мне нужно получить реализацию, неизвестную до времени выполнения. Мой подход до сих пор заключается в создании класса обслуживания (чтобы абстрагироваться от logi c классов, которые их используют). Клиенты и сервис зарегистрированы в DI. Вызывающие классы запрашивают только Service.

Ниже приведены два разных подхода (упрощенные):

public class Service
{
    private readonly IClient client1;
    private readonly IClient client2;

    public Service(Client1 client1, Client2 client2)
    {
        this.client1 = client1;
        this.client2 = client2;
    }

    public Data GetData(string client, string something)
    {
        if (client == "client1")
            return this.client1.GetData(something);

        return this.client2.GetData(something);
    }
}

И:

public class Service
{
    private readonly IServiceProvider serviceProvider;

    public Service(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public Data GetData(string client, string something)
    {
        if (client == "client1")
            return this.serviceProvider.GetRequiredService<Client1>().GetData(something);

        return this.serviceProvider.GetRequiredService<Client2>().GetData(something);
    }
}

И затем это используется путем вызова: service.GetData("client1", ...)

Подходит ли для этого какая-либо из этих альтернатив? Один предпочтительнее другого?

Ответы [ 3 ]

1 голос
/ 03 августа 2020

Во всех случаях вариант 2 - плохой.

  • Шаблон локатора службы широко считается антипаттерном. Это может решить имеющуюся проблему, но в дальнейшем создает множество других проблем.
  • Вы позволяете своему потребителю решать, какой клиент использовать, что фактически сводит на нет идею позволить службе определять свою собственную зависимость с помощью конструктор.
  • Magi c строки нежелательны. Если ваш потребитель все равно выбирает клиента, то для него нет смысла использовать какую-то строку magi c для выбора правильного клиента. Если позволить им передать самого клиента, значительно меньше вероятность ошибок, но тогда Service не полагается на структуру DI для клиентского объекта, что может нарушить цель вашей настройки.

ЕСЛИ выбор клиента выполняется динамически каждый раз при вызове GetData(), тогда вариант 1 является допустимым подходом.

Хотя я бы предложил использовать более описательные имена, чем «client1 "и" client2 ", где это возможно.

IF выбор клиента - Dynami c, но остается фиксированным после запуска приложения, что означает, что все вызовы GetData() в течение одного времени выполнения будут обрабатываться одним и тем же клиентом, тогда это лучше чтобы выбрать этого клиента при регистрации ваших зависимостей:

// Startup.cs

if( /* selection criteria */)
{
    services.AddScoped<IClient, Client1>();
}
else
{
    services.AddScoped<IClient, Client2>();
}

// Service.cs

public class Service
{
    private readonly IClient client;

    public Service(IClient client)
    {
        this.client = client;
    }

    public Data GetData(string something)
    {
        return this.client.GetData(something);
    }
}

Хотя я бы предложил использовать более описательные имена, чем «client1» и «client2», где это возможно.

Обратите внимание, что ваш критерий выбора может быть какими бы они ни были, например, значение конфигурации приложения, значение базы данных, значение среды, тип компиляции (отладка / выпуск) ... Мир - это ваша устрица.

Также оцените, не лучше ли вам реализовать дополнительную абстракцию, которая может решить, на какого клиента перенаправлять (например, ClientFactory или ClientRouter). Это не всегда необходимо, но если ваши требования нетривиальны, абстракция может помочь упростить задачу.

0 голосов
/ 03 августа 2020

вы можете внедрить ClientX модифицированный интерфейс для большей гибкости, как показано в коде ниже:

public class Service<T> where T : IClient
{
    private readonly IServiceProvider serviceProvider;

    public Service(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public Data GetData<T>(string something)
    {
        return this.serviceProvider.GetRequiredService<T>().GetData(something);
    }
}
0 голосов
/ 03 августа 2020

У обоих есть недостатки:

  • Версия # 1 всегда получает два экземпляра клиента. Если создание экземпляра - тяжелый процесс, это нехорошо.

  • Версия № 2 скрывает свои зависимости. Это хорошо известный анти-шаблон.

Идеальным решением было бы ввести IClient1Factory и IClient2Factory и при необходимости вызвать их фабричные методы создания. Это означает, что вы по-прежнему создаете только один экземпляр, а не оба, но вы не скрываете зависимости.

Как всегда, идеального решения нет, теперь вам, очевидно, нужно написать и поддерживать эти две фабрики. Убедитесь, что оно того стоит. Если создание экземпляра класса Client1 / Client2 - это просто new, при котором в конструкторе ничего не происходит, вы можете выбрать более простой подход Версии №1. Если он простой и работает, не оборачивайте его слишком большим количеством слоев узора. Используйте их, только если они вам нужны.

...