четверг, 5 августа 2010 г.

Kiev Alt.NET Group–первые шаги

Итак, почти неделю назад состоялась первая встреча группы с нехитрым названием Kiev Alt.NET Group. По свежим следам я собирался было поделиться какими-то впечатлениями, благодарностями, написать что-то о светлом будущем, которое нас всех ожидает. Но так как меня опередили, то я попробую рассмотреть все уже почти в ретроспективе: почему группа собралась и чем она отличается от «официальной» Kyiv .NET User Group? Тем более что в качестве одной из реакций, кроме ожидаемого обсуждения того, что понравилось, а что нет, прозвучал термин «раскол» в отношении к самому факту появления этой группы и ее сосуществованию с другими подобными группами.

В чем же «проблема раскола» и почему появление нашей группы оправдано по нашему мнению?

Для начала я скажу – никакой проблемы раскола нет, а есть вполне закономерное и логичное развитие сообщества. А сообщество в свою очередь развивается вместе с самой платформой – сравните .NET сегодня и почти 10 лет назад: главная разница в том, что в начале своего пути, платформа была целостнее в своей простоте, против сегодняшней сложности всего ее разнообразия.

А что сообщество? С одной стороны, сообщество разрослось за счет сотрудничества разработчиков нескольких поколений (в рамках технологий, 10 лет можно рассматривать как несколько поколений, да). Разные поколения – это разный опыт, разные уровни ответственности, разные задачи, которые приходится решать. С другой стороны, накопившееся количество знаний уже ни в одну голову целиком не помещается и планка входа в технологию поднимается.

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

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

На сегодня же сегментация – естественна и необходима.

В споре рождается истина, и чем ближе спорщики друг к другу по духу, по пониманию мира, тем качественнее эта самая истина – эффективное сотрудничество лингвистов и биологов едва ли поможет физикам поймать бозон Хиггса.

И вот мы подходим к «кризису раскола» в киевском .NET-сообществе.

Ребята, коллеги, все эти группы в первую очередь – не более чем кружки по интересам, участники которых пытаются через общение (слушая, выступая, задавая вопросы) повысить свой профессиональный уровень для самых корыстных целей – для решения повседневных рабочих задач. Как писалось выше, .NET перестал быть чем-то «вообще» - он все больше превращается в набор слабо связанных инструментов и практик использования.

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

И о какой универсальной группе может идти речь? Какими должны быть обсуждаемые темы, чтобы в нынешних условиях собрать однородное сообщество? Ведь никто не ведет блог просто «О программировании», а если что-то такое и начинается, то очень быстро скатывается на уровень научно-популярной беллетристики.

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

Одна группа – это еще не сообщество, а сообщество – уже не группа. Поэтому развиваться сообщество будет по пути тесного сотрудничества между подобными группками. Существует масса тем, вызывающих общий интерес, которые могут собрать аудиторию в несколько сотен человек. Но ведь есть еще и темы, которые интересны лишь небольшому клубу озадаченных. Так зачем же заставлять друг друга скучать на таких «частных» темах, впустую убивая время?

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

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

Давайте общаться и сотрудничать! Ведь все мы все равно решаем одни и те же задачи Smile

понедельник, 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% случаев использования информации о состоянии модели просто уходят в небытие и вместе с этим уходит вся необходимая инфраструктура в виде публичных свойств.

воскресенье, 25 октября 2009 г.

Command-query separation — две стороны одной модели

Паттерн Domain Model, о котором многие наверняка читали в одной небезызвестной книге в сочетании с подходом Domain Driven Design поначалу привлекает множество разработчиков своей логичностью, простотой и универсальностью, которые видятся им в первом приближении. Но как только начинается работа над реальным приложением, то сразу появляется куча нетривиальных проблем, которые вкупе с достаточно строгой «дисциплиной» подхода не дают возможности увидеть каких-то простых и элегантных решений. И тогда проект начинает скатываться в суровую действительность с кучей костылей и инвалидных колясок.

Проблема

Разработаем небольшую но типичную CRM. У нас будет база клиентов, контакты и история общения с ними. С точки зрения UI у нас будет список клиентов, формы редактирования деталей отдельно взятого клиента, что-то для управления логом каждого клиента. Все выглядит действительно просто и предсказуемо. За один вечер мы быстренько набрасываем код и к утру заказчик получает готовое к эксплуатации приложение, а разработчик — глубокую сатисфакцию от проделанной работы.

