RSS
 

Паттерны проектирования в Drupal

Паттерны проектирования в Drupal
Larry Garfield (aka Crell)

"Хорошие программисты - ленивы", как говорит пословица. Это не значит, что хорошие программисты избегают выполнения своей работы; это значит, что они не любят делать работу, которую делать не нужно. Лучшим способом сделать это: не решать 2 отдельные задачи, а найти общий способ решения этих двух задач. Обычно это называется "повторным использованием кода", и это основа знаний любого человек, занимающегося разработкой софта.

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

Паттерн проектирования - не исполняемый код; паттерн проектирования - идея, которую можно применить для заданного набора задач. Пусть у Вас есть набор значений и Вам нужно применить одинаковые преобразования к каждому из них. Если Вашим первым порывом было: "Ага, мне нужен массив и итератор по нему, например foreach()", то Вы на верном пути. Паттерны проектирования ПО - это одинаковая идея при различной реализации. Понимание общих паттернов может сэкономить время по нахождению решения, также как и предложить лучшее решение, чем то, до которого Вы бы додумались сами.

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

Хотя, большинство паттернов описаны в рамках ООП, идеи, стоящие за ними - довольно универсальны. Drupal вообще говоря использует некоторые распространенные паттерны, а также использует некоторые свои идиомы. Давайте посмотрим на некотрые паттерны в Drupal 7, и как мы можем использовать те же подходы в нашем собственном коде.

Наблюдатель (Observer), посетитель (visitor) и peeping toms

Наблюдатель и посетитель - наиболее обсуждаемые паттерны и очень важны. Мы упоминаем их вместе, т.к. идейно они очень похожи.

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

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

class Subject implements Observable {
  protected $observers = array();
  public function addObserver(Observer $o) {
    $observers[] = $o;
  }
  public function sayHello() {
    foreach ($this->observers as $o) {
      $o->speak();
    }
  }
}
 
class France implements Observer {
  public function speak() {
    print "Bonjour!";
  }
}
 
$s = new Subject();
$s->addObserver(new France());
$s->sayHello(); // prints "Bonjour!"

При таком подходе мы можем добавить неограниченное количество действий, который выполнятся при вызове метода sayHello(). Это и есть суть событийного программирования; когда что-то случается - это автоматически провоцирует некоторый набор действий, которые можно задать без изменения всей системы.

Паттерн посетитель реализует похожую идею, но используется для расширения возможностей субъекта, нежели простого реагирования на события. По существу Вы передаете объекту-посетителю "субъект" и говорит "выполни это сам". В самом простом случае мы бы взяли пример выше и передали бы $this в объект France, который в свою очередь бы вызвал методы субъекта для изменения его состояния. Посетитель особенно используется в языках без множественного наследования (таких как PHP и Java) для добавления функционала к объекту в runtime режиме.

Drupal вообще говоря построен на этих близнецах-паттернах: наблюдателе и посетителе, хотя по имени они в нем и не встречаются. Вместо этого они названы хуками. hook_node_load(), hook_user_login(), и т.д. являются наблюдателями над нодами и пользователями. hook_form_alter(), hook_node_view(), и т.д. - посетители. Т.к. Drupal не делает различий между 2-мя этими паттернами, то некоторые хуки можно отнести к обоим паттернам, но это не меняет сути. Вместо активной регистрации, которая показана выше, Drupal полагается на магическое именование функций для "регистрации" наблюдателя и посетителя. В этом есть смысл в PHP где в противном случае нам бы приходилось делать "перерегистрацию" каждого хука на каждую загрузку страницы.

Фабрики (factory) и команды (command)

Слой БД в Drupal 7 реализует некоторое число общих паттернов проектирование в более традиционном виде, т.к. бОльшей частью это объектно-ориентированная подсистема. Две из них в особенности стоит упомянуть, т.к. они служат примером того насколько сложные подсистемы Drupal-а можно упростить с помощью фабрик и объектов-команд.

Есть несколько вариаций паттерна Фабрика, но все они крутятся вокруг одной и той же идеи: один объект - клиент просит другой объект - фабрику о подходящей реализации куска логики, но не беспокоится о самой реализации. Об этом заботится фабрика. Рассмотрим функцию db_insert(), важные части которой показаны ниже:

function db_insert($table, array $options = array()) {
  // ...
  return Database::getConnection($options['target'])->insert($table, $options);
}

