Принцип открытости/закрытости

Я должен сказать, что тот, кто придумал определения этих принципов, конечно, не думал о менее опытных разработчиках. То же самое происходит и с принципом “открыто-закрыто”, и те, до кого он дойдет быстрее, будут на шаг впереди по странности. 😂😂

Все статьи серии

SOLID принципы на примере Laravel

Как бы то ни было, давайте посмотрим на определение, найденное в сети для этого принципа: классы должны быть открыты для расширения, но закрыты для модификации. А?? Чтоо?? Вы что то поняли? Да, я тоже не особо, когда впервые столкнулся с этим, но со временем я пришел к пониманию: код, написанный один раз, не должен быть изменен.

Что вы меня грузите?..

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

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

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

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

Имея это в виду, давайте рассмотрим одну такую технику. Предположим, нам нужно добавить функциональность для преобразования данного HTML-контента (возможно, счета-фактуры?) в PDF-файл, а также запустить немедленную загрузку в браузере. Давайте также предположим, что у нас есть платная подписка на гипотетический сервис под названием MilkyWay, который будет делать фактическую генерацию PDF. Мы могли бы в конечном итоге написать метод контроллера, например вот:

class InvoiceController extends Controller {
    public function generatePDFDownload(Request $request) {
        $pdfGenerator = new MilkyWay();
        $pdfGenerator->apiKey = env('MILKY_WAY_API_KEY');
        $pdfGenerator->setContent($request->content); // HTML format
        $pdfFile = 
                $pdfGenerator->generateFile('invoice.pdf');

        return response()->download($pdfFile, [
            'Content-Type' => 'application/pdf',
        ]);
    }
}

Я пропустил проверку запроса и т. д., чтобы сосредоточиться на главном вопросе. Вы можете заметить, что мы воспользовались советами из предыдущего урока Принцип единой ответственности: SOLID принципы на примере Laravel. Мы переложили ответственность на сервис MilkyWay. Контроллер ничего не знает о входящей информации и какой формат файла генерируется и то, как он генерируется. В ответственности контроллера лишь передать данные сервису и отдать обработанные данные пользователю.

Но есть небольшая проблема.

Наш метод контроллера слишком зависит от класса MilkyWay. Если следующая версия API MilkyWay изменит интерфейс, наш метод перестанет работать. И если мы хотим когда-нибудь использовать какой-то другой сервис, нам придется буквально выполнить глобальный поиск в нашем редакторе кода и изменить все фрагменты кода, которые упоминают MilkyWay. И почему это плохо? Потому что это значительно увеличивает вероятность ошибки и является бременем для бизнеса (время разработчика тратится на разборку бардака).

Все это напрасно, потому что мы создали метод, который не был закрыт для изменения.

Можем ли мы сделать лучше?

Да, мы можем!

В этом случае мы можем воспользоваться практикой, которая идет примерно так – программируем интерфейсы, а не реализации.

Это говорит о том, что наш код должен зависеть от типов вещей, а не от самих конкретных вещей. В нашем случае нам нужно освободиться от необходимости зависеть от класса MilkyWay, а вместо этого зависеть от универсального, типа класса PDF (все это станет ясно через секунду).

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

С учетом этого давайте создадим интерфейс, который определяет, какие методы мы хотим, чтобы все наши классы PDF содержали:

interface PDFGenerator {
    public function setup(); // API keys, etc.
    public function setContent($content);
    public function generatePDF($fileName = null);
}

Итак, что же мы имеем здесь?

Через этот интерфейс мы говорим, что ожидаем, что все наши классы PDF будут иметь по крайней мере эти три метода. Теперь, если сервис, который мы хотим использовать (MilkyWay, в нашем случае), не следует этому интерфейсу, наша задача – написать класс (используя паттерн Адаптер), который это делает. Примерный набросок того, как мы могли бы написать класс – адаптер для нашего MilkyWay сервиса, выглядит следующим образом:

