Dependency Injection

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

Последнее обновление: 15.11.2022

Dependency injection (DI) или внедрение зависимостей представляет механизм, который позволяет сделать компоненты программы слабосвязанными, а всю программу в целом более гибкой, более адаптируемой и расширяемой.

В центре подобного механизма находится понятие зависимость - некоторая сущность, от которой зависит другая сущность. Например:

var logger = new Logger();
logger.Log("Hello METANIT.COM");

class SimpleLogService
{
    public void Write(string message) => Console.WriteLine(message);
}
class Logger
{
    SimpleLogService logService = new SimpleLogService();
    public void Log(string message) =>logService.Write($"{DateTime.Now}  {message}");
}

Здесь имеется класс Logger - условный логгер, который логгирует некоторое сообщение с помощью метода Log(). При логгировании Logger добавляет к логгируемому сообщению дату. Для логгирования Logger использует дополнительный класс SimpleLogService, который непосредственно управляет, как и куда будет логгироваться сообщение. В данном случае с помощью метода Write он просто выводит сообщение на консоль. И при выполнении программы, как и ожидается, мы увидим на консоли сообщение, предваряемое датой:

15.11.2022 11:37:24  Hello METANIT.COM

Данная программа прекрасно работает. Тем не менее в дальнейшем мы можем столкнуться с рядом проблем. Прежде всего, класс Logger жестко привязан к классу SimpleLogService. И если мы захотим вместо SimpleLogService использовать другой тип логгера, например, логгировать в файл, а не на консоль, то нам придется менять класс Logger. Один класс не составит труда поменять. Но если у нас в проекте много классов, которые используют SimpleLogService и с его помощью длоггируют сообщения на консоль. И вдруг понадобилось, чтобы все они стали логгировать сообщения файл. Поменять во всех класс Logger на другой будет труднее. Кроме того, класс SimpleLogService может иметь свои зависимости, которые тоже может потребоваться поменять. В итоге такими системами сложнее управлять и сложнее тестировать.

Чтобы отвязать объект Logger от класса SimpleLogService, мы можем создать абстракцию, которая будет представлять сервис логгера, и передавать его извне в объект Logger:

var logger = new Logger(new SimpleLogService());
logger.Log("Hello METANIT.COM");

logger = new Logger(new GreenLogService());
logger.Log("Hello METANIT.COM");

interface ILogService
{
    void Write(string message);
}
// простой вывод на консоль
class SimpleLogService : ILogService
{
    public void Write(string message) => Console.WriteLine(message);
}
// сервис, который выводит сообщение зеленым цветом
class GreenLogService : ILogService
{
    public void Write(string message)
    {
        var defaultColor = Console.ForegroundColor;
        Console.ForegroundColor = ConsoleColor.DarkGreen;
        Console.WriteLine(message);
        Console.ForegroundColor = defaultColor;
    }
}
class Logger
{
    ILogService logService;
    public Logger(ILogService logService) => this.logService = logService;
    public void Log(string message) =>logService.Write($"{DateTime.Now}  {message}");
}

Теперь класс Logger не зависит от конкретной реализации класса SimpleLogService - это может быть любая реализация интерфейса ILogService. Кроме того, создание сервиса логгера выносится во внешний код. Класс Logger больше ничего не знает о сервисе кроме того, что у него есть метод Write, который позволяет логгировать сообщение куда-то каким-то образом.

Для демонстрации я добавил второй класс сервиса логгера, который выводит сообщение на консоль зеленым цветом.

15.11.2022 13:40:53  Hello METANIT.COM
15.11.2022 13:40:53  Hello METANIT.COM

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

Для упрощения управления зависимостями нередко используются специальные контейнеры - IoC-контейнеры (Inversion of Control). Такие контейнеры позволяют устанавливать зависимости между абстракциями и конкретными объектами и, как правило, управляют созданием этих объектов. Преимуществом .NET является то, что фреймворк имеет встроенный контейнер внедрения зависимостей, который представлен интерфейсом IServiceProvider. А сами зависимости еще называются сервисами, собственно поэтому контейнер можно назвать провайдером сервисов. Этот контейнер отвечает за сопоставление зависимостей с конкретными типами и за внедрение зависимостей в различные объекты.

Вся основная функциональность внедрения зависимостей в .NET расположена в пакете Microsoft.Extensions.DependencyInjection. Стоит отметить, что в проект консольного приложения, а также в ряд других типов проектов этот пакет по умолчанию НЕ установлен. Поэтому нам надо предварительно установить через Nuget данный пакет.

