Сетевой дневник одного программиста

Персональный блог Константина Огородова

WaterMeterAutomation рефакторинг

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

  • Saures API и TomRC API зависят от предметной области и ядра приложения, что не есть хорошо, поскольку с концептуальной точки зрения обе библиотеки предоставляют доступ к соответсnвующему API и на этом их ответственность заканчивается. Этим библиотекам не важно кто и как будет их использовать, важно что логика вызывающих приложений не должна просачиваться внутрь.
  • Код инъекций зависимостей «размазан» ровным слоем по всем библиотекам. Да, зависимость от интерфейса, а не от конкретной реализации, но это всё равно не хорошо.

Произведу рефакторинг компонентов таким образом, чтобы в конечном итоге получилось вот так:

Начну с библиотек Saures API и TomRC API — уберу ссылку на проект Core и NuGet пакет Microsoft.Extensions.DependencyInjection.Abstractions, а также выделю классы предметной области (Domain). В результате проекты выглядят вот так:

Минимум зависимостей и единственная ответственность — то что надо! Однако нужно как-то обеспечить связь Saures API и TomRC API с интерфейсами объявленными в ядре приложения. Для этого создам проект Infrastructure. Задача этого слоя — быть мостом между ядром приложения и внешними зависимостями. Тут не должно быть сложных алгоритмов или реализаций некоторых сервисов, а лишь посреднические услуги:

Наконец последний проект WaterMeterAutomation переименованный в EntryPoint. Задачи у точки входа простые:

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

В процессе реализации решено было отказаться от использования универсального узла .NET, поскольку он больше подходит для длительно работающих приложений, типа win-сервисов, а это не тот случай. Да и интеграция с расширениями от MS тут тоже не требуется. Так что я взял в качестве основы реализацию метода Main со страницы NLog — Getting started with .NET Core 2 Console application. В результате получилось вот так:

internal class Program
{
    private static async Task Main()
    {
        var logger = LogManager.GetCurrentClassLogger();

        try
        {
            var configuration = CreateConfiguration();
            ConfigureMailTarget(configuration);

            var container = new StashboxContainer()
                .RegisterInstance(configuration)
                .ComposeAssembly(Assembly.GetExecutingAssembly());

#if DEBUG
            container.Validate();
#endif

            var actor = container.Resolve<MainInteractionService>();
            await actor.SendNewWaterMetersValuesFromSourceToConsumer();
        }
        catch (Exception ex)
        {
            logger.Error(ex, "Stopped program because of exception");
            throw;
        }
        finally
        {
            // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
            LogManager.Shutdown();
        }
    }

    private static IConfiguration CreateConfiguration()
    {
        return new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .AddEnvironmentVariables("wma_")
            .Build();
    }

    private static void ConfigureMailTarget(IConfiguration configuration)
    {
        var loggerCredentials = configuration.GetSection("Smtp").Get<LoggerCredentials>();
        var config = LogManager.Configuration;
        var mailTarget = config.FindTargetByName<MailTarget>("mail");
        mailTarget.SmtpAuthentication = SmtpAuthenticationMode.Basic;
        mailTarget.SmtpUserName = loggerCredentials.Login;
        mailTarget.SmtpPassword = loggerCredentials.Password;
    }

    private record LoggerCredentials
    {
        public string Login { get; init; }
        public string Password { get; init; }
    }
}

Вторым нововведением стало использование контейнера внедрения зависимостей StashBox, По сравнению с решением от MS этот контейнер обладает куда большим набором возможностей, например я воспользовался регистрацией зависимостей в классах с реализацией интерфейса ICompositionRoot:

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

internal sealed class CompositionRootConfiguration : ICompositionRoot
{
    private readonly IConfiguration _configuration;

    public CompositionRootConfiguration(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void Compose(IStashboxContainer container)
    {
        container.Register<SauresConfiguration>(o =>
            o.WithFactory(() => _configuration.GetSection("SauresApi").Get<SauresConfiguration>())
                .WithLifetime(Lifetimes.Singleton));

        container.Register<TomRcConfiguration>(o =>
            o.WithFactory(() => _configuration.GetSection("TomRcApi").Get<TomRcConfiguration>())
                .WithLifetime(Lifetimes.Singleton));

        container.Register<MainInteractionServiceConfiguration>(o =>
            o.WithFactory(() => _configuration.GetSection("MainInteractionService").Get<MainInteractionServiceConfiguration>())
                .WithLifetime(Lifetimes.Singleton));
    }
}

internal sealed class CompositionRootCore : ICompositionRoot
{
    public void Compose(IStashboxContainer container)
    {
        container.Register<MainInteractionService>();
    }
}

internal class CompositionRootLoggerAndNotification : ICompositionRoot
{
    public void Compose(IStashboxContainer container)
    {
        container.Register<ILogger>(o => o.WithFactory(() => LogManager.GetLogger("Global")));

        container.RegisterSingleton<INotificationService, NotificationService>();
    }
}

internal sealed class CompositionRootSauresApi : ICompositionRoot
{
    private readonly SauresConfiguration _sauresConfiguration;

    public CompositionRootSauresApi(SauresConfiguration sauresConfiguration)
    {
        _sauresConfiguration = sauresConfiguration;
    }

    public void Compose(IStashboxContainer container)
    {
        container.Register<Api>(o =>
            o.WithFactory(() => new Api(_sauresConfiguration.Host))
                .WithLifetime(Lifetimes.Singleton));

        container.Register<IWaterMetersDataSourceService, WaterMetersDataSourceService>(o =>
            o.WithFactory<Api>(api => new WaterMetersDataSourceService(api, _sauresConfiguration.Login, _sauresConfiguration.Password))
                .WithLifetime(Lifetimes.Singleton));
    }
}

internal class CompositionRootTomRcApi : ICompositionRoot
{
    private readonly TomRcConfiguration _tomRcConfiguration;

    public CompositionRootTomRcApi(TomRcConfiguration tomRcConfiguration)
    {
        _tomRcConfiguration = tomRcConfiguration;
    }

    public void Compose(IStashboxContainer container)
    {
        container.Register<Api>(o =>
            o.WithFactory(() => new Api(_tomRcConfiguration.Host))
                .WithLifetime(Lifetimes.Singleton));

        container.RegisterSingleton<DocumentParser>();

        container.Register<IWaterMetersDataConsumerService, WaterMetersDataConsumerService>(o =>
            o.WithFactory<Api, DocumentParser>((api, parser) => new WaterMetersDataConsumerService(api, _tomRcConfiguration.Login, _tomRcConfiguration.Password, parser))
                .WithLifetime(Lifetimes.Singleton));
    }
}

И немного проще стали классы конфигурации для SauresAPI и TomRcAPI:

internal abstract record ApiConfiguration
{
    public string Host { get; init; }
    public string Login { get; init; }
    public string Password { get; init; }
}

internal record SauresConfiguration : ApiConfiguration;

internal record TomRcConfiguration : ApiConfiguration;

На этом рефакторинг решения завершён. Однако напоследок хочу поделиться особенностями отправки эл. писем из приложения. В настоящее время большинство почтовых служб вводят дополнительные проверки при авторизации и зачастую закрывают авторизацию только по имени пользователя и паролю. Yandex предлагает элегантное решение: Пароли приложений. Суть проста: для некоторых служб создаётся отдельная связка имя пользователя \ пароль для простой авторизации. В это же время для основного аккаунта дополнительные средства безопасности и в том числе двухфакторная авторизация продолжают работать. Удобно? — безусловно.

WaterMeterAutomation рефакторинг

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Пролистать наверх