AdSense - шапка

воскресенье, 20 ноября 2011 г.

PHP. Паттерн Abstract Factory(Абстрактная фабрика).

Общие сведения о паттерне Абстрактная фабрика

Итак, я начинаю цикл статей, посвященных использованию паттернов проектирования при разработке web-приложений на PHP.

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

К плюсам паттерна относят:
  1. изоляция конкретных классов
  2. упрощение замены семейств продуктов
  3. гарантированная сочетаемость продуктов
Минус только 1:
  1. сложно добавить поддержку нового вида продуктов.
UML диаграмма паттерна Абстрактная фабрика


Если взглянуть на UML диаграмму паттерна, то минус становится очевидным. Если в системе присутствует 10 Конкретных фабрик, то, чтобы добавить новый продукт, придется обновить 10 классов фабрик и написать новые 10 классов продуктов.

На википедии есть пример реализации паттерна абстрактная фабрика на языке PHP. В нём описан подход к созданию GUI(графического интерфейса пользователя). Но PHP не предназначен для решения задач подобного рода. Это, прежде всего, язык разработки web-приложений. Поэтому, давайте рассмотрим возможности для активного использования паттерна в web - приложениях.

Применимость в web-приложенияx

1. Настраиваемые компоненты системы
Например, в php - фреймворке symfony можно в отдельном конфигурационном файле задать, какой класс следует использовать при создании какого либо компонента системы. Например, можно переопределить класс хранилища сессии. По-умолчанию, сессия лежит в обычном массиве $_SESSION. Можно создать свой класс, который, например, хранит данные сессии в базе данных, и прописать этот класс в настройки фреймворка.
Можно использовать подобный подход, чтобы придать создаваемой вами системы большую гибкость и настраиваемость (данный пример будет рассмотрен более детально ниже).

2. Разработка собственной ORM
Допустим, необходимо реализовать ORM, которая будет поддерживать различные СУБД - MySQL, Oracle и т.д.
Для решения это задачи можно разработать интерфейс фабрики, которая будет создавать все необходимые компоненты системы:

  • DBConnection - соединение с базой
  • DBRecrord - запись таблицы базы данных
  • DBQueryBuiler - конструктор запросов к базе
  • и т.д.
А также написать фабрики, реализующие этот интерфейс (MySQLFactory, OracleFactory). При этом, каждая фабрика возвращает объекты, специфичные для каждой СУБД (MySQLDBConnection, MySQLDBRecrord, MySQLQueryBuilder).
В результате, получаем ряд преимуществ:
  1. Можно абстрагироваться от конкретной СУБД, ведь всё взаимодействие с базой данных будет инкапсулировано внутри продуктов конкретных фабрик.
  2. Можно настраивать и динамически менять базу данных, с которой будет работать ваше приложение.