Метод Database::getConnection() - фабрика, которая возвращает объект подсоединения к БД валидный для данного сайта. Есть много факторов для определения какой объект возвращать, такие как: работаем ли мы с MySQL или SQLite, или же есть ли у нас slave server. Как функции, которая делает вызов - нам безразлично. Нам просто нужен "правильный объект соединения", который фабрика создает для нас. (Вообще говоря она хранит индекс объектов и просто возвращает нам нужный, но все же довольно близко. Это ведь паттерн все же, так ведь?)

Вообще говоря тут у нас есть вторая фабрика. Метод insert() объекта соединения не выполняет запрос на вставку. Он создает полностью новый объект InsertQuery и возвращает его нам. Вообще-то он может вернуть нам экземпляр InsertQuery_mysql или InsertQuery_pgsql или любого другого потомка InsertQuery; И снова мы не беспокоимся о том, что нам вернет фабрика. Это отличный способ для создания расширяемых систем; Пока все объекты, которые мы можем получить имеют одинаковый интерфейс (в идеале определенный языковыми конструкциями в случае PHP), мы не беспокоимся о том какой это класс или какая у него логика внутри. Фабрики также заботятся за нас о конфигурации сложных объектов, сильно улучшая юзабилити API.

InsertQuery сам по себе пример паттерна Command. В этом паттерне процесс "создания чего-то" обернут в объект. После мы создаем новый экземпляр этого объекта, конфигурируем его и выполняем, при этом срабатывает действие (action).

Почему просто не выполнить само дейсвие, спросите Вы? На то есть много причин. Одна из них: если "настройка" сложна - может быть значительно легче определить "действие" как набор вызовов, котрые валидитруют инструкции конфигурации, нежели передавать дюжину параметров в функцию, или определить огромный, труднодокументируемый и трудноотлаживаемый массив в надежде, что мы не опечатались где-либо. Это как раз тот случай, когда InsertQuery может, в зависимости от обстоятельств, вставить один или несколько строк в таблицу и существует несколько путей с помощтю которых мы можем определить какие строки вставлять. Юзабилити для разработчика - намного лучше в виде command object, чем большогомассива.

Другим преимуществом является гибкость. Мы можем создать набор объектов-команд без их выполнения и сохранить их на будущее, или мы можем позволить другой системе изменить команды перед ее запууском в помощью паттернов наблюдателя или посетителя. По факту, объект SelectQuery, еще один объект-команда, делает это в точности: он передает запрос в хук-посетитель hook_query_alter() и позволяет другим системам изменить его перед выполнением.

Другим преимуществом объекта-команды, которого на данный момент нет в Drupal - инкапсуляция поведения. Даже после запуска у нас есть возможность писать все-что угодно вовнутрь объекта. Один из распространненых примеров: операция отмены последнего действия. Объект команда знает в точности какие действия были предприняты и, таким образом, какие шаги нужно отменить. Это делает команду отмены довольно простой: убедитесь, что все связанные действия (например SQL запросы) - контролируются объектом-командой, запишите в объекте как их обратить (если это возможно), и потом держите этот объект при себе для вызова $command->undo() если нужно.

Доктор "инъекция зависимости" (dependency injection) в Drupal

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

На самом простом уровне - это простое правильное использование параметров функции. Если функции нужно знать ID ноды, над которой производить операцию - она должна получить это пераметр в качестве аргумента, а не из какого-то хитрого места вовне. Это делает код значительно более гибким.

Как пример, в старые плохие времена Drupal 4.6 способ которым бы мы создали callback страницы:

