SOLID на практике. Принцип открытости-закрытости и ActiveQuery Yii2

от автора

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

  • Реализация принципов и паттернов требует слишком много времени. 

  • Код становится тяжеловесным и сложным для понимания. 

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

Отправная точка

Работаем над проектом на Yii2, в котором для доступа к данным используется ActiveRecord. Клиентский код загружает некий набор данных, используя метод ActiveRecord::find()

class ClientClass   {       // Много кода...       public function buildQuery(): ActiveQueryImplementation      {          $query = ActiveRecordModel::find(); // Получаем экземпляр ActiveQuery из модели          $query->active()->unfinished(); // Применяем условия, реализованные в конкретном классе ActiveQuery                   return $query; // Далее результаты построения ActiveQuery используются для получения выборки из БД, например $query->all();      }       // Много кода...   } 

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

А если нужно добавить новые условия в запрос?

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

$query->active()->unfinished()->newConditionA()->newConditionB(); 

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

Но что если эти условия нужны не во всех случаях, когда вызывается наш метод? Что если условия к запросу нужно применять динамически? 

Тут нас ждут определенные трудности. Очевидно, что при таком подходе весь клиентский код, использующий наш метод, будет получать запрос, к которому уже применены новые условия. Почему так получилось? Потому что мы… 

Нарушаем принцип открытости-закрытости

 Напомню, что принцип открытости-закрытости SOLID гласит: 

Код должен быть открытым для дополнения и закрытым для изменения. 

Мы изменили код нашего метода таким образом, что он начал выдавать другие результаты.  

А как сделать правильно? 

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

Для этого изменим код нашего метода следующим образом. 

class ClientClass   {      /**       * @var ActiveQueryFilter[]       */      public $filters = []; // Добавляем возможность сконфигурировать экземпляр класса массивом фильтров       // Много кода...       public function buildQuery(): ActiveQueryImplementation     {          $query = ActiveRecordModel::find();          $query->active()->unfinished();            $this->applyFilters($query); // Применяем фильтры к запросу   				return $query;      }       private function applyFilters(ActiveQueryImplementation &$query): void       {          foreach ($this->filters as $filter) {              $filter->applyTo($query);          }      }       // Много кода...   } 

Определим интерфейс ActiveQueryFitler предполагает единственный метод applyTo(), применяющий дополнительные условия к запросу в качестве побочного эффекта. 

interface ActiveQueryFilter  {      public function applyTo(ActiveQuery $query): void;  } 

Теперь для добавления в запрос новых условий нам достаточно добавить соответствующую реализацию интерфейса ActiveQueryFilter

class NewConditionsAAndBFilter implements ActiveQueryFilter  {      public function applyTo(ActiveQuery $query): void       {          $query->newCondtionA()->newConditionB();      }  } 

Что мы имеем в итоге 

  • Мы решили стоящую перед нами задачу, дополнив первоначальный метод всего одним вызовом (минимальное вмешательство в первоначальный код). 

  • Поведение по умолчанию исходного метода не изменилось. 

  • Вызываемый из исходного метода новый метод applyFilters() не реализовывает собственной логики — вся логика делегируется классам фильтров. Таким образом, мы оставили неизменным и поведение всего исходного класса. 

ссылка на оригинал статьи https://habr.com/ru/post/531186/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *