Содержание
Внедрение зависимостей в .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. Теперь приложение будет работать следующим образом:
- В контейнере зависимостей регистрируется сервис
ILogger
- При создании экземпляра
MainPage
контейнер видит, что этот класс требует зависимостьILogger
- Сервис
ILogger
ищется в контейнере. Находится как сервис, так и его реализация - Проверяется необходимы ли для
FileLogger
какие-либо зависимости. Если требуются — то они также ищутся в контейнере. - Пункт 4 повторяется до тех пор пока не будут разрешены все зависимости или требуемый сервис не будет обнаружен — тогда появится ошибка и процесс прервется
- Контейнер создает по порядку все необходимые зависимости и объект
FileLogger
- Готовый к использованию объект типа
FileLogger
возвращается вMainPage
- Мы получаем в распоряжению объект типа
MainPage
с готовым к использованию сервисом.
Можете запустить приложение и убедиться, что наш логгер всё так же работает и записывает текст кнопки в файл. В следующей части мы продолжим работу с нашим сервисом.
Итого
Внедрение зависимостей — это возможность самой платформы .NET, благодаря которой код приложения становится слабосвязанным. В .NET MAUI уже имеется готовая к использованию реализация контейнера зависимостей, которую мы можем использовать в своем приложении для регистрации сервисов.