Пример с настраиваемыми компонентами системы
А теперь рассмотрим небольшой пример системы, компоненты которой можно настраивать, например, во внешнем конфигурационном файле и, тем самым, изменять её поведения.

 /**
  * Описание компонентов, присутствующих в системе по-умолчанию.
  * Обычно конфигурацию выносят в отдельный yml или xml файл. Но, для простоты,
  * обойдёмся обыкновенным массивом.
  */
 $g_defaultComponentSettings = array(
  'layout' => array(
   'class' => 'Layout'
  ),
  'content_container' => array(
   'class' => 'ContentContainer'
  ),
  'top_menu' => array(
   'class' => 'TopMenu'
  ),
 );

 /**
  * Описание нестандартных компонентов системы
  * или переопределения двефолтовых компонентов. 
  */
 $g_componentSettings = array(
  'top_menu' => array(
   'class' => 'MyTopMenu'
  ),
 );
 
 class TopMenu
 {
  public function render()
  {
   return '  [top_menu]It\'s a top menu[/top_menu]';
  }
 }
 
 class ContentContainer
 {
  public function render()
  {
   return '  [content]Content[/content]';
  }
 }
 
 class MyTopMenu extends TopMenu
 {
  public function render()
  {
   return "  [decorated_top_menu]\n  " . parent::render() . "\n  [/decorated_top_menu]";
  }
 }
 
 /**
  * Класс представляет собой общую разметку страницы.
  */
 class Layout
 {
  private $contentContainer = null;
  private $topMenu = null;
  
  public function setContentContainer($contentContainer)
  {
   $this->contentContainer = $contentContainer;
  }
  
  public function setTopMenu($topMenu)
  {
   $this->topMenu = $topMenu;
  }
  
  /**
   * Печатает страницу на экран.
   */
  public function display()
  {
   echo "[layout]\n";
   echo $this->topMenu->render() . "\n";
   echo $this->contentContainer->render() . "\n";
   echo "[/layout]\n";
  }
 }
 
 /**
  * Фабрика компонентов системы.
  */
 class ComponentFactory
 {
  private $defaultComponentSettings = null;
  private $componentSettings = null;

  public function __construct(array $defaultComponentSettings, array $componentSettings = array())
  {
   $this->defaultComponentSettings = $defaultComponentSettings;
   $this->componentSettings = $componentSettings;
  }
  
  /**
   * Создаёт компонент по его имени
   * @param string $componentName
   * @return object
   */
  public function createComponent($componentName)
  {
   $componentClassName = "";
   if (isset($this->componentSettings[$componentName]))
   {
    $componentClassName = $this->componentSettings[$componentName]['class']; 
   }
   else if (isset($this->defaultComponentSettings[$componentName]))
   {
    $componentClassName = $this->defaultComponentSettings[$componentName]['class']; 
   }
   if (empty($componentClassName))
   {
    throw new Exception("Component '$componentName' not found.");
   }
   return new $componentClassName();
  }
 }
 
 /**
  * Пример 1. Используем только дефолтную конфигурацию фабрики.
  */
 $factory = new ComponentFactory($g_defaultComponentSettings);
 $layout  = $factory->createComponent('layout');
 $topMenu = $factory->createComponent('top_menu');
 $content = $factory->createComponent('content_container');

 $layout->setContentContainer($content);
 $layout->setTopMenu($topMenu);
 $layout->display();
 
 /**
     На выходе получим:

  [layout]
    [top_menu]It's a top menu[/top_menu]
    [content]Content[/content]
  [/layout]
 */
 
 /**
  * Пример 2. Используем дополнительные настройки фабрики.
  */
 $factory = new ComponentFactory($g_defaultComponentSettings, $g_componentSettings);
 $layout  = $factory->createComponent('layout');
 $topMenu = $factory->createComponent('top_menu');
 $content = $factory->createComponent('content_container');
 
 $layout->setContentContainer($content);
 $layout->setTopMenu($topMenu);
 $layout->display();
 
 /**
  На выходе получим:
 
  [layout]
    [decorated_top_menu]
      [top_menu]It's a top menu[/top_menu]
    [/decorated_top_menu]
    [content]Content[/content]
  [/layout]
 */
В результате, у нас получилась хорошо настраиваемая система. У неё есть дефолтовые компоненты, а, при необходимости, их можно переопределить и, тем самым, изменить поведение системы.

10 комментариев:

  1. Мне понравилось, что Вы привели реальные примеры применимости паттерна в отличие от многих статтей в инете. Реализация функционала для разных СУБД - это гораздо конструктивнее, чем пример с зоопарком в другом источнике. Мы всё-таки PHP-программисты, а не ветеринары. Спасибо.

    ОтветитьУдалить
    Ответы
    1. Рад, что вам понравилось) Буду дальше стараться

      Удалить
  2. Спасибо за статью, очень полезная.

    ОтветитьУдалить
  3. В шпаргалки! Однозначно!

    Автору спасибо!

    ОтветитьУдалить
  4. Спасибо!
    Хороший пример)

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

    ОтветитьУдалить
  6. В createComponent() вы кидаете Exception, но в коде вы создаете компоненты без try? почему?

    ОтветитьУдалить
  7. Спасибо за статью!
    Только по-моему в примере всё-таки паттерн Фабрика, а не Абстрактная Фабрика?

    ОтветитьУдалить