понедельник, 9 ноября 2009 г.

CQS – конец монополии систем реляционных баз данных

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

Краткое содержание предыдущих серий

В общем случае, используя CQS где-то на уровне кода, в 80% случаев мы придем примерно к такой схеме:

CQS в первом приближении

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

Но если пойти дальше и применить CQS даже к базам данных, то мы получим две различных структуры для хранения данных: Reporting Database и Domain Database (да, здесь нет никакой ссылки :)). В сочетании с Domain Events картинка выше преобразится следующим образом:

CQS и Domain Events

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

Итак, что мы получаем в итоге:

  1. Существует некая база данных, структура которой максимально адаптирована под конкретные задачи отображения данных;
  2. База для хранения данных домена, которая не видна пользователю, со структурой, непосредственно заточенной под хранение всех этих entities и value objects.

Вместо базы данных — репозиторий

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

Чаще всего любые манипуляции над доменом начинаются с того, что мы либо создаем новые экземпляры aggregate roots, либо, «достав» из репозитория нужный экземпляр, производим над ним какие-то манипуляции.

Таким образом типичный репозиторий будет выглядеть так:

interface IOrderRepository
{
   void Add(Order newOrder);
   Order Get(OrderId orderId);
}

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

А теперь фокусы

Подводим итоги из всего вышесказанного и пробуем сделать выводы:

  1. Domain Database предназначена исключительно для доступа к ней инфраструктуры репозиториев.

    А значит никаких сложных sql-запросов и нестандартных структур выходных данных. Все запросы строго стандартны, все выходные структуры данных определены структурой типов самого домена. Более того, структура хранилища подразумевает наличие всего нескольких ключевых сущностей или же наборов сущностей с непересекающимися структурами, с которыми работают, как с едиными целыми. А значит нам ничего не мешает провести «денормализацию» такого рода:

    Денормализация

    Где OrderSnapshot — это просто сериализованное состояние нашего aggregate root со всей его иерархией. Таким образом все наши сложные иерархии и множества объектов превращаются в самую обычную хэш-таблицу, не оставляя и следа от реляционности.

  2. Стандартными операциями в репозитории являются Add и Get.

    Если рассматривать Aggregate Root как атом, с которым нам приходится работать, используя лишь методы add/get, то для этих нам совершенно не обязательно пользоваться именно реляционными базами данных. Мы можем обратить свой взор в сторону документо-ориентированных, или в сторону объектных баз данных.

  3. Для отчетов используются оптимизированные для этих целей структуры

    Как уже показывалось выше, «сужение» специализации базы данных позволяет нам, решая частные задачи, использовать специализированные решения, которые из за своей частной природы могут не быть совместимыми с некой «общей теорией всего». Под специализированными структурами я подразумеваю как обычную грубую денормализацию, так и использование совершенно специальных решений для хранилищ данных, всевозможных OLAP-решений, и систем интеллектуального анализа данных.

В итоге

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

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

Но как быть, когда частные случаи являют собой достаточно сложную материю, с чем мы сталкиваемся ежедневно, пытаясь смоделировать сложную предметную область?

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

Применение принципе CQS в общем случае и помогает нам перейти от поисков генериков к решению вполне себе частных задач частными средствами. И, как мы видим, принцип этот так же удачно применим и к дизайну баз данных.

Ссылки

  1. Unshackle Your Domain
  2. Eager Read Derivation
  3. CQRS à la Greg Young example code
  4. Domain-Driven Design Group

среда, 4 ноября 2009 г.

CQS, приватное состояние – а как же тесты?

... спросите вы, и для этого вопроса будут все основания.

Для начала рассмотрим код, каким он является до рефакторинга в строну сокрытия его состояния:

class Review : RootAggregate
{
    public Review(string fileName, string description)
    {
        FileName = fileName;
        Description = description;
    }

    public static Review Create(string fileName, string description)
    {
        return new Review(fileName, description);
    }

    public string FileName { get; private set; }
    public string Description { get; private set; }
}

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

[Test]
public void Should_set_the_properties()
{
    createdReview.FileName.Should().Be.EqualTo(FIRST_NAME);
    createdReview.Description.Should().Be.EqualTo(DESCRIPTION);
}

