Внедрение адаптеров в клиент

Внедрение адаптеров в клиент
Внедрение адаптеров в клиент - south_ink @ Unsplash

Итак, я создаю клиент для третьего API и хочу иметь возможность Get() и GetMany() для каждого типа, который может предоставить этот контроллер.

Поэтому я создал этот адаптер, который предоставляет для них методы общего типа.

public interface IClientAdapter<T>
{
    public Task<T?> Get(string predicates = "");
    public Task<List<T>?> GetMany(string predicates = "");
}

И это основной интерфейс реального клиента.

public interface IBasicSpaceDataClient
{
    public IClientAdapter<Announcement> Announcements { get; }
    public IClientAdapter<BoxScore> BoxScores { get; }
    public IClientAdapter<Conjunction> Conjunctions { get; }
    public IClientAdapter<Decay> Decays { get; }
    public IClientAdapter<GeneralPerturbation> GeneralPerturbations { get; }
    public IClientAdapter<GeneralPerturbation> GeneralPerturbationHistory { get; }
    public IClientAdapter<LaunchSite> LaunchSites { get; } 
    public IClientAdapter<SatCatChange> SatCatChanges { get; }
    public IClientAdapter<SatCatEntry> SatCatEntries { get; }
    public IClientAdapter<SatCatEntry> SatCatDebuts { get; }
    public IClientAdapter<TrackingAndImpactPrediction> TrackingAndImpactPredictions { get; }
}

Это означает, что вы можете сделать:

List<Announcement> announcements = await myClient.Announcements.GetMany();

Что я думаю, довольно приятно.

С точки зрения фактической реализации адаптер выглядит так:

internal class HttpClientAdapter<T>: IClientAdapter<T>
{
    private readonly HttpClient _client;
    private readonly string _endpoint;

    public HttpClientAdapter(HttpClient client, string endpoint)
    {
        _client = client;
        _endpoint = endpoint;
    }

    public async Task<T?> Get(string predicates)
    {
        T[]? arrayOfOne = await _client.GetFromJsonAsync<T[]>($"{_endpoint}/limit/1/{predicates}");
        return arrayOfOne is null ? default : arrayOfOne.Single();
    }
    
    public async Task<List<T>?> GetMany(string predicates) => await _client.GetFromJsonAsync<List<T>>($"{_endpoint}/{predicates}");
}

И все это работает достаточно хорошо.

То, в чем я не уверен, закрадывается в конструктор конкретного клиента BasicSpaceData.

public BasicSpaceDataClient(HttpClient client) {
    Announcements = new HttpClientAdapter<Announcement>(client, $"{BasicSpaceDataEndpoint}announcement");
    BoxScores = new HttpClientAdapter<BoxScore>(client, $"{BasicSpaceDataEndpoint}boxscore");
    Conjunctions = new HttpClientAdapter<Conjunction>(client, $"{BasicSpaceDataEndpoint}cdm_public");
    Decays = new HttpClientAdapter<Decay>(client, $"{BasicSpaceDataEndpoint}decay");
    GeneralPerturbations = new HttpClientAdapter<GeneralPerturbation>(client, $"{BasicSpaceDataEndpoint}gp");
    GeneralPerturbationHistory = new HttpClientAdapter<GeneralPerturbation>(client, $"{BasicSpaceDataEndpoint}gp_history");
    LaunchSites = new HttpClientAdapter<LaunchSite>(client, $"{BasicSpaceDataEndpoint}launch_site");
    SatCatChanges = new HttpClientAdapter<SatCatChange>(client, $"{BasicSpaceDataEndpoint}satcat_change");
    SatCatEntries = new HttpClientAdapter<SatCatEntry>(client, $"{BasicSpaceDataEndpoint}satcat");
    SatCatDebuts = new HttpClientAdapter<SatCatEntry>(client, $"{BasicSpaceDataEndpoint}satcat_debut");
    TrackingAndImpactPredictions = new HttpClientAdapter<TrackingAndImpactPrediction>(client, $"{BasicSpaceDataEndpoint}tip");
}

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

Это вторая часть, в которой я не уверен. С одной стороны, это очень простые объекты, и вся функциональность реализуется за счет внедрения HttpClient. С другой стороны, такого рода вещи всегда заставляют меня задуматься о том, не лучше ли им сделать инъекцию. Обычно я выбираю последнее, но в этом случае мне интересно, есть ли в этом какая-то польза, и все, что это может сделать, это увеличить сложность моей настройки DI.

Что думают люди по этому поводу?

Мои мысли таковы.

  1. Как только одна из конечных точек будет отличаться каким-либо образом, вы потеряете свои сбережения в плане LoC

  2. Если дать класс клиента для некоторого API, я автоматически предположу, что все методы являются конечными точками того же API. Однако если мне придется инжектировать каждый клиент ресурса... Возникает вопрос, указывают ли они все на разные API. Если я вызываю AddAddress("Glasgow"), могу ли я позже вызвать GetCity("Glasgow")?

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

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

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


LetsCodeIt, 19 января 2023 г., 03:18