Декомпозиция и декорация

Приветствуем!

Хотите что-то написать?

Нужно назвать себя.

Если вы пришли в первый раз,
то нужно зарегистрироваться.

Читайте нас в:

Блог на ya.ru
Блог на Деловом квартале
Блог на Twitter.om
Блог на Livejournal.com
19 марта

Базы данныхДекомпозиция и декорация

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

Что делать и как жить? Об этом на примере symfony и propel в этой статьей.

Возьмем к примеру абстрактную таблицу с товаром, которая сейчас выглядит вот так:

# название поля комментарий
1 id автоинкремент обыкновенный
2 parent_id родительский каталог
3 title название товара
4 price стоимость товара
5 price_sale стоимость товара со скидкой
6 price_somecurrency стоимость товара в какой-нибудь валюте
7 price_currency_sale стоимость товара со скидкой в какой-нибудь другой валют
8 publish флаг, указывающий опубликован ли товар
9 description краткое описание товара
10 body полное описание товара
11 image изображение товара
12 icon небольшая пиктограммка (иконка)
13 image_thumb небольшое изображение товара (для списка товаров, например)
14 status какой-нибудь статусь, например о наличии
15 rank какой-нибудь статусь, например о наличии
16 article статья о товаре (не описание, отдельная статья, на отдельной странице)
17 article_image описание статьи

Вот так ужастик!

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

Ну чтож, поехали!

Это основная таблица с товаром:

item
# название поля
1 id
2 parent_id
3 title
4 publish
5 rank
6 status

Описание тоже выделим, пожалуй, нечего болтаться в общей таблице:

item_description
# название поля
1 item_id
2 description
3 body

Статью с изображением прикрепленной к ней просто нужно полюбому выносить. :

item_article
# название поля
1 item_id
2 body
2 image

Даже иконку выносим отдельно:

item_icon
# название поля
1 item_id
2 filename

Изображение выносим теперь:

item_image
# название поля
1 item_id
2 filename
3 filename_thumb

Валюты не только вынесем, но и сделаем оптимизацию:

item_price
# название поля
1 id
2 item_id
3 currency_type
4 price
5 price_type

Вроде бы ничего не забыл. Теперь резюме всех наших действий.

  • Таблица заметно похудела, но самих таблиц стало гораздо больше, что может вызвать сложности при работе с ними. Об оптимизации процесса — позже
  • Архитектура стала понятней, видно за что отвечает каждая бизнес-еденица всей это модели, при этом отпала нужна выдумывать имена полям, нужно задуматься о таблицах.
  • Все сопряженный таблицы не имеют главного id. Автоинкремент это лишнее, использовать его стоит только в том случае, если есть ограничения в Вашей ORM, например.

    В данном случае item_id становится уникальным ключем, а точнее primary_key

    Хотя опять же дело вкуса, самое главное — чтобы была целостность, а то есть ли id. В таблице item_price сделать уникальным этот ключ не получится.

  • Таблицу item_article можно разбить на 2 части: item_article и item_article_image, хотя тоже уже дело вкуса. Делать такое лучше, когда изображений будет несколько.
  • Изображения стали собственной еденицей. Теперь галерейку забабахать — как 2 пальца. При этом тянуться из базы будет не так уж много информации и задумывать над вопрос «вот тут мы это полем выбираем а тут нет» — не надо. Головной боли меньше.

    Если thumbnail'ов нужно будет несколько — выделяем их отдельной таблицей item_image_thumb

    С иконками история похожа.

  • Тяжеленное описание теперь не будет тащиться при каждом запросе, где вы забыли исключить его из выборки. Если правильно подойти к вопросу администрирования — в админке вы сможете сделать удобный полезный инструмент для быстрого редактирования без потери скорости.
  • Цены стали волшебными. Теперь наличие для вас любой валюты в системе — не такая уж и помеха. Мало того — про тип цены вы тоже можете забыть, по идее. Если у вас появится помимо цены со скидкой, например, цена на крупный опт — всё упирается только в добавлением в поле price_type ещё одного вида стоимости (предполагается, что поле будет enum)

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

Говоря словами педивикии:

In object-oriented programming, the decorator pattern is a design pattern that allows new/additional behaviour to be added to an existing object dynamically.

Говоря по нашему:

Декоратор расширяет основной класс за счет других классов, которые подключаются динамически.

В нашем случае ведущей еденицей, которой мы должны пользоваться — это, безусловно, таблица item

В ORM будет присвоен ей класс Item, который мы и будем расширять.

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

