Архитектура приложений. DDD(Domain Driven Design), MongoDB, PHP, скрипты.
10 марта, 2013
Размышления о применимости DDD (Domain Driven Design) для веб проектов не дают покоя.
Сразу оговорюсь, что рассуждения касательно применимости не в энтерпрайз, а в своих мелких проектикиках.
Попробую систематизировать размышления, т.к. текущее состояние мыслей похоже на это в центральной стадии:
1. принцип DDD — проектируем модель, а затем думаем как сохранять. Хорошо, если модель легко проектируется на место хранения…
Возможно у меня кривое представление модели, но, под моделью(MVC, DDD) я понимаю большой жирный слой, в котором нужно реализовать следующее:
Интерфейсы, реализация классов объектов(геттеры, сеттеры). Связи между объектами пока опустим.
Обычно не сложно.
Сохранением объектов занимаются мапперы. Проектируем интерфейс маппера и реализуем по 1 мапперу на каждый класс объекта. Что-то простое плоское сохранять и извлекать легко. Идеологически, в коллекциях MongoDB правильно хранить агрегаты. Агрегаты — это сложные объекты, имеющие не плоскую структуру(состоящие из вложенных объектов). Для декомпозиции агрегатов нужен ещё один уровень абстракции. Назовём его Data Abstraction layer(DAL). Но о нём позже.
О реализации маппера, 2 варианта:
- Dependency Injection, инициатива создания объекта исходит от маппера. При создании объекта передаём ссылку на его маппер.
- Изначальное наделение объектов информацией о их маппере.
Оба варианта холиварные. Мне нравится второй, т.к. мапперы меняются не так часто(никогда) и получаем автокомплит.
Опять же удобно сделать MappersManager, который будет конструировать мапперов.
IdentityMap для объектов:
Неплохо бы кешировать объекты, чтобы при повторном обращении к мапперу получать данные из кеша, 2 варианта:
- декоратор к мапперу
- отдельный компонент, обращение к которому маппер делает самостоятельно
Мне нравится второй, т.к. в маппере могут быть самостоятельные обращения к IdentityMap.
Lazy Load конструирование данных:
При выборке большого количества объектов, маппер возвращает итератор над коллекцией, элементы которой будут созданы только в момент фактического доступа (foreach), 2 варианта:
- либо отключаем IdentityMap для этой коллекции
- либо используем инвалидацию хранилища в IdentityMap(сброс кеша).
Инвалидация IdentityMap:
Для этого все создаваемые маппером объекты нужно обернуть в проксирующие дектораторы. Которые декорируют метод объекта getId(), без фактического обращения к объекту. Иногда это выручает, т.к. не всем нужно кроме Id элемента что-то ещё.
Этот проксирующий декоратор позволит создать объект, в случае, если хранилище IdentityMap было инвалидировано.
Зависимые загрузки. Проблема 1+n запросов.
$posts = $user->posts() и затем foreach $posts->comments() фактически вызовет N запросов(для каждого поста по запросу), решается уведомлением зависимого маппера о вероятном использовании некоторых объектов. Например CommentMapper::notifyUsage($comment_id) или MapersManager::getMapper(‘comment’)->notifyUsage($comment_id);
Но для этого нужно отказаться от итератора над коллекцией возвращаемых значений для постов.
Связи. Самое сложное, 2 решения:
- Анемичная модель: PostService::addComment($post, $comment). В этом случае PostService должен выполнить операцию сам(изменить фактические данные). Для этого он должен знать о реальной структуре хранимых данных. И обращаться ему придётся к Data Abstration Layer, на уровень ниже.
- Богатая модель: $post->addComment($comment); В этом случае, вероятно, вызов должен быть делегирован обоим мапперам, чтобы каждый выполнил свою часть операции. Хранение связей в MongoDB отличается от реляционной модели. Возможно связь должны хранить оба элемента(тип значения «список» в MongoDB).
Скрипты(отчёты, регламентные процедуры, и.т.д.):
Скрипты в доменной модели, всегда веселуха. Напрямую обращаться к базе нельзя, всё через уровень доменной модели. Медленно.
Резюме:
Это только первый слой, без Data Abstration Layer. Выглядит уже сложно. Возникает ощущение программирования ради программирования.
Как возможная альтернатива: Анемичная модель и отказ от объектов пользу массивов. Интеграция приложения на уровне базы данных, когда всё приложение пронизано знанием о способе хранения и структуре возвращаемых данных.
Как показывает опыт анемичная модель доминирует в вебе. Суппорт анемичной модели так же более лёгкое занятие.
> Dependency Injection, инициатива создания объекта исходит от маппера. При создании объекта передаём ссылку на его маппер.
> Изначальное наделение объектов информацией о их маппере.
Смысл DataMapper’а в том, чтобы сущности о не знали о нем, иначе это ActiveRecord. Сущности в DDD — это POPO объекты, со связями.
А еще забыли, про самое главное — Unit of Work. Он отвечает за транзакционность при сохранении нескольких сущностей.
> При выборке большого количества объектов, маппер возвращает итератор над коллекцией.
Итератор? Возвращать нужно коллекцию, которую либо наполнили, либо она сама наполнится в момент обращения/получения итератора у нее.
> Напрямую обращаться к базе нельзя, всё через уровень доменной модели. Медленно.
Для этого есть Service Layer — если что-то проседает в нем всегда можно напрямую выполнить запрос.
> Анемичная модель и отказ от объектов пользу массивов.
Зачем тогда DDD? DDD — это всегда rich model, фасад в виде service layer и infrastructure layer для связи с ORM.
Вообще, вы должны реализовать IRepository и IUnitOfWork. И прокидывать их в сервисы. Они могут быть заточены как под конкретный ORM, так и быть композитом, чтобы работать с несколькими. http://blog.byndyu.ru/2010/07/2-unit-of-work_10.html
И еще вот: https://github.com/idr0id/ddd-blog/tree/master/src/Blog/InfrastructureBundle
Dr0ID, в твоем ddd-blog доменные сущности зависят от Doctrine, которая является инфраструктурой. Это означает, что стоит поменять Doctrine на что-то другое и доменную модель можно выбрасывать. Помоему ddd как раз о том, что доменная модель не должна ни от чего зависеть.
я думаю за 3 года у него что-то изменилось 🙂