Русский
Русский
English
Статистика
Реклама

Service-layer

Еще немного про сервисный слой в PHP

17.03.2021 14:12:14 | Автор: admin

В жизни каждого разработчика наступает момент, когда одного понимания популярных паттернов и правил написания чистого кода начинает не хватать. Обычно это происходит, когда на поток поступает проект сложнее типового сайта-каталога. При создании такого проекта очень важно заложить правильную архитектуру (особенно, если проект долгосрочный), которая будет способна максимально гибко и быстро адаптироваться под новые требования бизнеса.

Сегодня мы поговорим об одном из способов организации бизнес логики - сервисном слое (он же 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);    }}

На этом моменте статья подошла к своему логическому завершению. Надеюсь, она поможет начинающим разработчикам и тем, кто мало знаком с сервисным слоем, до конца понять суть подхода и проблемы, которые он решает.

Всем спасибо за внимание!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru