Продолжая раскапывать возможности архитектуры, основанной на принципе разделения команд и запросов, попробуем порассуждать, как все это вяжется с проектированием базы данных.
Краткое содержание предыдущих серий
В общем случае, используя CQS где-то на уровне кода, в 80% случаев мы придем примерно к такой схеме:
Данные из базы данных запрашиваются для отображения, а на основе увиденного пользователем генерируется поток команд, который в конце концов приводит к изменению исходных данных в БД.
Но если пойти дальше и применить CQS даже к базам данных, то мы получим две различных структуры для хранения данных: Reporting Database и Domain Database (да, здесь нет никакой ссылки :)). В сочетании с Domain Events картинка выше преобразится следующим образом:
То есть, все изменения состояния домена порождают некий поток событий, обработав которые мы можем изменить содержимое связанных отчетов. Ничего сложного — все это просто более продвинутая версия решения задачи с обновлением остатков товара на складе при каждом поступлении или отгрузке оного.
Итак, что мы получаем в итоге:
- Существует некая база данных, структура которой максимально адаптирована под конкретные задачи отображения данных;
- База для хранения данных домена, которая не видна пользователю, со структурой, непосредственно заточенной под хранение всех этих entities и value objects.
Вместо базы данных — репозиторий
Как упоминалось ранее, точками входа в наш домен и естественными логическими единицами бизнес-логики являются так называемые Aggregate Roots: корневые сущности, над которыми мы можем производить какие-то действия, которые и моделируют некое поведение модели предметной области.
Чаще всего любые манипуляции над доменом начинаются с того, что мы либо создаем новые экземпляры aggregate roots, либо, «достав» из репозитория нужный экземпляр, производим над ним какие-то манипуляции.
Таким образом типичный репозиторий будет выглядеть так:
interface IOrderRepository { void Add(Order newOrder); Order Get(OrderId orderId); }
И в некоторых особых случаях репозиторий может поддерживать удаление или какие-то другие специальные методы. Все. Ничего лишнего.
А теперь фокусы
Подводим итоги из всего вышесказанного и пробуем сделать выводы:
- Domain Database предназначена исключительно для доступа к ней инфраструктуры репозиториев.
А значит никаких сложных sql-запросов и нестандартных структур выходных данных. Все запросы строго стандартны, все выходные структуры данных определены структурой типов самого домена. Более того, структура хранилища подразумевает наличие всего нескольких ключевых сущностей или же наборов сущностей с непересекающимися структурами, с которыми работают, как с едиными целыми. А значит нам ничего не мешает провести «денормализацию» такого рода:
Где OrderSnapshot — это просто сериализованное состояние нашего aggregate root со всей его иерархией. Таким образом все наши сложные иерархии и множества объектов превращаются в самую обычную хэш-таблицу, не оставляя и следа от реляционности.
- Стандартными операциями в репозитории являются Add и Get.
Если рассматривать Aggregate Root как атом, с которым нам приходится работать, используя лишь методы add/get, то для этих нам совершенно не обязательно пользоваться именно реляционными базами данных. Мы можем обратить свой взор в сторону документо-ориентированных, или в сторону объектных баз данных.
- Для отчетов используются оптимизированные для этих целей структуры
Как уже показывалось выше, «сужение» специализации базы данных позволяет нам, решая частные задачи, использовать специализированные решения, которые из за своей частной природы могут не быть совместимыми с некой «общей теорией всего». Под специализированными структурами я подразумеваю как обычную грубую денормализацию, так и использование совершенно специальных решений для хранилищ данных, всевозможных OLAP-решений, и систем интеллектуального анализа данных.
В итоге
Оказывается, использование реляционных баз данных не является единственным возможным принимаемым решением при разработке сложных корпоративных приложений.
Сама по себе реляционная структура данных хороша прежде всего своей универсальностью и многозадачностью. Но любая универсальная парадигма привносит в наши жизни массу сложностей, связанных прежде всего с необходимостью поиска алгоритма перехода от общих положений к частным случаям.
Но как быть, когда частные случаи являют собой достаточно сложную материю, с чем мы сталкиваемся ежедневно, пытаясь смоделировать сложную предметную область?
Ответ прост — нужно максимально абстрагироваться от обобщенных теорий и использовать как можно более частные, приспособленные для конкретной задачи инструменты. Ведь мы можем с легкостью пренебречь релятивистской поправкой при расчете массы тела, движущегося со скоростями, много меньшими скорости света.
Применение принципе CQS в общем случае и помогает нам перейти от поисков генериков к решению вполне себе частных задач частными средствами. И, как мы видим, принцип этот так же удачно применим и к дизайну баз данных.