Как-то в рабочей беседе один мой коллега-программист заметил, что всевозможные принципы и паттерны проектирования ПО хорошо применять, когда делаешь тестовые задания, однако в реальных, боевых проектах они как правило неприменимы. Почему так? Основных причин две:
-
Реализация принципов и паттернов требует слишком много времени.
-
Код становится тяжеловесным и сложным для понимания.
В серии статей На практике я попробую развеять эти предубеждения, продемонстрировав кейсы, реализующие принципы проектирования в практических задачах таким образом, чтобы этот код не был слишком сложным и на его написание уходило разумное количество времени. Вот первая статья из этой серии.
Отправная точка
Работаем над проектом на Yii2, в котором для доступа к
данным используетсяActiveRecord
. Клиентский код
загружает некий набор данных, используя
методActiveRecord::find()
.
classClientClass{ // Много кода... publicfunctionbuildQuery():ActiveQueryImplementation { $query=ActiveRecordModel::find(); // Получаем экземплярActiveQueryиз модели $query->active()->unfinished(); // Применяем условия, реализованные в конкретном классеActiveQuery return$query; // Далее результаты построенияActiveQueryиспользуются для получения выборки из БД, например $query->all(); } // Много кода...}
Этот код применяет к экземпляру,
реализовывающемуActiveQueryInterface
, фиксированный
набор условий и возвращает сконфигурированный таким образом
экземпляр для дальнейшего использования.
А если нужно добавить новые условия в запрос?
Придерживаясь традиционного подхода, мы добавим вызовы методов с новыми условиями прямо в строку, применяющую эти условия к запросу.
$query->active()->unfinished()->newConditionA()->newConditionB();
Вуаля! Пять секунд работы, и мы получили изящный, легко читаемый код.
Но что если эти условия нужны не во всех случаях, когда вызывается наш метод? Что если условия к запросу нужно применять динамически?
Тут нас ждут определенные трудности. Очевидно, что при таком подходе весь клиентский код, использующий наш метод, будет получать запрос, к которому уже применены новые условия. Почему так получилось? Потому что мы...
Нарушаем принцип открытости-закрытости
Напомню, что принцип открытости-закрытости SOLID гласит:
Код должен быть открытым для дополнения и закрытым для изменения.
Мыизмениликод нашего метода таким образом, что он начал выдаватьдругиерезультаты.
А как сделать правильно?
Чтобы соблюсти принцип открытости-закрытости, нам нужно было бы предусмотреть возможность в случае необходимостидополнятькод таким образом, чтобы добавлять в него новые условия, не внося изменений в логику работы метода по умолчанию и не меняя его исходный код.
Для этого изменим код нашего метода следующим образом.
classClientClass{ /** * @varActiveQueryFilter[] */ public$filters= []; // Добавляем возможность сконфигурировать экземпляр класса массивомфильтров // Много кода... publicfunctionbuildQuery():ActiveQueryImplementation { $query=ActiveRecordModel::find(); $query->active()->unfinished(); $this->applyFilters($query);// Применяем фильтры к запросуreturn$query; } privatefunctionapplyFilters(ActiveQueryImplementation&$query):void { foreach($this->filtersas$filter){ $filter->applyTo($query); } } // Много кода...}
Определим интерфейсActiveQueryFitler
предполагает
единственный метод applyTo()
, применяющий
дополнительные условия к запросу в качестве побочного эффекта.
interfaceActiveQueryFilter{ publicfunctionapplyTo(ActiveQuery$query):void;}
Теперь для добавления в запрос новых условий нам достаточно
добавить соответствующую реализацию
интерфейсаActiveQueryFilter
.
classNewConditionsAAndBFilterimplementsActiveQueryFilter{ publicfunctionapplyTo(ActiveQuery$query):void { $query->newCondtionA()->newConditionB(); }}
Что мы имеем в итоге
-
Мы решили стоящую перед нами задачу,дополнивпервоначальный метод всего одним вызовом (минимальное вмешательство в первоначальный код).
-
Поведение по умолчанию исходного метода не изменилось.
-
Вызываемый из исходного метода новый метод
applyFilters()
не реализовывает собственной логики вся логика делегируется классам фильтров. Таким образом, мы оставили неизменным и поведение всего исходного класса.