Повышение качества Laravel с помощью принципов SOLID: Лучшие практики и примеры

RedDeveloper
12.03.2023 12:33
Повышение качества Laravel с помощью принципов SOLID: Лучшие практики и примеры

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

Существует множество идей о том, как улучшить ваш код, например, KISS (Keep it Simple), DRY (Don't Repeat Yourself) и та, которая будет рассмотрена здесь: SOLID.

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

SOLID - это аббревиатура, которая расшифровывается как:

  • Принцип единой ответственности
  • Принцип открытости/закрытости
  • Принцип замещения Лискова
  • Принцип сегрегации интерфейсов
  • Принцип инверсии зависимостей
SOLID - это аббревиатура правил, которые облегчают жизнь тому, кто читает код для следующего сопровождения, поскольку они подобны "законам", которым вы должны следовать в объектно-ориентированном программировании для лучшей читаемости и управляемости кода.
Эти принципы были впервые представлены Робертом К. Мартином, он же Дядя Боб, в его работе 2000 года "Принципы проектирования и паттерны проектирования", позже эти принципы были перегруппированы, а аббревиатура SOLID была введена Мишелем Фезерсом.

Принципы SOLID не относятся только к PHP, они применимы к различным языкам OOPS. Это скорее принципы проектирования программного обеспечения. Это не функция, а способ мышления о том, как структурировать код, чтобы он был более удобным в обслуживании, понятным, гибким и легко расширяемым.

Почему именно SOLID Principles?

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

Цель принципов SOLID

  • Сделать код более легким для чтения и понимания
  • Чтобы облегчить быстрое расширение новой функциональности без нарушения существующей
  • Чтобы сделать код более расширяемым и тестируемым.

S - Принцип единой ответственности (SRP)

Принцип единой ответственности гласит: "Класс должен делать что-то одно, и поэтому у него должна быть только одна причина для изменений".

Пример 1

Итак, допустим, у нас есть класс SaleReports.php внутри App\Solid.

<?php

namespace App\Solid;

use DB;

class SaleReports {
  public function export() {
    $sales = DB::table('sales')
      ->latest()
      ->get();
    
    return 'CSV format';
  }
}

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

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

<?php

namespace App\Solid;

use DB;

class SaleReports {
  public function export($format) {
    $sales = DB::table('sales')
      ->latest()
      ->get();

    if ($format === 'pdf'){
      return 'PDF format'
    }
    
    return 'CSV format';
  }
}

Здесь мы нарушаем SRP, потому что в приведенном выше классе много вещей происходит в одном классе. Теперь давайте реализуем SRP на вышеуказанном классе.

<?php

namespace App\Solid;

class PdfExport
{
  public function export($data) {
    return 'PDF format';
  }
}
<?php

namespace App\Solid;

class CsvExport
{
  public function export($data) {
    return 'CSV format';
  }
}
<?php

namespace App\Solid;

use DB;

class SaleReports {
  public function export($format) {
    $sales = DB::table('sales')
      ->latest()
      ->get();
  }
}

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

При генерации PDF SaleReports мы можем использовать их следующим образом:

...
$salesReports = new SaleReports();
$pdfExport = new PdfExport();

return $pdfExport->export($salesReports);

, а если мы хотим сгенерировать CSV SaleReports и так далее.

...
$salesReports = new SaleReports();
$csvExport = new CsvExport();

return $csvExport->export($salesReports);

Пример 2

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

Мы будем использовать экосистему Laravel, где входом запроса будет контроллер. В обязанности контроллера входит:

  • Получение запроса;
  • Обработать запрос;
  • возвращает ответ на запрос.

Вот сниппет:

namespace App\Http\Controllers;

use DB;
use Illuminate\Foundation\Http\Request;
use App\Events\ChatMessage;

class MessagesController extends Controller {

    public function postMessage(Request $request) {
        $this->validate($request, [
            'user_id' => 'required|exists:users,id',
            'message' => 'required'
        ]);

        if ($this->getUserSpecificMessagesCount($data['message']) >= 5) {
            Log::alert('[User Alert] Flooding', $data)
        }

        $model = Message::create($request->all());
        broadcast(new ChatMessage($model));

        return response()->json(['message' => 'message created'], 201);
    }

    public function getUserSpecificMessagesCount(int $userId, string $message) {
        return DB::table('user_messages')->where([
            ['user_id', '=', $userId],
            ['message', '=', $message],
        ])->count();
    }

}

Давайте перечислим, что можно увидеть в сниппете MessagesController

  • Получение запроса;
  • Проверяем данные;
  • Проверить возможность флуда;
  • Создать новый регистр сообщения в базе данных;
  • Транслировать сообщение на некоторый канал;
  • Вернуть сообщение клиенту.

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

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

Используя команду php artisan make:request CreateMessageRequest, вы создадите класс FormRequest, который появится в папке/пространстве имен App\Http\Requests и будет нести УНИКАЛЬНУЮ ответственность за валидацию вашего запроса и ничего больше:

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateMessageRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'user_id' => 'required|exists:users,id',
            'message' => 'required'
        ];
    }
}