function example_page() {
  if (arg(0) == 'node' && is_numeric(arg(1)) {
    $node = node_load(arg(1));
    if (arg(2) == 'example') {
        // Do useful stuff here.
      }
  }
}

Это пример частного антипаттерна в Drupal: arg() (правильно произносится "ARGH!"). Легко написать, но такой код ужасно хрупок. Он делает эту функцию полезной ровно в 1 месте: node/$nid/example. Проблема здесь в том, что функция аквтино получает данные из глобального состояния. Глобальное пространство имен - ужасная идея, т.к. она делает невозможным инкапсулировать части системы и исползовать их повторно.

Если, например, мы хотели бы использовать повторно описанный callback по другому пути, единственным способом сделать это было бы манипулирование вручную масивом $_GET, чтобы претвориться будто бы мы находимся по адресу node/$nid/example и надеяться, что ничего больше не сломается, как результат. Хорошо, если Вы поняли насколько плох этот антипаттерн.

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

function example_menu() {
  $items['node/%node/example'] = array(
    'page callback' => 'example_page',
    'page arguments' => array(1),
    // ...
  );
  return $items;
}
 
function example_page($node) {
  // Do useful stuff here.
}

В таком новом варианте роутер меню становится более сложным. Однако, callback страницы получает ноду, от которой он зависит и которая передается ему. Это дает нам ряд преимуществ. Во-первых мы можем теперь разместить example_page() по новому пути, или же отображать по нескольким адресам, без необходимости менять функцию. Такой код легче читается. Также теперь стало возможным применять юнит-тесты, т.к. они должны отличаться лишь объектом $node который в них передается. Это означает, что мы можем передать туда "поддельную" ноду, чтобы проверить правильность работы.

Однако это очень простой пример. А что если example_page() зависит от некоторого набора других значений помимо ноды? Что если значения, от которых зависит функция - сами зависят от значений объекта $node?

Тут уже инъекция зависимостей может стать сложной. Есть несколько путей для обработки более сложных случаев. Первый - переключиться с простых функций на объекты. Объекты обладают значительно большим количеством способов для получения (передачи в них) нужной информации. Мы можем задать значение с помощью конструктора или метода setX() какого-то вида. И если есть довольно много зависимых объектов или значений - мы можем упростить процесс, используя Фабрику, чтобы она сделала это за нас.

Возвращаясь к слою БД, это в точности, что он делает. Ранее мы создали новый объект InsertQuery. Этому объекту нужен объект соединения, которого он попросит выполнить запрос. Метод insert() объекта соединения предоставляет такой объект без необходимости волноваться. Выглядит это следующим образом:

public function insert($table, array $options = array()) {
  $class = $this->getDriverClass('InsertQuery', array('query.inc'));
  return new $class($this, $table, $options);
}

Первая строка - логика фабрики, которая определяет какую версию класса InsertQuery мы будем использовать. Вторая строка создает новый объект запроса, и добавляет в него всю информацию, которая ему будет нужна: объект соединения ($this), таблицу в которую будет происходить вставка, и любые опции, зависящие от пользователя. В большинстве случаев вся эта работа выполняется фабрикой, так что пазработчикам модулей нужно лишь вызвать

db_insert('mytable')
  ->fields(array('id' => 1, 'name' => 'Example'))
  ->execute();

и все просто работает.

Брокер (broker)

Что же делать, если мы наперед не знаем какая информация нам понадобится или если она будет различаться? Это делает инъекцию зависимостей сложнее. На данный момент у Друпала нет хороших решений этой проблемы. У разных фреймворков, на PHP и не только, есть некоторый подход к решению этой проблемы, часто называемый контейнером инъекции зависимостей. Они могут отличаться от простых массивов объектов до сложной взаимоозависимой каши, которая зависит от реализации. В Java, довольно распространено иметь XML-конфигурацию для зависимых контекстных объектов.

Один общий подход - некоторая вариация Брокера или Посредника. В таком варианте один объект не запрашивает информацию напрямую у другого объекта. Вместо этого он просит некоторый промежуточный объект, который мог быть "вставлен" (injected) в него, сделать такой запрос на его стороне. Хотя это все еще значит, что объект должен активно запрашивать информацию, все же он тесно привязан лишь к этому объекту-посреднику, а не мириад других объектов, которых ему может оказаться нужным опросить. Это значит, что если те другие системы изменятся - нам нужно будет обновить только посредника, но не каждую систему, связанную с измененной.

Паттерны, не код

Помните, что паттерны проектирования - общий словарь/запас опробованных-и-оттестированных идей. Сами по себе они не являются кодом, хотя часто они предлагают некую колею по написанию кода. Традиционная реализация Наблюдателя на классических языках (Java, PHP, C++, и т.д.) будет выглядеть очень похоже, даже если у них будут небольшие различия.

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

Также это дает нам основу для понимания чужого кода, который мы никогда раньше не видели. Если Вы смотрите на слой БД в Drupal 7 впервые - он может показаться беспорядочным набором объектов, которые дергают друг друга без всякой нужды. Для чего такие сложности?

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

оригинал статьи: http://drupalwatchdog.com/1/1/design-patterns-of-drupal