Начнем с простого. Наша задача — подключить все релятивные модели таблиц к основной, т.е. декорировать item всеми таблицами, которые несут в себе информацию связанную с ней.

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

  private $item_description_decoration = null;

  private $item_article_decoration = null;

  private $item_icon_decoration = null;

  private $item_image_decoration = null;

  private $item_price_decoration = array();

Хранить такое лучше как private и давать доступ через функции, почему — объясню далее.

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

А вот теперь почему private. Если оставить как public — можно нарваться на неприятность в виде доп. проверок, что, допустим, будет лишним в php-шаблоне c html-кодом. Обращение к декораторам сделаем через public-методы. Все не будем указывать, а возьмем за пример одну декорацию.

  public function getDescription()

  {

    return $this->item_description_decoration;

  }

Стоп. Мы ведь теперь сделали тоже самое, если бы открыли сам параметр $item_description_decoration класса.

Не тут то было. Кто нам не дает туда затолкать проверку?

  public function getDescription()

  {

    if ($this->isDecoratedWithDescription()) {

      return $this->item_description_decoration;

    } else {

      return new ItemDescription();

    }

  }

Новая функция проверки, может выглядеть так:

  public function isDecoratedWithDescription()

  {

    return $this->item_description_decoration? true: false;

  }

Может так:

  public function isDecoratedWithDescription()

  {

    if (is_object($this->item_description_decoration)) {

      return (get_class($this->item_description_decoration) == ‘ItemDescription’)? true: false;

    } else {

      return false;

    }

  }

А вот и ещё способ:

  public function isDecoratedWithDescription()

  {

    if (is_object($this->item_description_decoration)) {

      if (get_class($this->item_description_decoration) != ‘ItemDescription’) {

        throw new Exception(‘Опс. Декорация не того немного класс’);

      }

      return true;

    } else {

      return false;

    }

  }

Что вы примените — зависит лишь от того, какой уровень управления вам нужен. Первый способ самый простой и если вы в себе уверены — можно применять его свободно.

Можно вообще сделать 3-в-1 и просто напросто управлять уровнем управления через флаг, который будет в параметрах. Тут повод пофантазировать.

Отдавать просто пустой объект-декорацию — не совсем верный способ. Поэтому расширим функцию getDescription

  protected function getDescription()

  {

    if (!$this->isDecoratedWithDescription()) {

      $this->decorateWithDescription($this->getDescriptionDecoration());

    }

    return $this->item_description_decoration;

  }

Появились аж 2 новые функции. Опишем их.

getDescriptionDecoration будет пытаться получать декорацию или же создавать пустую.

  public function getDescriptionDecoration()

  {

    $item_description = ItemDescriptionPeer::doSelectByItemId($this->getId());

    if (!$item_description) {

      $item_description = new ItemDescription();

    }

    return $item_description();

  }

ItemDescriptionPeer::doSelectByItemId — не будем вдавать в подробности. Там самое обычное формирование критерии и получение объекта ItemDescription

Функция декорирования decorateWithDescription:

  public function decorateWithDescription(ItemDescription $item_description)

  {

    $this->item_description_decoration = $item_description;

  }

Вот уже почти и всё. Само собой последнюю функцию можно расширить, ведь одним ItemDescription может не ограничиться, могут быть дочерние классы, поэтому проверку, возможно, лучше делать внутри и выкидывать исключение — например проверка на основной и родительский класс. Не совпали? На те эксепшн с…!

Использовать это без дополнительных махинаци невозможно. Представьте, что вы сделали выборку на 20 товаров, к ним к каждому 5 декораций, это ещё 20*5 запросов, если обращаться к каждой декорации. Ужас тихий! Не обещаю всё решить в один запрос (по крайней мере не через Propel), но вот сократить до 6 запросов — не проблема!

Но об этом через несколько дней :)

Коментарии:

ringtail 2010-03-19 13:22:33

А почему с Propel нельзя сократить это дело до одного запроса? Придется, конечно, писать огромную ужасную функцию, где ручками гидрируются результаты пяти джойнов, но в принципе же это реально)

ответить
maddogg 2010-03-19 13:28:41

не спорю что реально. Вообще лучший выход — сделать view'шку прямо в mysql и гидрировать её

ответить
fruit.dev 2010-03-20 15:30:47

(ИМХО) или как альтернативный вариант — пересесть на Doctrine-у ;>

ответить
maddogg 2010-03-22 10:36:01

:) да мы с нее удачно слезли

ответить
fruit.dev 2010-03-20 15:33:33

на тему читаемости кода в статье — не думали использовать GeSHi? (есть готовый плагин в Symfony)

ответить
maddogg 2010-03-22 10:36:27

подумаем над этим, до блога надо чтобы руки дошли

ответить

Свой комментарий: