Добро пожаловать в новую серию! Мы собираемся углубиться в то, как .NET реализует внедрение зависимостей (Dependency injection или, если кратко DI), и как мы можем использовать его, чтобы сделать наши приложения более удобными для изменений. Приступим!

Что такое внедрение зависимостей?

Короче говоря, внедрение зависимостей — это методология, с помощью которой объект кода получает другие объекты, от которых он зависит (зависимости), а не те зависимости, которые должны быть созданы этим объектом. Мне нравится сравнивать это с иглой и ниткой: в иглу должна быть вставлена нить (т. е. пропущена через игольное ушко), прежде чем она сможет выполнять какую-либо значимую работу.

Почему мы это делаем? Чтобы улучшить наше разделение задач; то есть позволить различным частям базы кода иметь возможность изменяться независимо от других частей.

Введение— принцип инверсии зависимостей.

Концепция внедрения зависимостей возникает из принципа проектирования программного обеспечения, называемого принципом инверсии зависимостей (Dependency Inversion Principle). Это один из принципов SOLID, и он состоит из двух частей:

 1. Модули высокого уровня не должны ничего импортировать из модулей низкого уровня. Оба должны зависеть от абстракций (например, интерфейсов).
 2. Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций.

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

Вкратце: принцип инверсии зависимостей — это теория, а внедрение зависимостей ( Dependency Injection)— одна из реализаций этой теории.

Пример — тесная связь.

Чтобы понять, почему принцип инверсии зависимостей и внедрение зависимостей являются хорошими идеями, рассмотрим следующие классы MoviesPageModel и MovieRepository:

public class MoviesPageModel
{
    public List<Movie> movies { get; set; } = new List<Movie>();

    public void OnGet()
    {   
        MovieRepository movieRepo = new MovieRepository();
        List<Movie> allMovies = movieRepo.GetAll();
    }
}

public class MovieRepository()
{
    public List<Movie> GetAll()
    {
        //Возвращает список всех фильмов в БД
    }
}

В этом случае мы говорим, что MoviesPageModel и MovieRepository «тесно связаны», потому что, если один из них изменит свою реализацию, другой, скорее всего, тоже придется изменить. Например, предположим, что мы изменили MovieRepository, чтобы иметь конструктор, который принимает строку подключения к базе данных.

public class MovieRepository()
{
    private readonly string _connectionString;

    public MovieRepository(string connectionString)
    {
        _connectionString = connectionString
    }

    public List<Movie> GetAll()
    {
        using(IDbConnection conn = new DbConnection(_connectionString)
        {
            //Возвращает список фильмов из нашей базы данных
        }
    }
}

Мы также должны изменить реализацию MoviesPageModel, чтобы она соответствовала новой реализации MovieRepository.

public class MoviesPageModel
{
    public List<Movie> movies { get; set; } = new List<Movie>();

    public void OnGet()
    {   
        MovieRepository movieRepo 
            = new MovieRepository("MyConnectionString");
        List<Movie> allMovies = movieRepo.GetAll();
    }
}

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

Используя внедрение зависимостей, мы можем сделать так, чтобы эти классы могли изменяться независимо. Однако перед этим мы должны понять, как работает внедрение зависимостей в .NET 6.

Основы внедрения зависимостей (Dependency Injection)

В .NET 6 внедрение зависимостей является «гражданином первого класса», что означает — изначально поддерживается .NET. Вам не нужны никакие сторонние реализации для использования DI в ваших приложениях .NET!

Чтобы использовать DI, мы должны внедрить зависимости в другие классы, которые зависят от них. Эти зависимости нуждаются в абстракции (обычно это интерфейс), и зависимый класс будет вызывать эту абстракцию, когда ему потребуется соответствующая функциональность. Однако конкретная реализация зависимости существует в другом месте, и классу, использующему абстракцию, не нужно заботиться о том, что именно представляет собой эта конкретная реализация. Таким образом, внедрение зависимостей в .NET 6 удовлетворяет принципу инверсии зависимостей.

Отсюда способ использования внедрения зависимостей в .NET состоит из ответов на пять вопросов:

1. Какая может быть зависимость? Что-нибудь с абстракцией. Чаще всего это небольшие, легко модифицируемые классы или объекты.
2. Где существуют эти зависимости? Мы добавляем эти сервисы в «контейнер» проекта.
3. Как эти зависимости внедряются в классы, которые в них нуждаются? .NET сделает это автоматически для служб в контейнере.
4. Как ведут себя эти зависимости? Мы можем назначить «сроки жизни» зависимостям, чтобы указать, создаются ли они один раз, или при каждом запросе, или каждый раз, когда они необходимы.
5. Какие проблемы мы можем решить с помощью DI? Основная из них — «круговые зависимости», но есть и другие.

Этот пост будет посвящен вопросу 1. Следующие две части этой серии ответят на оставшиеся вопросы.

Что можно внедрить?

Если кратко, то всё!

В целом, мы хотим, чтобы наши зависимости были небольшими, сфокусированными и легко изменяемыми (это общее правило проектирования программного обеспечения, а не что-то конкретное для внедрения зависимостей). Например, давайте рассмотрим простой класс «репозиторий» или класс, который обращается к некоторому хранилищу данных (чаще всего к базе данных) и возвращает объекты, созданные из данных в этом хранилище данных.

Помните класс MovieRepository описанный ранее? Мы можем изменить этот класс, чтобы использовать интерфейс IMovieRepository и сделать его внедряемым (инжектируемым):

public interface IMovieRepository
{
    List<Movie> GetAll();
    Movie GetByID(int id);
}

public class MovieRepository : IMovieRepository
{
    public List<Movie> GetAll()
    {
        //Реализация
    }

    public Movie GetByID(int id)
    {
        //Реализация
    }
}

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

public interface ICacheService
{
    void Add(string key, object value);
    T Get<T>(string key);
}

public class CacheService : ICacheService
{
    public void Add(string key, object value)
    {
        //Реализация
    }

    public T Get<T>(string key)
    {
        //Реализация
    }
}
Мы будем более тщательно использовать эти примеры классов в следующих частях этой серии.

В итоге…

Внедрение зависимостей в .NET 6 — это процесс, посредством которого зависимости передаются в зависимые объекты объектом, который содержит и создает зависимости, называемым контейнером. В .NET 6 DI является основополагающей частью, изначально поддерживаемой платформой. Каждая зависимость состоит из абстракции и реализации (чаще всего интерфейса и реализующего класса соответственно). Любой объект с обоими этими параметрами может быть зависимостью. Как правило, мы хотим, чтобы зависимости были небольшими, сфокусированными и легко изменяемыми.