Применение принципа инверсии управления на архитектурном уровне в микросервисах.

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

Тесная связь между пакетами

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

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

Вот упрощенный код пакета чтения кеша, который предоставляет простой интерфейс для извлечения данных из кеша Redis:

//Interface for package clients
public interface ICacheReader
{
    UserData GetUserData(int userId);
}

//Internal implementation of the interface
internal class CacheReader : ICacheReader
{
    //Reading data from Redis cache
}

/* 
 An extension method that clients must call after 
 installing the NuGet package to add an interface and 
 implementation to the IoC container.
*/
public static void AddCacheReaderServices(this IServiceCollection services)
{
    services.AddScoped<ICacheReader, CacheReader>();
}

Вот как пакет генератора отчетов использует интерфейс ICacheReader:

//Interface for package clients
public interface IReportGenerator
{
    Report GetReport(int userId);
}

//Internal implementation of the interface
internal class ReportGenerator : IReportGenerator
{
    private ICacheReader _cacheReader;
    
    public ReportGenerator(ICacheReader cacheReader) 
        => _cacheReader = cacheReader;

    public Report GetReport(int userId)
    {
        var userData = _cacheReader.GetUserData(userId);
        
        //Generating a report based on user data
    }
}

/* 
 An extension method that clients must call after 
 installing the NuGet package to add an interface and 
 implementation to the IoC container.
*/
public static void AddReportGeneratorServices(this IServiceCollection services)
{
    services.AddScoped<IReportGenerator, ReportGenerator>();
    services.AddCacheReaderServices();
}

Каждая микрослужба, которой необходимо использовать логику отчетов, может просто установить пакет NuGet генератора отчетов и вызвать метод AddReportGeneratorServices из класса Startup. После этих шагов микрослужба может просто внедрить интерфейс IReportGenerator в любое из своих приложений или доменных служб и использовать логику создания отчетов.

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

Реализация принципа инверсии управления

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

Вот как выглядит код пакета генератора отчетов после рефакторинга:

//Interface for providing user data (NEW)
public interface IUserDataProvider
{
    UserData GetUserData(int userId);
}

//Interface for package clients (OLD)
public interface IReportGenerator 
{
    Report GetReport(int userId);
}

//Internal implementation of the interface (OLD)
internal class ReportGenerator : IReportGenerator
{
    //IUserDataProvider interface instead of ICacheReader
    private IUserDataProvider _userDataProvider; 
    
    public ReportGenerator(IUserDataProvider userDataProvider) 
        => _userDataProvider = userDataProvider;

    public Report GetReport(int userId)
    {
        var userData = _userDataProvider.GetUserData(userId);

        //Generating a report based on user data
    }
}

/* 
 An extension method that clients must call after 
 installing the NuGet package to add an interface and 
 implementation to the IoC container.
*/
public static void AddReportGeneratorServices(this IServiceCollection services)
{
    services.AddScoped<IReportGenerator, ReportGenerator>();
}

Теперь пакет генератора отчетов определяет только интерфейс IUserDataProvider и получает через него необходимые данные. Пакет не использует интерфейс ICacheReader и больше не требует ссылки на пакет чтения кэша.

Каждая микрослужба, которой необходимо использовать пакет Reporter, должна выполнить следующие действия:

  1. Установите пакет NuGet генератора отчетов в проект.
  2. Вызовите метод AddReportGeneratorServices из класса Startup.
  3. Реализовать IUserDataProvider интерфейс.
  4. Зарегистрируйте реализацию интерфейса IUserDataProvider в контейнере IoC.

Вот как выглядит реализация IUserDataProvider, которую должны реализовать микросервисы заказа и доставки:

//Implementing IUserDataProvider interface
public class UserDataProvider : IUserDataProvider
{
   private ICacheReader _cacheReader;
    
   public UserDataProvider(ICacheReader cacheReader) 
        => _cacheReader = cacheReader;

   public UserData GetUserData(int userId)
   {
      var userData = _cacheReader.GetData(userId);
      return userData;
   }
}

//In the Startup class
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IUserDataProvider, UserDataProvider>();
    services.AddReportGeneratorServices();
}

А вот новая схема архитектуры:

Таким образом, пакеты больше не являются тесно связанными. Теперь микросервисы выступают посредниками между генератором отчетов и пакетами чтения кэша.
Однако тесная связь была устранена за счет введения избыточности в систему: микросервисы имеют идентичную реализацию интерфейса IUserDataProvider. Это хорошо или плохо? Давайте обсудим в следующей главе.

Резюме: Жесткая связь против принципа IoC

Теперь давайте проанализируем оба подхода на предмет их основных плюсов и минусов.

Подход с жесткой связью

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

  1. Внесите изменения в пакет чтения кэша и выпустите его новую версию.
  2. Обновите версию пакета чтения кэша в пакете генератора отчетов.
  3. Обновите версию генератора отчетов в каждом клиентском микросервисе.

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

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

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

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

Инверсия подхода к управлению

С другой стороны, управление версией пакета в подходе Inversion of Control так же просто, как обновление версии пакета NuGet для его прямых клиентов. Однако, поскольку реализация IUserDataProvider может быть одинаковой для нескольких клиентских микрослужб, могут возникнуть некоторые проблемы с ремонтопригодностью.

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

Спасибо за прочтение. Если вам понравилось то, что вы прочитали, ознакомьтесь с этой историей ниже:



Также подумайте о том, чтобы стать участником Medium.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу