[PHP, Проектирование и рефакторинг, Laravel] Еще немного про сервисный слой в PHP
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В жизни каждого разработчика наступает момент, когда одного понимания популярных паттернов и правил написания чистого кода начинает не хватать. Обычно это происходит, когда на поток поступает проект сложнее типового сайта-каталога. При создании такого проекта очень важно заложить правильную архитектуру (особенно, если проект долгосрочный), которая будет способна максимально гибко и быстро адаптироваться под новые требования бизнеса.Сегодня мы поговорим об одном из способов организации бизнес логики - сервисном слое (он же service layer), когда и зачем его нужно применять, а также какие проблемы архитектуры он поможет решить. Примеры реализации будут показаны с использованием архитектурного паттерна MVC и фреймворка Laravel.Решение, описанное ниже, не является единственной истинной того, как надо делать. Идея данной статьи в том, чтобы осветить определенные проблемы, возникающие на этапе разработки веб-приложений, а также показать, на субъективный взгляд автора, практичные примеры решения данных проблем.Стоит также заметить, что Service layer - не панацея всего. Это всего лишь один из подходов к структурированию кода в вашем приложении, который будет поддаваться расширению и будет понятен для других программистов.ОсновыЕсли обратиться к теории, то сервисный слой можно описать таким определением:Сервисный слой (Service layer) — это шаблон проектирования, который инкапсулирует бизнес логику вашего приложения и определяет границу и набор допустимых операций с точки зрения взаимодействующих с ним клиентов.Думаю, что звучит запутанно и сложно. Если простыми словами, то вы сосредотачиваете логику вашего приложения в одном (или нескольких) классе-сервисе, а в своих контроллерах обращаетесь к нему. Это избавляет от дублирования кода в разных участках системы, делая ваш контроллер действительно соответствующим букве S из SOLID.Мы не будем рассматривать примеры, где показывают работу с Eloquent в контроллере, как его выносят в сервисный слой и т.д. Такие примеры, на мой взгляд, достаточно абстрактно описывают проблему и не показывают преимущества сервисного слоя. Вместо этого, мы рассмотрим реализацию классической логики интернет-магазина, где при определенных событиях, система оповещает клиента определенными сообщениями. Итак, начнем.Email уведомленияПредставим, что вы разрабатываете интернет-магазин и задача на текущий день - написать код, который будет оповещать клиента об успешном оформлении заказа. Реализуем эту простую логику.
namespace App\Http\Controllers;
use App\Http\Requests\CreateOrderRequest;
use Illuminate\Support\Facades\Mail;
class OrderController
{
public function createOrder(CreateOrderRequest $request)
{
// Логика создания заказа...
Mail::send('mail.order_created', [
'order' => $order
], function ($message) use ($order) {
$message->to($order->email)
->subject(trans('mail/order_created.mail_title'));
});
}
}
Думаю, что ничего сложного. Laravel позволяет нам реализовывать типовую логику большинства приложений в несколько строчек кода и это прекрасно. Далее, нам поступила задача также отправлять письмо клиенту, когда статус его заказа изменился.
public function editOrder(EditOrderRequest $request)
{
// Логика обновления данных заказа...
Mail::send('mail.order_updated', [
'order' => $order
], function ($message) use ($order) {
$message->to($order->email)
->subject(trans('mail/order_updated.mail_title'));
});
}
И конечно, нам нужно поприветствовать клиента, когда он решил стать нашим постоянным покупателем.
public function registerCustomer(RegisterCustomerRequest $request)
{
// Логика регистрации пользователя...
Mail::send('mail.customer_register', [
'customer' => $customer
], function ($message) use ($customer) {
$message->to($customer->email)
->subject(trans('mail/customer_register.mail_title'));
});
}
Таким образом, мы решаем все последующие задачи оповещений с помощью фасада Mail и все идет по плану, до тех пор, пока наш интернет-магазин не начинает стремительно набирать обороты.Набор оборотовНаш магазин уже достаточно давно на рынке и в один прекрасный день бизнес понимает, что ему мало стандартных email оповещений. Необходимо отслеживать активность, предлагать оставить отзыв о купленном товаре, массово оповещать о новых поступлениях, скидках и т.д. В общем - бизнес хочет перейти на сервисы email рассылок, которые прекрасно справятся с поставленными задачами. Конечно бизнес приходит к нам и просит перейти на новую систему оповещений.Выше, мы реализовали три отправки email сообщений в трех разных частях системы, а поскольку мы рассматриваем близкую к реальности ситуацию, то по мере развития интернет-магазина будет реализовано еще очень много таких отправок. А теперь представьте, что нам нужно пройтись по всем частям системы и заменить старый код с фасадом Mail на новую логику отправки с помощью сервиса рассылок. Сколько времени необходимо на это потратить и сколько тестов нужно переписать (если код конечно покрывался тестами)? И чем больше кода разработчику необходимо изменить, тем больше вероятность допущения ошибки по причине человеческого фактора. Хорошо еще, если разработчик вынесет логику обращения к сервису рассылок в отдельный класс, а не будет дублировать код по всем частям системы. Чтобы не попадать в такие ситуации, перепроектируем систему с применением сервисного слоя.Сервисный слойДля начала, давайте инкапсулируем логику уведомлений в новый класс NotificationService.
namespace App\Services;
use Illuminate\Support\Facades\Mail;
use App\Mail\Events\MailEventInterface;
use App\Mail\Events\OrderCreatedEvent;
use App\Mail\Events\OrderUpdatedEvent;
use App\Mail\Events\CustomerRegisterEvent;
class NotificationService
{
public function notify(string $event, array $data)
{
$event = $this->makeNotificationEvent($event, $data);
Mail::send($event->getView(), $event->getData(), function ($message) use ($event) {
$message->to($event->getEmail())
->subject($event->getMailSubject());
});
}
private function makeNotificationEvent(string $event, array $data) : MailEventInterface
{
switch ($event) {
case 'order_created':
return new OrderCreatedEvent($data);
case 'order_updated':
return new OrderUpdatedEvent($data);
case 'customer_register':
return new CustomerRegisterEvent($data);
default:
throw new \InvalidArgumentException("Undefined event $event");
}
}
}
Далее, создадим интерфейс MailEventInterface.
namespace App\Mail\Events;
interface MailEventInterface
{
public function getView() : string;
public function getData() : array;
public function getEmail() : string;
public function getMailSubject() : string;
}
А также, в качестве примера, напишем новый класс OrderCreatedEvent (оповещение клиента об успешном оформлении заказа).
namespace App\Mail\Events;
class OrderCreatedEvent implements MailEventInterface
{
private $order;
public function __construct(array $data)
{
// Логика валидации (на любителя)
$this->order = $data['order'];
}
public function getView(): string
{
return 'mail.order_created';
}
public function getData(): array
{
return [
'order' => $this->order
];
}
public function getEmail(): string
{
return $this->order->email;
}
public function getMailSubject(): string
{
return trans('mail/order_created.mail_title');
}
}
Теперь мы можем переписать наш контроллер, используя сервисный слой.
namespace App\Http\Controllers;
use App\Http\Requests\CreateOrderRequest;
use App\Services\NotificationService;
class OrderController
{
private $notificationService;
public function __construct(NotificationService $notificationService)
{
$this->notificationService = $notificationService;
}
public function createOrder(CreateOrderRequest $request)
{
// Логика создания заказа...
$this->notificationService->notify('order_created', [
'order' => $order
]);
}
}
Что у нас получилось? Мы передали ответственность за отправку писем сервисному слою, избавив наш контроллер от дополнительных обязанностей. Теперь контроллеру не важно, каким образом клиент будет оповещен о нужных событиях, он лишь знает как использовать предоставленный ему интерфейс. Если в будущем нам поступит задача об отправке оповещений через сервис рассылок (или еще что-нибудь), то мы изменим реализацию лишь в одном месте, вместо замены десятка раскиданных по системе реализаций. На этом этапе, я надеюсь, что преимущества сервисного слоя перед традиционным подходом "раздутый контроллер" стали очевиднее. Теперь поговорим о деталях.Нужно ли объявлять интерфейс для сервисного слоя?И да и нет. Ответ тут зависит от ситуации. Взгляните на пример выше. Этот код прекрасно проявит себя в деле, если поступит задача отправлять все письма через сервис рассылок. Но что если нам понадобиться перевести лишь часть событий? В таком случае, гораздо эффективнее было бы объявить общий интерфейс NotificationServiceInterface и в зависимости от контроллера, пробрасывать соответствующую реализацию в нашем сервис-провайдере. Что-то по типу этого.
$this->app->when(OrderController::class)
->needs(NotificationServiceInterface::class)
->give(function () {
return new ESputnikNotificationService();
});
$this->app->when(OrderUpdateController::class)
->needs(NotificationServiceInterface::class)
->give(function () {
return new MailNotificationService();
});
К слову, в 95% случаях, интерфейсы для сервисного слоя все-таки не нужны.Можно ли использовать сервисы внутри сервисов?Я бы однозначно не рекомендовал такую практику, так как этим вы нарушаете single responsibility принцип, делая ваш код, к тому же, достаточно запутанным.Работу с несколькими сервисами можно организовать такими способами.1. Внедрением зависимостей в контроллер и поочередный их вызов в экшене. В таком случае отлов неудач можно, например, делегировать блоку try/catch.
class OrderController
{
public function saveOrder(
SaveOrderRequest $request,
OrderService $orderService,
NotificationService $notificationService
) {
try {
$order = $orderService->createOrderFromRequest($request);
$notificationService->notify('order_created', [
'order' => $order
]);
return response()->json([
'success' => true,
'data' => [
'order' => $order
]
]);
}
catch (OrderServiceException|NotificationServiceException $e) {
return response()->json([
'success' => false,
'exception' => $e->getMessage()
]);
}
}
}
2. Выделение класса, которому можно делегировать работу с цепочкой сервисов. Например, я обычно использую класс с суффиксом Operation (CreateOrderOperation). Ошибки можно все также отлавливать с помощью try/catch, но гораздо практичнее будет ввести сущность OperationResult, которую будет возвращать каждая операция в не зависимости от результата выполнения. Это способ мне нравится больше.
class OrderController
{
public function saveOrder(
SaveOrderRequest $request,
CreateOrderOperation $createOrderOperation
) {
// Внутри операции выполняются все обращения к сервисам и т.д.
$result = $createOrderOperation->createOrderFromRequest($request);
// Для более чистого экшена, сущность OperationResult
// может имплементировать JsonSerializable
return response()->json($result);
}
}
На этом моменте статья подошла к своему логическому завершению. Надеюсь, она поможет начинающим разработчикам и тем, кто мало знаком с сервисным слоем, до конца понять суть подхода и проблемы, которые он решает.Всем спасибо за внимание!
===========
Источник:
habr.com
===========
Похожие новости:
- [PHP, Программирование] Как дойти до CQRS, если у тебя PHP
- [Высокая производительность, Разработка веб-сайтов, PHP, Программирование] Теория программирования: пакетные принципы и метрики
- [Работа с 3D-графикой, Проектирование и рефакторинг, CAD/CAM] Элемент внезапности ( или нет?) при разработке продукции
- [Разработка веб-сайтов, Проектирование и рефакторинг, Терминология IT, Хранение данных] Символы Unicode: о чём должен знать каждый разработчик (перевод)
- [Разработка веб-сайтов, PHP, Laravel, Локализация продуктов] Разделение кода и текста: прототип
- [PHP, Программирование] Работа с заказом через админку OpenCart, взгляд изнутри
- [Разработка веб-сайтов, JavaScript, Программирование, Проектирование и рефакторинг, ReactJS] Фреймворк-независимое браузерное SPA (перевод)
- [Разработка веб-сайтов, PHP, Программирование, Учебный процесс в IT, Карьера в IT-индустрии] Курсы PHP-программирования в Минске. Куда пойти учиться?
- [Анализ и проектирование систем, Проектирование и рефакторинг, Подготовка технической документации] О роли системного аналитика и шаблон для проектирования
- [Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, ReactJS] Вариант повторного использования кода компонентов, который упустили в веб-разработке
Теги для поиска: #_php, #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_laravel, #_php, #_servicelayer, #_laravel, #_php, #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
), #_laravel
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:35
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В жизни каждого разработчика наступает момент, когда одного понимания популярных паттернов и правил написания чистого кода начинает не хватать. Обычно это происходит, когда на поток поступает проект сложнее типового сайта-каталога. При создании такого проекта очень важно заложить правильную архитектуру (особенно, если проект долгосрочный), которая будет способна максимально гибко и быстро адаптироваться под новые требования бизнеса.Сегодня мы поговорим об одном из способов организации бизнес логики - сервисном слое (он же service layer), когда и зачем его нужно применять, а также какие проблемы архитектуры он поможет решить. Примеры реализации будут показаны с использованием архитектурного паттерна MVC и фреймворка Laravel.Решение, описанное ниже, не является единственной истинной того, как надо делать. Идея данной статьи в том, чтобы осветить определенные проблемы, возникающие на этапе разработки веб-приложений, а также показать, на субъективный взгляд автора, практичные примеры решения данных проблем.Стоит также заметить, что Service layer - не панацея всего. Это всего лишь один из подходов к структурированию кода в вашем приложении, который будет поддаваться расширению и будет понятен для других программистов.ОсновыЕсли обратиться к теории, то сервисный слой можно описать таким определением:Сервисный слой (Service layer) — это шаблон проектирования, который инкапсулирует бизнес логику вашего приложения и определяет границу и набор допустимых операций с точки зрения взаимодействующих с ним клиентов.Думаю, что звучит запутанно и сложно. Если простыми словами, то вы сосредотачиваете логику вашего приложения в одном (или нескольких) классе-сервисе, а в своих контроллерах обращаетесь к нему. Это избавляет от дублирования кода в разных участках системы, делая ваш контроллер действительно соответствующим букве S из SOLID.Мы не будем рассматривать примеры, где показывают работу с Eloquent в контроллере, как его выносят в сервисный слой и т.д. Такие примеры, на мой взгляд, достаточно абстрактно описывают проблему и не показывают преимущества сервисного слоя. Вместо этого, мы рассмотрим реализацию классической логики интернет-магазина, где при определенных событиях, система оповещает клиента определенными сообщениями. Итак, начнем.Email уведомленияПредставим, что вы разрабатываете интернет-магазин и задача на текущий день - написать код, который будет оповещать клиента об успешном оформлении заказа. Реализуем эту простую логику. namespace App\Http\Controllers;
use App\Http\Requests\CreateOrderRequest; use Illuminate\Support\Facades\Mail; class OrderController { public function createOrder(CreateOrderRequest $request) { // Логика создания заказа... Mail::send('mail.order_created', [ 'order' => $order ], function ($message) use ($order) { $message->to($order->email) ->subject(trans('mail/order_created.mail_title')); }); } } public function editOrder(EditOrderRequest $request)
{ // Логика обновления данных заказа... Mail::send('mail.order_updated', [ 'order' => $order ], function ($message) use ($order) { $message->to($order->email) ->subject(trans('mail/order_updated.mail_title')); }); } public function registerCustomer(RegisterCustomerRequest $request)
{ // Логика регистрации пользователя... Mail::send('mail.customer_register', [ 'customer' => $customer ], function ($message) use ($customer) { $message->to($customer->email) ->subject(trans('mail/customer_register.mail_title')); }); } namespace App\Services;
use Illuminate\Support\Facades\Mail; use App\Mail\Events\MailEventInterface; use App\Mail\Events\OrderCreatedEvent; use App\Mail\Events\OrderUpdatedEvent; use App\Mail\Events\CustomerRegisterEvent; class NotificationService { public function notify(string $event, array $data) { $event = $this->makeNotificationEvent($event, $data); Mail::send($event->getView(), $event->getData(), function ($message) use ($event) { $message->to($event->getEmail()) ->subject($event->getMailSubject()); }); } private function makeNotificationEvent(string $event, array $data) : MailEventInterface { switch ($event) { case 'order_created': return new OrderCreatedEvent($data); case 'order_updated': return new OrderUpdatedEvent($data); case 'customer_register': return new CustomerRegisterEvent($data); default: throw new \InvalidArgumentException("Undefined event $event"); } } } namespace App\Mail\Events;
interface MailEventInterface { public function getView() : string; public function getData() : array; public function getEmail() : string; public function getMailSubject() : string; } namespace App\Mail\Events;
class OrderCreatedEvent implements MailEventInterface { private $order; public function __construct(array $data) { // Логика валидации (на любителя) $this->order = $data['order']; } public function getView(): string { return 'mail.order_created'; } public function getData(): array { return [ 'order' => $this->order ]; } public function getEmail(): string { return $this->order->email; } public function getMailSubject(): string { return trans('mail/order_created.mail_title'); } } namespace App\Http\Controllers;
use App\Http\Requests\CreateOrderRequest; use App\Services\NotificationService; class OrderController { private $notificationService; public function __construct(NotificationService $notificationService) { $this->notificationService = $notificationService; } public function createOrder(CreateOrderRequest $request) { // Логика создания заказа... $this->notificationService->notify('order_created', [ 'order' => $order ]); } } $this->app->when(OrderController::class)
->needs(NotificationServiceInterface::class) ->give(function () { return new ESputnikNotificationService(); }); $this->app->when(OrderUpdateController::class) ->needs(NotificationServiceInterface::class) ->give(function () { return new MailNotificationService(); }); class OrderController
{ public function saveOrder( SaveOrderRequest $request, OrderService $orderService, NotificationService $notificationService ) { try { $order = $orderService->createOrderFromRequest($request); $notificationService->notify('order_created', [ 'order' => $order ]); return response()->json([ 'success' => true, 'data' => [ 'order' => $order ] ]); } catch (OrderServiceException|NotificationServiceException $e) { return response()->json([ 'success' => false, 'exception' => $e->getMessage() ]); } } } class OrderController
{ public function saveOrder( SaveOrderRequest $request, CreateOrderOperation $createOrderOperation ) { // Внутри операции выполняются все обращения к сервисам и т.д. $result = $createOrderOperation->createOrderFromRequest($request); // Для более чистого экшена, сущность OperationResult // может имплементировать JsonSerializable return response()->json($result); } } =========== Источник: habr.com =========== Похожие новости:
Проектирование и рефакторинг ), #_laravel |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:35
Часовой пояс: UTC + 5