На следующий день заказчик присылает новое требование: «Хочу видеть в списке клиентов среднее время отклика из логов». Вы кидаетесь в код класса Customer и бодро добавляете свойство AvgCallbackTime примерно такого содержимого:

public TimeSpan AverageCallbackTime
{
    get
    {
        return _calls.Average(c => c.CallbackTime);
    }
}

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

И вот настает тот самый день «Х». К вам на рабочий стол ложиться задача: «Реализовать редактируемый список клиентов, активно покупающих товар в заданной категории, с адресом доставки, средней суммой чека, частотой покупок, популярным способом оплаты и фамилией ответственного менеджера. При этом пользователь должен иметь возможность отсортировать и отфильтровать список по производному (в том числе и вычисляемому) полю. Кроме этого, список должен выводиться постранично. Ну и список колонок в выборке должен формироваться на основе уровня доступа пользователя к некоторым критичным данным».

Думаю, расписывать дальнейший ход мыслей и стиль принимаемых решения будет бессмысленно :)

Итак, как же нам сохранить модель красивой и лаконичной? Что нам делать с требованиями, касающимися отображения данных и не несущественным с точки зрения каких-то законов поведения объектов?

Двойственность природы модели домена

Если модель является функцией приложения, то было бы логичным предположить, что на нее распространяются всевозможные архитектурные «правила хорошего тона», а в нашем конкретном случае речь пойдет именно Single Responsibility Principle. Как сформулировать эту самую единственную ответственность модели домена? Что именно имплементирует эта модель?

Согласно определению Фаулера, модель домена — это:

An object model of the domain that incorporates both behavior and data.

Но это определение не самое удачное с точки зрения принципа SRP, потому как при наличии двух дополнений в предложении в нем точно так же остается поле для домысливания: что же в модели домена главнее — данные или поведение?

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

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

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

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

Решение рядом

Для начала согласимся с посылом, что данные (другими словами, состояние) являются неотъемлемой частью модели, и функция поведения — это всего лишь функция изменения соответствующих данных (состояния).

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

И вот тут-то мы практически вплотную подошли к тому, чтобы назвать, а затем выделить из модели ту «лишнюю» ответственность, «уродует» ее и мешает нормально развиваться.

Решение было сформулировано много лет назад Бертраном Мейером, как одно из основных правил разрабатываемого им языка программирования Eiffel. Принцип называется Command-query Separation Principle и его самая известная формулировка выглядит так:

Аsking a question should not change the answer

Другими словами:

Каждый метод должен либо выполнять какое-то действие, либо возвращать какие-то данные, но никак ни то и другое вместе.

Этот принцип был сформулирован применительно к языку программирования и стал одним из основных правил разработки чистого кода без сторонних эффектов.

Но как же хороша его формулировка применительно к нашей модели домена, не правда ли? Есть данные, есть запросы, есть методы модификации данных. Нам остается лишь сформулировать этот принцип для архитектуры нашего приложения в целом.

Запросы и команды

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

Command-query separation

Другими словами, все данные, которые пользователь видит — подготавливаются подсистемой отчетов: все списки, детали, печатные формы. Как только пользователь подтверждает изменение каких-то данных — его команды обрабатывает модель домена.

В чем плюсы?

С этого места начинаем собирать урожай полученных преимуществ :)

  1. Моделью для отчетов служит самая что ни на есть настоящая модель данных со всеми своими нормальными формами, денормализациями и связками по ключам. Таким образом мы получаем возможность достать для пользователя любые данные, не спотыкаясь о репозитории, агрегаты и, прости господи, value objects;
  2. В подсистеме отчетов отпадает необходимость в поддержке сложной инфраструктуры в виде каких-либо ORM, поддержки транзакций и прочего (хотя, о транзакциях говорят всякое);
  3. Если у нас нет необходимости в сложной инфраструктуре и у нас под руками настоящая схема данных, то самым удобным средством выборки данных является SQL во всех его ипостасях;
  4. Никто не запрещает нам использовать специализированные репортинг-сервисы, всевозможные OLAP и прочие удобности из области Business Intelligence;
  5. Вследствие упрощения инфраструктуры субъективно увеличивается производительность;
  6. Модель домена становится максимально лаконичной и сфокусированной исключительно на поведенческих аспектах бизнес-логики.

