Внедрение зависимостей в Blazor Hybrid

Внедрение зависимостей (Dependency Injection, DI) — это набор принципов и паттернов проектирования программных продуктов, позволяющий разрабатывать слабосвязанный код. Dependency Injection в ASP.NET Core и Blazor, в частности — это основной способ расширения возможностей приложения. Вместе с тем, так как приложения Blazor Hybrid направлены на разработку кроссплатформенных приложений, то здесь использование DI имеет свои особенности, которые мы и рассмотрим в этой части. Но начнем всё же с наиболее общих моментов использования DI в приложении Blazor.

Зависимости и сервисы

Пример без зависимостей

Зависимость — это любой объект, от которого зависит другой объект. Например, в процессе разработки приложения вы решите, что было бы неплохо записывать в файл лог работы какого-либо компонента приложения, например, счётчика (Counter.razor). Создадим новый класс с именем FileLogger и разместим его в папке Services приложения Blazor Hybrid:

namespace BlazorDI.Services
{
    public class FileLogger
    {
        private const string FileName = "Log.log";
        private readonly string _newLine = Environment.NewLine;

        public void WriteLine(string message)
        {
            File.AppendAllText(FileName, $"{message}{_newLine}");
        }
    }
}

Теперь используем этот класс в компоненте Counter следующим образом:

@page "/counter"

@using BlazorDI.Services

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private FileLogger logger = new();

    private void IncrementCount()
    {
        currentCount++;
        logger.WriteLine("Значение счётчика увеличено на 1");
    }
}

Здесь с помощью директивы Razor @using мы подключили новое пространство имен в котором содержится наш новый класс, а в коде C# создали объект типа и вызвали метод:

private FileLogger logger = new(); 

private void IncrementCount() 
{ 
     currentCount++; 
     logger.WriteLine("Значение счётчика увеличено на 1"); 
}

Теперь можно запустить приложение и убедиться, что файл действительно создается и в него пишется строка «Значение счётчика увеличено на 1», каждый раз как только мы кликаем по кнопке счётчика. Теперь класс компонента Razor зависит от класса FileLogger. То есть FileLogger — это зависимость. С течением времени мы можем использовать этот класс во многих местах нашего приложения, развивать его, добавляя новые методы, свойства и так далее. Приложение будет работать и даже, возможно, хорошо поддерживаться до тех пор, пока вы не решите кардинально изменить способ ведения лога приложения. Например, вы решите, что неплохо бы писать лог не в файл, а вообще отправлять по сети или вести лог в базе данных SQLite. Вариантов масса, но все они приводят нас сейчас к тому, что придется пересматривать весь код приложения в поисках использования FileLogger и менять его на новый класс, а, если учесть, что FileLogger, в свою очередь, может к этому времени зависеть от других классов, то поддержка такого кода становится весьма нетривиальной задачей. И чтобы решить эту задачу с минимальными потерями, мы и можем задействовать внедрение зависимостей.

Использование абстракций

Чтобы можно было без проблем заменять одну зависимость на другую в приложении используются абстракции, например, интерфейсы. Перепишем код нашего приложения следующим образом. Во-первых, создадим в папке Services интерфейс IBlazorLogger:

namespace BlazorDI.Services
{
    public interface IBlazorLogger
    {
        public void WriteLine(string message);
    }
}

Этот интерфейс пока содержит всего один метод, но, в дальнейшем мы можем добавить в него и другие методы работы с логом приложения. В терминах DI наш интерфейс называется служба или сервис.

Во-вторых, реализуем этот интерфейс в классе FileLogger:

public class FileLogger: IBlazorLogger
{
    private const string FileName = "Log.log";
    private readonly string _newLine = Environment.NewLine;

    public void WriteLine(string message)
    {
        File.AppendAllText(FileName, $"{message}{_newLine}");
    }
}

Реализуя интерфейс IBlazorLogger в различных классах мы можем создавать различные реализации нашего сервиса, например:

public class DbLogger : IBlazorLogger
{
    public void WriteLine(string message)
    {
        //тут реализация записи лога в БД
    }
}

public class HttpLogger : IBlazorLogger
{
    public void WriteLine(string message)
    {
        //тут реализация отправки лога по сети
    }
}

Теперь нам необходимо каким-либо образом указать компоненту Counter, что мы будем использовать реализацию IBlazorLogger. Самый простой вариант — сделать вот так:

private IBlazorLogger logger = new FileLogger();

private void IncrementCount()
{
    currentCount++;
    logger.WriteLine("Значение счётчика увеличено на 1");
}

Но так мы теряем смысл внедрения зависимостей — мы всё также указываем, что за интерфейсом скрывается FileLogger. Воспользуемся уже имеющимися возможностями .NET.

Пространство имен Microsoft.Extensions.DependencyInjection

Основные типы данных по работе с зависимостями в .NET сосредоточены в пространстве имен Microsoft.Extensions.DependencyInjection. Здесь содержатся три основных интерфейса с которыми мы будем дальше работать:

  • IServiceCollection — определяет контракт для коллекции дескрипторов сервисов.
  • IServiceProvider — определяет механизм получения объекта сервиса.
  • ServiceDescriptor — описывает сервис.

Смысл нашей работы заключается в следующем. Мы добавляем сервис в коллекцию IServiceCollection перед запуском приложения. Когда приложение запущено, мы используем IServiceProvider для получения сервиса из коллекции (контейнера DI). При этом, контейнер DI самостоятельно определяет все связи зависимостей, создает и возвращает нам уже готовый к использованию экземпляр сервиса. В зависимости от типа проекта .NET, необходимые классы для работы с DI могут уже присутствовать в проекте. Например, в приложении Blazor Hybrid у нас уже имеется готовый к использованию контейнер DI. Посмотрим на код файла MauiProgram.cs:

После того, как с использованием метода CreateBuilder() класса MauiApp создается экземпляр MauiAppBuilder мы получаем в свое распоряжение объект, реализующий IServiceCollection и можем, используя его, зарегистрировать свой сервис в приложении. Ниже мы рассмотрим один из вариантов регистрации сервиса, а другие варианты — рассмотрим в следующей части.

Регистрация и использование собственного сервиса в приложении

Итак, вернемся к нашему приложению и зарегистрируем сервис FileLogger.  Допишем код файла MauiProgram.cs следующим образом:

using BlazorDI.Services;

using Microsoft.Extensions.Logging;

namespace BlazorDI
{
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                });

            builder.Services.AddSingleton<IBlazorLogger, FileLogger>();//регистрируем свой сервис
            builder.Services.AddMauiBlazorWebView();
#if DEBUG
            builder.Services.AddBlazorWebViewDeveloperTools();
    		builder.Logging.AddDebug();
#endif

           return builder.Build();
           
        }
    }
}

Здесь мы, используя метод расширения AddSingleton() добавили в контейнер DI свой сервис IBlazorLogger, указав также и его реализацию — FileLogger. Теперь при запросе сервиса мы будем получать в свое распоряжение экземпляр именно этого класса.

Осталось запросить сервис и воспользоваться им. Опять же, ниже показан один из вариантов того, как мы можем это сделать. Откроем файл Counter.razor и перепишем его следующим образом:

@page "/counter"

@using BlazorDI.Services

@*запрашиваем сервис*@
@inject IBlazorLogger Logger 

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

   // private IBlazorLogger logger = new FileLogger();

    private void IncrementCount()
    {
        currentCount++;
        Logger.WriteLine("Значение счётчика увеличено на 1"); //используем сервис
    }
}

Здесь мы используем директиву @inject для запроса сервиса

@inject IBlazorLogger Logger

IBlazorLogger — сам сервис, а Logger свойство, которое будет содержать готовый к использованию экземпляр сервиса. Обратите внимание, что мы больше нигде (кроме файла MauiProgram.cs) не указываем конкретный тип, реализующий сервис — нам не важно, что скрывается за интерфейсом — FileLogger или другой класс. Главное, что мы можем воспользоваться методом WriteLine() о котором мы знаем.  Более того, теперь все сервисы регистрируются в одном месте приложения — в файле MauiProgram.cs и мы можем с минимальными затратами ресурсов заменить одну реализацию сервиса на другую, не меняя при этом код всего приложения. Теперь можно запустить приложение и убедиться, что оно работает как и ранее.

Итого

В этой части мы познакомились с механизмом внедрения зависимостей в приложениях Blazor Hybrid, а также зарегистрировали свой собственный сервис и воспользовались им в компоненте Razor. Далее мы рассмотрим другие возможности Dependency Injection в Blazor Hybrid.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии