Принцип SRP (Принцип Единой Ответственности) — один из основополагающих принципов написания поддерживаемого кода. В этой статье я покажу как применить данный принцип на примере языка PHP и фреймворка Laravel.

Часто, описывая модель разработки MVC (MVP, MVVM или другие M**), на контроллер возлагаются необоснованно большие задачи. Получение параметров, бизнес логика, авторизация и ответ.

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

Принцип единой ответственности


Для примера возьмем довольно грубый, но часто встречающийся тип «толстого» контроллера.

class OrderController extends Controller
{
    public function create(Request $request)
    {
        $rules = [
            'product_id' => 'required|max:10',
            'count' => 'required|max:10',
        ];
        $validator = Validator::make($request->all(), $rules);
        if ($validator->fails()) {
            return response()->json($validator->errors(), 400);
        }

         // Создаем заказ
        $order = Order::create($request->all());

        // Резервируем товар 

        // Отправляем sms сообщение с подтверждением покупателю

        return response()->json($order, 201);
    }
}

В данном примере видно, что контроллер слишком много знает о «размещении заказа», так же на него возложена задача оповестить покупателя и зарезервировать товар.

Мы будем стремиться к тому, что контроллеру предстоит только контролировать весь процесс и никакой бизнес логики.

Начнем с того, что вынесем валидацию параметров в отдельный Request класс.

class OrderRequest extends Request
{
    public function rules(): array
    {
         return [
             'product_id' => 'required|max:10',
             'count' => 'required|max:10',
         ];    
    }
}

А всю бизнес логику переместим в класс OrderService

public class OrderService
{
    public function create(array $params)
    {
        // Создаем заказ

        // Резервируем товар

        // Создаем класс для отправки sms
        $sms = new RuSms($appId);

        // Получаем номер, формируем сообщение и отправляем
        $sms->send($number, $text);
     }
}

Используя Сервис Контейнер внедрим наш сервис в контроллер.

В итоге имеем «тонкий» контроллер.

class OrderController extends Controller
{
    protected $service;
	
    public function __construct(OrderService $service)
    {
	$this->service = $service;	
    }

    public function create(OrderRequest $request)
    {
        $this->service->create($request->all());

        return response()->noContent(201);
    }
}

Уже лучше. Вся бизнес логика перемещена в сервис. Контроллер знает только о классах OrderRequest и OrderService — минимальном наборе информации, необходимом для выполнения своей работы.

Но теперь наш сервис так же нуждается в рефакторинге. Вынесем логику отправки sms в отдельный класс.

class SmsRu
{
    protected $appId;

    public function __constructor(string $appId)
    {
        $this->appId = $appId;
    }

    public function send($number, $text): void
    {
          // Описываем здесь логику отправки смс
    }
}    

И внедрим его через конструктор

class OrderService
{
    private $sms;

    public function __construct()
    {
	$this->sms = new SmsRu($appId);	
    }

    public function create(array $params): void
    {
          // Создаем заказ

          // Резервируем товар

          // Получаем номер, формируем сообщение и отправляем
          $this->sms->send($nubmer, $text);
    }
}    

Уже лучше, но класс OrderService до сих пор знает слишком много об отправке сообщений. Возможно нам потребуется в будущем заменить провайдера рассылки сообщений или добавить модульные тесты. Продолжим рефакторинг добавив интерфейс SmsSender, а сам SmsRu укажем через провайдер SmsServiceProvider.

interface SmsSenderInterface
{
    public function send($number, $text): void;
}

class SmsServiceProvider implements ServiceProviderInterface
{
    public function register(): void
    {
        $this->app->singleton(SmsSenderInterface::class, function ($app) {
            return new SmsRu($params['app_id']);
        });
    }
}

Теперь сервис так же освобожден от ненужных деталей

class OrderService
{
    private $sms;

    public function __construct(SmsSenderInterface $sms)
    {
	$this->sms = $sms;	
    }

    public function create(): void
    {
          // Создаем заказ

          // Резервируем товар

          // Получаем номер, формируем сообщение и отправляем
          $this->sms->send($nubmer, $text);
    }
}    

Событийно-управляемая архитектура


Отправка сообщений не является частью основного процесса создания заказа. Заказ создастся вне зависимости от отправки сообщения, так же возможно в будущем будет добавлена опция отмены sms оповещения. Для того чтобы не перегружать OrderService ненужными подробностями оповещения, можно использовать Laravel Observers. Класс который будет отслеживать события при определенном поведении нашей модели Order и возложить на него всю логику оповещения покупателя.

class OrderObserver
{
    private $sms;

    public function __construct(SmsSenderInterface $sms)
    {
	$this->sms = $sms;	
    }

    /**
     * Handle the Order "created" event.
     */
    public function created(Order $order)
    {
        $this->sms->send($nubmer, $text);
    }

Не забываем зарегистрировать OrderObserver в AppServiceProvider.

Вывод


Я понимаю, что описываю в этом посте довольно банальные вещи, но хотелось показать, как этот принцип реализуется на фреймворке Laravel.

Принцип SPR один из важнейших и открывает дорогу к применению остальных. Обязателен к применению, тем более что фреймворк Laravel предоставляет все необходимые инструменты.