Внедрение зависимостей в .NET MAUI. Введение

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

Понятие зависимости

Зависимость — это любой объект, от которого зависит другой объект.

Например, при создании приложения .NET MAUI вы решите, что было бы неплохо, чтобы программа сохраняла лог своей работы в файл. Это можно сделать, используя обычный класс C#, например, вот такой:

public class FileLogger
{
    const string FILE_NAME = "Log.txt";
   
    private readonly string _newLine = Environment.NewLine;
    public void WriteLine(string message)
    {
        File.AppendAllText(FILE_NAME, $"{message}{_newLine}");
    }
}

Теперь мы можем использовать объекты этого класса везде, где нам необходимо произвести запись в лог. Например, используем его при клике по кнопке в шаблонном приложении .NET MAUI. Изменим файл отдельного кода следующим образом:

public partial class MainPage : ContentPage
{
    int count = 0;

    private readonly FileLogger fileLogger;

    public MainPage()
    {
        InitializeComponent();
        fileLogger = new FileLogger();
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);

        //добавляем запись в лог
        fileLogger.WriteLine(CounterBtn.Text);
    }
}

Теперь рядом с exe-файлом приложения (если мы запускаем приложение в Windows) появится файл с именем Log.txt в который будет записываться текст кнопки. Тот момент, что наше приложение может запускаться на других платформах и там наш логгер в том виде, в котором он сейчас есть может и не заработать — пока опустим, но позднее мы с ним обязательно разберемся. Вернемся к логгеру. Сейчас наш логгер — это зависимость. От него зависит работа класса MainPage. Рассмотрим основные проблемы, с которыми мы можем столкнуться, если мы продолжим использовать нашу зависимость как есть.

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

Пойдем дальше. Следующая проблема — поддержка такого кода. Наш логгер может вызываться в десятках мест приложения и, в случае замены класса или расширения его возможностей, нам придется все эти места в коде найти и изменить. А если наш класс будет зависеть от других классов, а те — от третьих и т.д.? В итоге мы можем создать такой код, который потом никто не сможет поддерживать. И здесь нам поможет внедрение зависимостей.

Внедрение зависимостей в .NET MAUI

Давайте попробуем избавиться от прямой зависимости класса MainPage от FileLogger. Сделать это можно, используя какие-либо абстракции первая из которых — это интерфейс. Добавим в наше приложение следующий интерфейс:

public interface ILogger
{
    public void WriteLine(string message);
}

и реализуем его в классе FileLogger:

public class FileLogger: ILogger
{
    const string FILE_NAME = "Log.txt";

   
    private readonly string _newLine = Environment.NewLine;
    public void WriteLine(string message)
    {
        File.AppendAllText(FILE_NAME, $"{message}{_newLine}");
    }
}

Такая конструкция из интерфейса и класса, как минимум, позволяет нам переписать код класса MainPage следующим образом:

public partial class MainPage : ContentPage
{
    int count = 0;

    private readonly ILogger fileLogger; //используем интерфейс

    public MainPage()
    {
        InitializeComponent();
        fileLogger = new FileLogger();
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);

        //добавляем запись в лог
        fileLogger.WriteLine(CounterBtn.Text);
    }
}

Теперь за интерфейсом ILogger может скрываться любой класс его реализующий и классу MainPage не важно как будет организована работа логгера — важно то, что у него есть метод WriteLine(), который можно вызвать. Но мы все равно пока ещё создаем экземпляр класса FileLogger в MainPage и это потребует от нас, в случае каких-либо изменений, менять код страницы приложения. Остается вопрос: как сделать так, чтобы класс MainPage вообще не занимался созданием экземпляров логгера, а просто запрашивал их откуда-либо и использовал их методы? И именно здесь нам на помощь может прийти внедрение зависимостей.

Контейнеры внедрения зависимостей

Итак, наша задача сделать так, чтобы MainPage «забыл», что из себя представляет и как создается логгер. С первой частью мы справились — используем интерфейс. Сейчас будем решать и вторую часть проблемы. Итак, если класс не создает экземпляры необходимых объектов, то эту работу должен выполнять кто-то другой. Этот «кто-то другой» в терминах DI называется контейнер внедрения зависимостей.

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

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

Существует ряд реализаций различных контейнеров внедрения зависимостей, однако .NET предоставляет нам уже готовый механизм для внедрения зависимостей, включающий в себя в том числе и стандартный контейнер внедрения зависимостей.

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

Основные типы данных по работе с зависимостями в .NET сосредоточены в пространстве имен Microsoft.Extensions.DependencyInjection. В этом пространстве имен содержатся два интерфейса и класс, выступающие основой для DI в .NET — это:

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

Смысл нашей работы заключается в следующем: добавить сервис в коллекцию IServiceCollection перед запуском приложения. Когда приложение запущено, мы используем методы IServiceProvider для получения сервиса из коллекции (контейнера DI). При этом, как и говорилось выше, контейнер DI самостоятельно разрешит все зависимости и вернет нам готовый к использованию объект.

В зависимости от типа проекта .NET, необходимые классы для работы с DI могут уже присутствовать в проекте. Так, тип проекта .NET MAUI уже имеет в своем составе готовую реализацию IServiceCollection и нам нет необходимости создавать свой контейнер. Контейнер внедрения зависимостей в проекте .NET MAUI содержится в свойстве Services класса MauiAppBuilder

Регистрация сервиса в .NET MAUI

Ниже будет продемонстрирован лишь один из способов регистрации и использования сервиса в приложении .NET MAUI. В дальнейшем мы рассмотрим и другие способы. Итак, вернемся к нашему приложению, откроем файл MauiProgram.cs и внесем в него следующее изменение:

using Microsoft.Extensions.Logging;

namespace MauiApp23;

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

        //зарегистрировали свой сервис в приложении
        builder.Services.AddSingleton<ILogger, FileLogger>();

#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

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

builder.Services.AddSingleton<ILogger, FileLogger>();

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

public partial class MainPage : ContentPage
{
    int count = 0;

    private readonly ILogger _logger; //сервис

    public MainPage(ILogger logger) //запрашиваем сервис через конструктор
    {
        InitializeComponent();
        _logger = logger;
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);

        //добавляем запись в лог
        _logger.WriteLine(CounterBtn.Text);
    }
}

Обратите внимание — теперь в классе MainPage нет ни одного упоминания о конкретной реализации ILogger. Более того, нигде в классе не создается экземпляр логгера. То есть теперь мы можем спрятать за ILogger любую его реализацию, не меняя при этом код класса MainPage. В нашем случае мы использовали внедрение зависимости через конструктор. Это часто используемый, но не единственный способ внедрения зависимостей в .NET. Теперь приложение будет работать следующим образом:

  1. В контейнере зависимостей регистрируется сервис ILogger
  2. При создании экземпляра MainPage контейнер видит, что этот класс требует зависимость ILogger
  3. Сервис ILogger ищется в контейнере. Находится как сервис, так и его реализация
  4. Проверяется необходимы ли для FileLogger какие-либо зависимости. Если требуются — то они также ищутся в контейнере.
  5. Пункт 4 повторяется до тех пор пока не будут разрешены все зависимости или требуемый сервис не будет обнаружен — тогда появится ошибка и процесс прервется
  6. Контейнер создает по порядку все необходимые зависимости и объект FileLogger
  7. Готовый к использованию объект типа FileLogger возвращается в MainPage
  8. Мы получаем в распоряжению объект типа MainPage с готовым к использованию сервисом.

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

Итого

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

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