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

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

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


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

Например, в листинге 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 нельзя. Как же быть? Вы знаете, где искать ответ.

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


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

Комментарии (2)


  1. yarosroman
    11.12.2017 13:25

    Слово имплементация режет слух, у этого слова есть явный однозначный перевод — реализация


  1. romaan27
    11.12.2017 23:40

    Вы просто книгу пересказываете, я правильно понимаю?