Декомпозиция и декорация
Приветствуем!
Хотите что-то написать?Нужно назвать себя.
Если вы пришли в первый раз,
то нужно зарегистрироваться.
Базы данных → Декомпозиция и декорация
В жизни любого программиста при проектировании достаточно сложных систем может наступить критический момент — таблица, рассказывающая нам об одной бизнес-еденице, разрастается до таких критических размеров, что ставноится страшно на неё смотреть и в огромно количестве полей таблицы тяжело уже уловить нужное. Или может вам в наследство досталась такая ужасная архитектура от дедушки-программиста и надо с ней работать.
Что делать и как жить? Об этом на примере 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 запросов — не проблема!
Но об этом через несколько дней :)
Коментарии:
А почему с Propel нельзя сократить это дело до одного запроса? Придется, конечно, писать огромную ужасную функцию, где ручками гидрируются результаты пяти джойнов, но в принципе же это реально)
ответитьне спорю что реально. Вообще лучший выход — сделать view'шку прямо в mysql и гидрировать её
ответитьна тему читаемости кода в статье — не думали использовать GeSHi? (есть готовый плагин в Symfony)
ответить