И именно в тестах такого рода и будет проблема, потому как при сохранении дизайна домена, мы сможем упрятать состояние, только сделав его internal. И это даже идеально подходит для случая, когда структура солюшна подразумевает наличие отдельной сборки для каждого агрегата. В любом случае, это половинчатое решение, которое в простонародье называется "хак".

Мы же попробуем найти именно архитектурное решение, которое не будет зависеть ни от каких инфраструктурных сиюминутных особенностей.

А теперь нашу модель ждет самое большое потрясение в ее короткой жизни. Для начала познакомимся с паттерном Domain Event. Вкратце (для нашего случая), это абстракция для событий, которые генерируются в домене и извещают подписчиков об изменении состояния этого самого домена.

В представленном выше коде единственное событие, которое может происходить – создание нового экземпляра Review. После небольшой модификации наш код будет выглядеть следующим образом:

    ...

    private string _fileName;
    private string _description;    
    ...
    public static Review Create(string fileName, string description)
    {
        var review = new Review(fileName, description);

        _messageBus.Publish(new ReviewCreatedDomainEvent(fileName, description));
        
        return review;
    }
    ...
}

class ReviewCreatedDomainEvent { public string Description { get; private set; } public string FileName { get; private set; } public ReviewCreatedDomainEvent(string fileName, string description) { FileName = fileName; Description = description; } }

И таким образом наша задача в тестах превращается в почти тривиальную (после некоторых манипуляций с кодом на тему перехвата отправляемых сообщений):

[Test]
public void Should_set_the_properties()
{
    Catch()
        .From(() => {
            Review.Create(FILE_NAME, DESCRIPTION);})
        .And
            .Assert.That(e => e.FileName.Should().Be.EqualTo(FILE_NAME))
            .Assert.That(e => e.Description.Should().Be.EqualTo(DESCRIPTION));
    
}

Что и требовалось доказать.

На самом деле, решение проблем с тестированием в данном случае является лишь поводом заикнуться о Domain Events. Разработка модели на основе событий пополам с Commands-Queries Separation Principle на сегодняшний день является, пожалуй, одним из самых мощных архитектурных стилей, позволяющих разрабатывать действительно гибкие, устойчивые и масштабируемые системы.

В следующих постах попробуем разработать эту тему глубже.

Добро пожаловать в высшую лигу :)

вторник, 3 ноября 2009 г.

CQS – как вам сущности без публичных свойств?

Продолжаем исследовать тему разделения логики запросов от логики команд.

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

Вместо абстрактного введения, небольшая иллюстрация. Вот так может выглядеть код некого условного заказа в нашем «очищенном» домене:

class Order : RootAggregate
{
    readonly IList _lines = new List();
    readonly DateTime _createdDate;
    
    decimal _totalAmount;

    public Order(DateTime createdDate)
    {
        _createdDate = createdDate;
    }

    public void AddProduct(string productName, int quantity, decimal price)
    {
        _lines.Add(new OrderLine(productName, quantity, price));
        UpdateTotal();
    }

    private void UpdateTotal()
    {
        _totalAmount = _lines.Sum(x => x.Price*x.Quantity);
    }
}

public class OrderLine : ValueObject
{
    public string ProductName { get; private set; }
    public int Quantity { get; private set; }
    public decimal Price { get; private set; }

    public OrderLine(string productName, int quantity, decimal price)
    {
        ProductName = productName;
        Quantity = quantity;
        Price = price;
    }
}

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

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

var orderIsApplicableForDiscountSpecification = 
    new OrderIsApplicableForDiscountSpecification();

if (orderIsApplicableForDiscountSpecification.Matches(order))
    order.ApplyDiscount(new StandardDiscount());

где сама спецификация реализована таким образом:

class OrderIsApplicableForDiscountSpecification
{
    public bool Matches(Order order)
    {
        return order.TotalAmount >= 10000;
    }
}

Таким образом появляется и свойство TotalAmount у заказа.

На самом деле, никакой мистики здесь нет — все вышепоказанное ни что иное, как обычная банальная инкапсуляция. Просто в свете применения принципа CQS 80% случаев использования информации о состоянии модели просто уходят в небытие и вместе с этим уходит вся необходимая инфраструктура в виде публичных свойств.