Зависимости между слоями приложения | Внедрение конструктора, время жизни
Продолжаем борьбу за слабую связанность. В предыдущей заметке мы рассмотрели зависимости между слоями приложения, прейдем к меньшим формам.
Агрегация, внедрение конструктора
Объекты/классы системы, как и слои, взаимодействуют друг с другом. Между классами тоже есть зависимости.
Например, в листинге 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 нельзя. Как же быть? Вы знаете, где искать ответ.
Продолжение следует
Мы рассмотрели основной прием внедрения завистей: агрегация, реализованная с помощью внедрения конструктора. Коснулись темы управления временем жизни объектов. До новых встреч.
Автор: Alex_BBB