Внедрение зависимостей в .Net Марка Симана 2 — Внедрение конструктора, время жизни

    Зависимости между слоями приложения | Внедрение конструктора, время жизни | Сквозные аспекты приложения, перехват, декоратор

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

    Агрегация, внедрение конструктора


    Объекты/классы системы, как и слои, взаимодействуют друг с другом. Между классами тоже есть зависимости.

    Например, в листинге 1 MyService использует MyDataContext (EF) – имеет зависимость MyDataContext.

    class MyService
    {
        public void DoSomething()
        { 
            using(var dbCtx = new MyDataContext())
            {
                // используем dbCtx
            }
        }
    }
    
    Листинг 1. Сильная зависимость MyService от MyDataContext
    

    У кода выше есть недостатки:

    — используется антипаттерн «Диктатор»: MyService сам создает и контролирует время жизни свой зависимости MyDataContext.
    — нарушен принцип инверсии зависимости (Dependency Inversion Principle, DIP) (куда же в «наукообразной» статье без SOLID): MyService зависит от конкретной реализации MyDataContext, было бы лучше использовать интерфейс/абстрактный класс.

    Принцип инверсии зависимости (Dependency Inversion Principle, DIP)

    Фактически синоним для требования «Программировать в соответствии с интерфейсом, а не с конкретной реализацией».
    (цитата из книги)

    Улучшим код с помощью агрегации — листинг 2:

    class MyService
    {
        private readonly IRepository Repository;
        public MyService(IRepository repository){
            if(repository == null) 
                throw new ArgumentNullException(nameof(repository));
            
            Repository = repository;
        }
    
        public void DoSomething()
        { 
            // используем Repository
        }
    }
    
    Листинг 2. Агрегация. MyService не создает и не управляет временем жизни свой зависимости Repository
    

    Отступление:

    Хорошая статья про агрегацию и композицию написана Сергеем Тепляковым. Кроме прочего статья научит вас рисовать умные схемы. В качестве спойлера: какая схема описывает агрегацию?

    Схемы композиции и агрегации
    Рис 1. Схемы композиции и агрегации

    Вернемся к листингу 2. Это и есть внедрение зависимости, при том лучший вариант — «Внедрение конструктора». Связанность уменьшилась, но появился вопрос: как же вызвать Dispose репозитория? Помните в листинге 1 использовался Using?

    Класс, передавший управление своими зависимостями, теряет более чем просто возможность выбирать конкретные реализации абстракций. Он также теряет возможность управления как моментом создания экземпляра, так и моментом, когда этот экземпляр становится недоступным.
    (цитата из книги)

    Интересное замечание: если класс имеет более 4-х зависимостей (более 4-х параметров конструктора) – это повод задуматься над рефакторингом. Похоже, что объект выполняет слишком много функций, нарушается принцип единичной ответственности (Single Responsibility Principle, SRP – опять SOLID).

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


    Отвечая на вопрос “как же вызвать Dispose репозитория?” Марк предлагает пойти на компромисс. MyService не должен знать об особенностях реализации IRepository, в том числе о необходимости освобождения ресурсов. Т.е. вот такое определение IRepository нежелательно:

    interface IRepository : IDisposable 
    {
        void DeleteProduct(int id);
    }

    Кроме того, что такой интерфейс открывает потребителю (MyService) часть знания о конкретной реализации, он еще накладывает ограничение на возможные реализации – они должны реализовать IDisposable (может он им не нужен).

    А в имплементации IRepository это знание, о реализации, допускается – листинг 3.

    class SqlRepository : IRepository 
    {
        IDataContextFactory DbContextFactory;
        public SqlRepository(IDataContextFactory dbContextFactory)
        {
            if(dbContextFactory == null) 
                throw new ArgumentNullException(nameof(dbContextFactory));
            
            DbContextFactory = dbContextFactory;
        }
    
        public void DeleteProduct(int id);
        {
            using(var dbCtx = DbContextFactory.Create())
            {
                // использование dbCtx
            }
        }
    }
    
    Листинг 3. Реализация IRepository инкапсулирует работу с базой данных
    

    Дополнение (не самое главное): SqlRepository управляет временем жизни DataConext, но создание вынесено в фабрику.

    В этом и заключается компромисс: да, SqlRepository управляет временем жизни DataContext, но это не влияет на остальной код.

    Выше описано хорошее решение, но применить его не всегда возможно. Например, нужна транзакционность:

    public void DoSomething(int productId)
    { 
        this.Repository.DeleteProduct(productId);
        this.Repository.DeleteHistory(productId);
    }
    
    Листинг 4. Удаление продукта и истории должно выполняться в одной транзакции
    

    Если удаление истории завершается ошибкой, удаление продукта должны быть отменено (по умному это паттерн Unit of Work). Тогда комитить в базу отдельно в методах DeleteProduct и DeleteHistory нельзя. Как же быть? Вы знаете, где искать ответ.

    Продолжение следует


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

    Ну. И что?
    Реклама
    Комментарии 5
      +1
      Слово имплементация режет слух, у этого слова есть явный однозначный перевод — реализация
        0
        Вы просто книгу пересказываете, я правильно понимаю?
          0
          Марк предлагает пойти на компромисс. MyService не должен знать об особенностях реализации IRepository

          Что-то я тут не понял, а именно: особенности реализации. MyService ведь получает интерфейс в качестве зависимости.

          Кроме того, что такой интерфейс открывает потребителю (MyService) часть знания о конкретной реализации

          «часть знания о конкретной реализации»: я так понимаю под частью знания имеется ввиду IDisposable?
            0
            «часть знания о конкретной реализации»: я так понимаю под частью знания имеется ввиду IDisposable?

            Да IDisposable. Этот интерфейчас указывает что объект реализован таким образом, что требуется освобождение ресурсов — это часть знания о его реализации.
            0
            промахнулся

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое