Замена условного оператора полиморфизмом

Понадобилось на днях сделать экспорт неких сущностей (заполненных протоколов обследования пациентов) в формат pdf. Но как оно всегда бывает: сегодня pdf, а завтра попросят docx, jpg, bmp и т.п.

Изначально накидал простой код “который работает”.

public function show(Protocol $protocol, Request $request)
    {
        if ($request->has('export')) {
            if ($request->get('export') === 'pdf') {

                $page = view('web.sections.protocols.show-pdf', compact('protocol'));
                $newFilePath = storage_path('pdf/protocol_'.$protocol->id.'.pdf');

                $client = new Client();
                $result = $client->post('http://pdf:3000/html', ['body'=>$page]);

                file_put_contents($newFilePath, $result->getBody()->getContents()); // save pdf

                return Response::download($newFilePath);
            }
        }

        return view('web.sections.protocols.show', compact('protocol'));
    }

Данный метод выводит в браузере заполненный протокол обследования. А если доктор нажал “экспортировать в pdf”, то к ссылке добавлялся параметр export=pdf и этот же метод генерировал мне нужный мне pdf.

Что в этом коде не так?

Ну начнем с того, что один метод выполняет две функции: показывает html страницу протокола и рендерит, а затем отправляет pdf файл в браузер пользователю. Давайте представим, что мы добавим в будущем сюда еще и рендер в docx и в jpg. Метод будет разрастаться очень сильно. Его будет сложно читать и поддерживать.

Что можно сделать с этим?

В первую очередь я разделил эту функциональность на два метода: show и export. Первый стал максимально простым:

public function show(Protocol $protocol) 
{
    return view('web.sections.protocols.show', compact('protocol'));
}

А со вторым мы немного повременим. Давайте вспомним, что такое полиморфизм?

Полиморфизм – это способность программы идентично использовать объекты с одинаковым интерфейсом без информации о конкретном типе этого объекта.

Ну например, у нас есть интерфейс Животные (Animals) и все они умеют Говорить (Speak). И конкретные реализации животных будут уметь одно и тоже – говорить, хоть и по разному это реализуя.

Вернемся к нашему экспорту. Предположим, что наш метод экспорта выглядит так:

if ($request->get('export') === 'pdf') {
     // 30 строк кода
}
if ($request->get('export') === 'xlsx') {
     // 50 строк кода
}
if ($request->get('export') === 'docx') {
     // 40 строк кода
}
if ($request->get('export') === 'jpg') {
     // 10 строк кода
}

Просто представьте как это сложно будет поддерживать через пол года. Наше решение – это замена условного оператора полиморфизмом. Давайте воспользуемся полиморфизмом и сделаем все много проще и лаконичнее.

Ну в первую очередь необходимо создать интерфейс, который будет описывать единое поведение для всех наших экспортеров. Он максимально простой:

interface ProtocolExportInterface
{
    public function run(Protocol $protocol): string;
}

На входе в метод рендера нам необходимо получать протокол, но а как я его буду обрабатывать нас не интересует. Главное, что на выходе я получу строку – путь к файлу.

Теперь давайте напишем реализацию pdf экспорта. В моем случае, я поднял в докере сервис на nodejs, который на вход получает html, а на выходе мы получаем готовый pdf файл.

namespace Src\Export;

use App\Models\Protocol;
use GuzzleHttp\Client;
use Src\Contracts\ProtocolExportInterface;

class PdfExportService implements ProtocolExportInterface
{
    private $client;
    private $serviceUrl;

    public function __construct(Client $client, string $serviceUrl)
    {
        $this->client = $client;
        $this->serviceUrl = $serviceUrl;
    }

    public function run(Protocol $protocol): string
    {
        $newFilePath = storage_path('pdf/protocol_' . $protocol->id . '.pdf');

        if (!file_exists($newFilePath)) {
            $page = view('web.sections.protocols.show-pdf', compact('protocol'));
            $result = $this->client->post($this->serviceUrl, ['body' => $page]);

            file_put_contents($newFilePath, $result->getBody()->getContents()); // save pdf
        }

        return $newFilePath;
    }
}

Затем в контроллере мы создаем приватный метод для выбора нужной реализации экспорта, в зависимости от типа, требуемого для экспорта:

public function getExportServiceByType(string $type): ProtocolExportInterface
    {
        switch($type)
        {
            case 'pdf':
                return app(ProtocolExportInterface::class);
            default:
                throw new \InvalidArgumentException('Invalid type export');
        }
    }

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

public function getExportServiceByType(string $type): ProtocolExportInterface
    {
        switch($type)
        {
            case 'pdf':
                return app(PdfExportService::class);
            case 'xlsx':
                return app(XlsxExportService::class);
            default:
                throw new \InvalidArgumentException('Invalid type export');
        }
    }

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

Советую почитать серию статей про SOLID принципы на примере Laravel

Что же получилось?

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

Убивает дублирование кода. Вы избавляетесь от множества почти одинаковых условных операторов.

Если вам потребуется добавить новый вариант выполнения, все, что придётся сделать, это добавить новый подкласс, не трогая существующий код (принцип открытости/закрытости).

Ну а что же с нашим методом экспорта в итоге? Он, как и метод show стал максимально простым и лаконичным.

public function export(Protocol $protocol, string $type)
    {
        $exportService = $this->getExportServiceByType($type);
        return Response::download($exportService->run($protocol));
    }

Надеюсь эта информация была для вас настолько же полезной как и для меня. Поделитесь с друзьями, давайте писать код красивым и чистым!

Рейтинг
( 4 оценки, среднее 5 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Комментарии: 1
  1. ян

    Скажу спасибо автору за наличие интересного материала на сайте и статей по фреймворку laravel .

    Внесу свои 5 копеек:
    – Если мы используем простую фабрику для генерации объектов по флагу (из массива) как в вашем случае по “типу” то окей switch case еще допустимо.

    В своей статье Вы пишите об использование полиморфизма – но при этом реализовываете его на половину – то есть немного не в том месте. Почему бы Вам не прокинуть уже готовый объект интерфейса ProtocolExportInterface в метод public function getExportService(ProtocolExportInterface $item):void и там уже дернуть метод run()? – это и будет полиморфизм.

    А вот простую фабрику как раз таки Вам нужно было бы использовать предварительно перед созданием объекта интерфейса ProtocolExportInterface исходя из типа.

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.