День 1478. #ЗаметкиНаПолях #DesignPatterns
Паттерн Сервис/Репозиторий
Идея паттерна заключается в том, что код верхнего уровня должен полностью игнорировать бизнес-уровень и уровень данных приложения. То есть, если вы запрашиваете заказ, ваш код должен выглядеть примерно так:
var order = orderService.GetOrder(12);
Что делает GetOrder, не важно. Это ответственность сервиса. Хотя, там может быть что-то такое (здесь и далее псевдокод):
GetSalesOrder(id) {
var order = repo.GetOrder(id);
if (order.HasAlert())
notifyService.SendAlert(order.Id);
return order;
}
Сервис предоставляет некоторый функционал: получает заказ из репозитория, а затем, например, отправляет уведомление.
Посмотрим на метод репозитория:
GetSalesOrder(id) {
var order = DB.RunSql(
"SELECT * FROM orders WHERE id = @id",
id);
var customer = DB.RunSql(
"SELECT * FROM customers WHERE id = @CustomerId",
order.CustomerId);
return new Order(order, customer);
}
Репозиторий выбирает данные из двух таблиц БД и возвращает результат. Это пример хорошего использования паттерна сервис/репозиторий: каждая часть стека выполняет свою функцию.
Что даёт этот паттерн? Типичный ответ: вы «можете захотеть сменить СУБД». На самом деле, это крайне маловероятно. Гораздо более вероятно, что вы захотите повторно использовать репозиторий в другом месте (возможно, из другого сервиса).
Особенности использования
1. Двойной репозиторий
Чаще всего это случается при использовании Entity Framework, который сам по себе является репозиторием. Использовать EF внутри репозитория обычно бесполезно, но бывают случаи, когда это имеет смысл. Например, если вы решили переключиться с EF на Dapper, нужно взять контекст БД, который использует сервис, и заменить его репозиторием.
2. Сквозной сервис
Рассмотрим следующий код метода сервиса:
GetSalesOrder(id) {
return repo.GetOrder(id);
}
В этом случае он абсолютно ничего не делает, просто предоставляет оболочку, которую вы можете использовать. Можно сказать, что так весь код будет согласованным (контроллер > сервис > репозиторий), но это создаёт кучу лишних классов и делает код менее понятным. Очевидно, идеальная ситуация, когда сервис предоставляет дополнительную бизнес-логику.
3. Утечка ответственности
Это ситуация, когда один уровень начинает брать на себя ответственность от следующего уровня. Причина в том, что вы добавляете сложность, разделяя код на слои, но тесно связываете их, так что репозиторий зависит от сервиса. Например:
GetSalesOrder(id) {
var order = repo.GetOrder(id);
if (order.Name.StartsWith("a"))
order.Notes = repo.GetNotes(id);
…
return order;
}
Это кажется безобидным: на основе полученных данных мы должны или не должны получать дополнительные данные. Ясно, что какие данные извлекаются и как, - это ответственность репозитория, а не сервиса. Если мы рассматриваем примечания как особенность заказа, то везде, где извлекается заказ, потребуется эта логика. Т.е. вы не сможете вызвать репозиторий без сервиса. Если сервис и репозиторий связаны, зачем поддерживать два уровня абстракции?
Более очевидный пример:
CreateOrder(order, orderLines) {
repo.StartTransaction();
repo.CreateOrder(order);
repo.CreateOrderLines(orderLines);
repo.CommitTransaction();
}
Очевидно, что транзакция принадлежит репозиторию и не имеет никакого отношения к сервису.
4. Внешний доступ
Представим, что мы вызываем внешний API. Конечно, этот вызов следует рассматривать так же, как вызов базы данных (в обоих случаях используются внешние для приложения зависимости). Почему бы не создать репозиторий, который бы вызывал API и обрабатывал его ответы так же, как если бы данные извлекались из базы? Таким образом, остальная часть системы остаётся полностью независимой от источника данных, и вы сможете поменять его в любое время.
Источник: https://pmichaels.net/service-repository-pattern/