Коллекция сервисов ServiceCollection

Все сервисы или зависимости хранятся в механизме DI в .NET хранятся в специальной коллекции сервисов, которая представляет тип IServiceCollection. .NET предоставляет встроенную реализацию этого интерфейса - класс ServiceCollection:

using Microsoft.Extensions.DependencyInjection;

IServiceCollection services = new ServiceCollection();

Для добавления сервисов в ServiceCollection применяется ряд методов. Например, добавим ранее определенный сервис ILogService:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection()
    .AddTransient<ILogService, SimpleLogService>();

interface ILogService
{
    void Write(string message);
}
// простой вывод на консоль
class SimpleLogService : ILogService
{
    public void Write(string message) => Console.WriteLine(message);
}

Здесь для добавления сервиса ILogService применяется метод AddTransient<S, I>(), который типизируется двумя типами. Первый тип представляет сам сервис, а второй - его конкретную реализацию. То есть в данном случае мы говорим, что в качестве реализации сервиса ILogService будет выступать класс SimpleLogService. Метод AddTransient возвращает измененный объект IServiceCollection.

Получение сервиса

Выше мы добавили сервис. Как теперь его получить и использовать в программе? Для этого нам нужен провайдер сервисов IServiceProvider. Для его получения у коллекции сервисов вызывается метод BuildServiceProvider(), который возвращает встроенную реализацию провайдера - объект ServiceProvider:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

// получаем провайдер сервисов
using var serviceProvider = services.BuildServiceProvider();

Для получения сервиса из провайдера ServiceProvider можно использовать ряд методов. Например, метод GetService<T> типизируется типом сервиса, который надо получить:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection()
    .AddTransient<ILogService, SimpleLogService>();

using var serviceProvider = services.BuildServiceProvider();
// получаем сервис ILogService
ILogService? logService = serviceProvider.GetService<ILogService>();
// используем сервис
logService?.Write("Hello METANIT.COM");

interface ILogService
{
    void Write(string message);
}
class SimpleLogService : ILogService
{
    public void Write(string message) => Console.WriteLine(message);
}

Таким образом, у нас определена единая точка, где мы определяем конкретную реализацию сервиса - метод AddTransient:

AddTransient<ILogService, SimpleLogService>();

И в программе в любой ее точке мы можем вызвать метод GetService и получить реализацию сервиса:

ILogService? logService = serviceProvider.GetService<ILogService>();

Получение сервиса в конструкторе

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

class Logger
{
    ILogService? logService;
    public Logger(ILogService? logService) => this.logService = logService;
    public void Log(string message) => logService?.Write($"{DateTime.Now}  {message}");
}

Класс Logger получает через конструктор сервис ILogService и для логгирования сообщения в методе Log вызывает его метод Write. Затем в программе мы можем явным образом создать объект этого класса, передав в конструктор нужную реализацию ILogService:

using var serviceProvider = services.BuildServiceProvider();
ILogService? logService = serviceProvider.GetService<ILogService>();
Logger logger = new Logger(logService); // создаем объект Logger
logger.Log("Hello METANIT.COM");

Но также мы можем для создания объекта Logger использовать тот же механизм внедрения зависимостей:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection()
    .AddTransient<ILogService, SimpleLogService>()
    .AddTransient<Logger>();

using var serviceProvider = services.BuildServiceProvider();
// получаем объект Logger
Logger? logger = serviceProvider.GetService<Logger>();
logger?.Log("Hello METANIT.COM");

interface ILogService
{
    void Write(string message);
}
class SimpleLogService : ILogService
{
    public void Write(string message) => Console.WriteLine(message);
}
class Logger
{
    ILogService? logService;
    public Logger(ILogService? logService) => this.logService = logService;
    public void Log(string message) => logService?.Write($"{DateTime.Now}  {message}");
}

Здесь Logger также выступает в качестве сервиса и добавляется в контейнер сервисов с помощью метода AddTransient:

AddTransient<Logger>();

Только теперь тип сервиса и его реализация будут совпадать. Затем также с помощью метода GetService получаем этот сервис из ServiceProvider:

Logger? logger = serviceProvider.GetService<Logger>();

Но обратите внимание, что здесь нигде явным образом мы не определяем объект ILogService, который передается в конструктор объекта Logger, это делает за нас система внедрения зависимостей. Она видит, что для сервиса ILogService зарегистрирована реализация SimpleLogService, поэтому при создании объекта Logger неявно создает объект SimpleLogService и передает его в конструктор Logger.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850