В чем минусы?

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

Что останется в модели?

Рефакторинг, связанный с CQS прежде всего коснется репозиториев. В самом лучшем случае репозиторий будет предоставлять только какой-нибудь метод типа GetByID — все остальные методы выборок благополучно уйдут в отчеты.

Нужно отметить, что возможность полноценного использования всех этих репозеториев, root aggregates, entities и value objects появляется во многом лишь благодаря правильному использованию CQS.

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

Что в итоге?

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

В целом, CQS является, пожалуй, самой яркой иллюстрацией процесса Separation of Concerns. Кроме этого, понятия запрос и команда применимы к любому аспекту архитектуры системы, связанной с обработкой данных.

Например он очень здорово отвечает на вопросы, возникающие в области SOA. Вкратце: все в курсе, что самый гибкий и удобный способ общение между компонентами — это асинхронный обмен сообщениями, но как быть со случаем, когда компонент просто должен предоставить какую-то информацию только для чтения, причем, в синхронном режиме? Об этом интересно написал Udi Dahan.

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

среда, 15 июля 2009 г.

Hexagonal/Onion Architecture - слоим приложения

На третий день отпуска в деревне мозг потребовал активных развлечений. Гибсоновская «Машина различий» благополучно переварилась и очередным кандидатом на расправу был выбран порядком подзабытый блог, в черновиках которого зависло уже приличное количество тем на проработку.

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

Архитектура по-умолчанию

Итак, мы начинаем новый проект. Чтобы легче было работать, пусть это будет какой-нибудь интернет-магазин со вполне себе ординарной функциональностью: какая-нибудь витрина, корзина и система заказов. Открыв любимую IDE, через несколько пассов у нас получится приложение с примерно такой структурой:

Default Architecture

Стрелочками обозначены ссылки между проектами.

Наверняка, сначала будет разработана база данных. Затем через какой-нибудь DAL сущности из проекта BusinessLogic будут этими самыми данными манипулировать. Ну и UI-проект все это будет отображать, собирать какой-то пользовательский ввод и дергать за ниточки бизнес-логику.

Все бы хорошо, но данная схема подразумевает первичность базы данных. Ее разработка влияет на все остальные уровни. И в случае, если мы хотим смоделировать бизнес-логику, используя Domain Driven Design, это влияние будет столь значительным, что рано или поздно все попытки разработать полноценную модель скатятся в использование Active Record или Table Module, похоронив все дальнейшие попытки использования преимуществ возможностей богатой модели.

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

А что если бы появилась возможность сконцентрироваться на разработке модели домена, как на первоочередном модуле?

Ресурсами наружу

Решение проблемы с первоочередностью разработки модели домена (как, в общем случае, и любой другой логики, связанной с разрабатываемым приложением) была хорошо сформулирована Алистером Кокберном в виде паттерна Hexagonal Architecture.

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

Перерисовав структуру нашего приложения согласно вышеописанному паттерну, мы получим примерно следующее:

Hexagonal Architecture

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

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

Центр управляет всем

В качестве решения этой проблемы предлагается использование так называемой «луковой архитектуры» (onion architecture). У этого паттерна такая же мотивация, что и у гексагональной архитектуры, но решение строится не просто на отделении абстракций от реализации, а на управлении доступом между различными уровнями архитектуры.

Если мы перерисуем структуру нашего проекта, согласно предложенной схеме, то у нас получится такой рисунок:

Onion Atchitecture

Смысл данного подхода заключается в следующем:

  1. В ядре системы находится модуль, не связанный ни с чем. Чистая абстракция предметной области;
  2. Другие модули выстраиваются вокруг ядра таким образом, чтобы их зависимости были направлены вовнутрь к ядру;
  3. Зависимости в противоположную торону от ядра недопустимы;
  4. Оболочка системы состоит из модулей конкретных реализаций внутренних асбтракций.

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

Заключение

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

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

Кроме того, не стоит забывать об использовании всевозможных Dependency Injection-фреймворкам, которые помогут избежать трудностей при управлении временем жизни объектов в такой вот «вывернутой» архитектуре.

Отпуск продолжается :)