Теперь мы собираемся реализовать решение на нашем фрагменте выше, и оно должно выглядеть следующим образом:

namespace App\Http\Controllers;

use DB;
use App\Http\Requests\CreateMessageRequest;
use App\Events\ChatMessage;

class MessagesController extends Controller {

    public function postMessage(CreateMessageRequest $request) {
        $data = $request->validated();

        if ($this->getUserSpecificMessagesCount($data['message'])) {
            Log::alert('[User Alert] Flooding', $data)
        }

        $model = Message::create($data);
        broadcast(new ChatMessage($model));

        return response()->json(['message' => 'message created'], 201);
    }

    public function getUserSpecificMessagesCount(int $userId, string $message) {
        return DB::table('user_messages')->where([
            ['user_id', '=', $userId],
            ['message', '=', $message],
        ])->count();
    }

}

Итак, мы отделили валидацию нашей главной функции. Теперь нам нужно извлечь бизнес-правила на новый уровень абстракции, который известен как паттерн репозитория. Идея паттерна репозитория заключается в том, что у вас есть место для работы с методами/классами, которые взаимодействуют с базой данных, почтой и всем остальным, что вам нужно. Это буквально то место, куда вы помещаете всю бизнес-логику (если хотите).

Паттерн репозитория - это не последний слой абстракции, в реальности вы можете абстрагировать сколько угодно слоев, чтобы ваш код был более читабельным.
namespace App\Repositories;

use App\Models\Message;
use App\Events\ChatMessage;

class MessageRepository {

    private $model;

    public function __construct()
    {
        $this->model = new Message();
    }

    public function create(array $payload): bool
    {
        if ($this->checkFloodPossibility($data['message'])) {
            Log::alert('[User Alert] Flooding', $data)
        }

        $model = Message::create($payload);
        broadcast(new ChatMessage($model));

        return true;
    }

    public function getUserSpecificMessagesCount(int $userId, string $message) {
        return DB::table('user_messages')->where([
            ['user_id', '=', $userId],
            ['message', '=', $message],
        ])->count();
    }
}

После того, как мы создадим наше хранилище и бросим туда всю необходимую ответственность, у нас будет три способа вызвать его на нашем контроллере:

  1. Инстанцировать непосредственно внутри функции
$repository = new MessageRepository();

2. Инжектирование зависимости в конструктор класса

class MessagesController {
    public $repository;

    public function __construct(MessageRepository $repository)
    {
        $this->repository = $repository;
    }
}

3. Использование контейнеров Laravel

$repository = app(MessageRepository::class)->create();

В нашем коде мы будем использовать Dependency Injection, чтобы лучше видеть код. В частности, это тот способ, который имеет для меня больше смысла, поскольку мы должны поддерживать код как можно чище.

namespace App\Http\Controllers;

use App\Http\Requests\CreateMessageRequest;
use App\Repositories\MessageRepository;

class MessagesController extends Controller {
    private $repository;

    public function __construct(MessageRepository $repository)
    {
        $this->repository = $repository;
    }

    public function postMessage(CreateMessageRequest $request)
    {
        $data = $request->validated();
        $this->repository->create($data);

        return response()->json(['message' => 'message created'], 201);
    }
}

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

Далее >> O - Принцип Открытости-Закрытости (скоро будет)

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

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

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.