class MilkyWayPDFGenerator implements PDFGenerator {
    public function __construct() {
        $this->setup();
    }

    public function setup() {
        $this->generator = new MilkyWay();
        $this->generator->api_key = env('MILKY_WAY_API_KEY');
    }

    public function setContent($content) {
        $this->generator->setContent($content);
    }

    public function generatePDF($fileName) {
        return $this->generator->generateFile($fileName);
    }
}

И точно так же, всякий раз, когда у нас есть новый PDF-сервис, мы напишем для него класс-оболочку. В результате все эти классы будут считаться типовыми PDFGenerator.

Итак, как все это связано с принципом “открыто-закрыто” и Laravel?

Чтобы достичь этой точки, мы должны знать еще две ключевые концепции: привязки контейнеров Laravel и очень распространенный метод, называемый инъекцией зависимостей. Опять же, большие слова, но инъекция зависимостей просто означает, что вместо того, чтобы создавать объекты классов самостоятельно, вы упоминаете их в аргументах функций, и что-то автоматически создаст их для вас. Это освобождает вас от необходимости все время писать код, например, $account = new Account(); и делает код более тестируемым (эта тема для другого раза).

А пока просто думайте об этом как о чем-то, что может создавать для нас новые экземпляры классов. Давайте посмотрим, как это поможет.

В сервисном контейнере в нашем примере мы можем написать что-то вроде этого:

$this->app->bind('App\Interfaces\PDFGenerator', 'App\Services\PDF\MilkyWayPDFGenerator');

Это говорит нашему фреймворку, что всякий раз, когда какой-то класс для своего создания требует App\Interfaces\PDFGenerator, передайте ему экземпляр класса MilkyWayPDFGenerator. И после всех этих манипуляций, дамы и господа, мы подходим к тому моменту, когда все встает на свои места и принцип “открыто-закрыто” раскрывается в действии!

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

class InvoiceController extends Controller {
    public function generatePDFDownload(Request $request, PDFGenerator $generator) {
        $generator->setContent($request->content);
        $pdfFile = $generator->generatePDF('invoice.pdf');

        return response()->download($pdfFile, [
            'Content-Type' => 'application/pdf',
        ]);
    }
}

Заметили разницу?

Во-первых, мы получаем наш экземпляр класса PDF generator в аргументе функции. Это создается и передается нам сервисным контейнером, как мы уже обсуждали ранее. Код стал чище, и там нет упоминания о ключах API и т. д. Но, что самое главное, от MilkyWay класса не осталось и следа. Это также имеет дополнительное побочное преимущество, облегчающее чтение кода (кто-то, читающий его в первый раз, не будет говорить: “Вау! Что это такое MilkyWay??”).

Самая большая выгода из всех

Этот метод теперь закрыт для модификации и устойчив к изменениям. Позвольте мне объяснить. Предположим, завтра мы почувствуем, что сервис MilkyWay слишком дорогой (или, как это часто бывает, их клиентская поддержка стала дерьмовой); в результате мы опробовали другой сервис, называемый SilkyWay и хотим перейти к нему. Все, что нам теперь нужно сделать, это написать новый PDFGenerator класс-оболочку SilkyWay и изменить привязку в нашем коде контейнера службы:

$this->app->bind('App\Interfaces\PDFGenerator', 'App\Services\PDF\SilkyWayPDFGenerator');

Вот и все!!

Больше ничего менять не нужно, потому что наше приложение написано на интерфейсах (интерфейс PDFGenerator), а не на конкретных реализациях. Бизнес-требование изменилось, был добавлен новый код (класс оболочки), и только одна строка кода была изменена — все остальное осталось нетронутым, и вся команда может спокойно идти домой и спать спокойно.

Хотите спать спокойно? Следуйте принципу “открыто-закрыто”! 🤭😆

Автор: Анкуш Тхакур 9 ноября 2020 года

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

    Вау! Просто Вау! Именно из этой статьи я понял что это за принцип. Спасибо! Так держать.

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

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

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