вторник, 3 февраля 2009 г.

Defensive Design. Откуда берутся сбои

Так сложилось, что если о всяких паттернах и архитектурах на собеседованиях соискатели более или менее в состоянии поддержать разговор, то как только речь заходит о вариантах использования Debug.Assert(…), сразу же начинается плавание вокруг да около. Нет, я не говорю, что никто не может сформулировать основных положений «оборонительного проектирования», просто очень редко случаются действительно «концептуальные» беседы: как правило, приходится поднимать знания, которые присутствуют где-то на уровне подсознания на более высокий уровень. Поэтому я попробую организовать небольшую шпаргалку, дабы помочь коллегам.

Введение

Для начала вспомним знаменитый Закону Мёрфи (в общем виде) применительно к разработке ПО: «если существуют условия, в которых программа ведет себя неверно, то такие условия обязательно проявятся». Это может быть неверный пользовательский ввод, сбои в железе, рассогласование протоколов или же ошибки внутри самой программы. Так вот, оборонительное проектирование и борется со всевозможными проявлениями Закона Мёрфи – эта техника обороняет систему от того, что «никогда не может случиться».

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

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

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

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

Неверные данные

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

Если же рассмотреть этот принцип в рамках концепции оборонительного проектирования, то можно сказать обобщенно, что любой программный модуль (функция, класс, сервис и т.д.) не должен никоим образом доверять входным данным. В случае же, если обнаружится, что аргументы не удовлетворяют каким-то условиям, модуль должен корректно обработать эту ситуацию.

В общем случае, есть три правила обработки входных данных:

  1. Проверке подлежат все данные из внешних источников;
  2. Проверке подлежат все входные параметры для данного модуля;
  3. Должна существовать четкая стратегия обработки неверных данных.

Плохой дизайн, грязный код

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

  1. Неоправданная сложность – потенциальный источник проблем (YAGNI, KISS, DRY);
  2. Повышение абстракции помогает изолировать проблемы (SOLID);
  3. Всестороннее тестирование, особенно на граничных и неверных условиях поможет выявить проблемы на ранних этапах (Software Testing);
  4. Чем меньше кода пишется с нуля – тем меньше в нем возникает ошибок (Code Reuse, Metaprogramming);
  5. Инспекция кода поможет выявить проблемы на уровне кодирования (Code Review, Code Audit).

Legacy-код, сторонние компоненты

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

Но даже хорошо написанный код унаследованных компонентов может стать источником проблем при использовании. Тривиальным примером является использование 16-битных инструкций в коде, который запускается на 64-битной операционной системе. Особенно, когда производители ОС заявляют о прекращении поддержки 16-битного API :) Еще одной знаменитой проблемой такого рода была «Проблема 2000».

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

В общем-то, все проблемы, связанные с legacy-кодом решаются установлением контроля над ним через изоляцию и рефакторинг. Изоляция позволяет нам максимально уменьшить зависимость и, как следствие, влияние проблемного компонента на систему в целом. А рефакторинг привносит в код недостающие элементы обороны.

Все эти техники подробно расписаны в хрестоматийной книге Working Effectively with Legacy Code (этот же материал, но в более компактной форме доступен в виде статьи).

Безопасность

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

Список рекомендаций для проектирования безопасных систем никогда не будет сформулирован окончательно. По сути, идет нескончаемая война между разработчиками и хакерами: война со своими стратегиями, героями, со своими победами и поражениями. И список рекомендаций было правильнее назвать списком тактик. Этот список будет пополняться по мере изобретения новых способов атак на системы. Приведу лишь самые основные из них:

  1. Любой код небезопасен, пока не доказано обратное;
  2. Безопасная обработка входных и выходных данных: доверяйте только проверенным источникам, ждите неприятностей на входе, никогда не сообщайте больше информации, чем нужно;
  3. Ограничивайте привилегии на уровне необходимого минимума для функционирования данного программного модуля;
  4. Неграмотная защита сродни ее отсутствию. Поэтому нужно защищаться от известных вам типов атак и всегда продолжать изучать новые типы атак;
  5. Пользуйтесь криптографией;
  6. Проводите регулярный аудит системы безопасности.

Паранойя

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

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

Заключение

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

В следующих постах я продолжу «конспект», останавливаясь более детально на отдельных аспектах.