воскресенье, 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 по критериям, которые обычно предъявляются к банковским системам. Или же проводить скрупулезное тестирование с использованием эвристических алгоритмов, как если бы вы разрабатывали систему для систем жизнеобеспечения или авиадиспетчерских. Но в то же время, вполне оправдано внимание к защите приложения от некорректных входных данных.

Заключение

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

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

понедельник, 19 января 2009 г.

Горячая вакансия

Пост немного не по теме блога, но мне кажется, что кое-кому он может быть интересен. Собственно, из заголовка видно, что у нас открылась вакансия в Киеве, поэтому, всем, кому интересно то, о чем я пишу, всем, кто хочет попробовать все это в деле – засылайте свои резюме на cv1@justapplications.co.uk.

Вкратце, мы ищем разработчика на позицию .NET Senior Developer. За этим названием скрываются требования:

  • Владение техниками объектно-ориентированного проектирования и программирования
  • Agile-навыки
  • Знание C# 3.0, платформы .NET 3.5;
  • ASP.NET (желательно ASP.NET MVC);
  • MS SQL  (желателен опыт работы с NHibernate);
  • Технический английский.

Дискуссия по каждому из пунктов на собеседовании :)

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

Кто мы? “Витринная” информация на нашем сайте. Начинали пару лет назад с разработки небольшой утилитки для людей, которые торгуют на eBay. Сегодня же у нас накопился целый букет разнообразных проектов и услуг + неплохой рост. Но на сегодняшний день мы выросли до уровня, когда силами распределенной команды задачи уже не решаются так хорошо, как раньше. Поэтому сейчас мы хотим собрать всю разработку под одной крышей в славном городе Киеве.

Что за проект? Сразу скажу, что проект долгосрочный. Выше я упомянул о букете сервисов и услуг, которые мы сейчас предоставляем. Так вот, новый проект – это разработка цельного комплексного решения, которое объединит в себе уже готовые продукты и расширится новыми. Сейчас мы разрабатываем ядро этой системы, на которую потом будет навешено все остальное. В общем, все достаточно интересно. Есть где развернуться. К этой же предметной области имеют отношение такие системы, как ChannelAdvisor.

среда, 31 декабря 2008 г.

Дао Scrum

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

Как известно, центром любого scrum-проекта является product backlog. Это место, куда product owner записывает свои пожелания, оценивая их значимость и вместе с командой планирует итерации. Казалось бы, что может быть проще? Но чем же этот подход оказался лучше других? Чем он отличается, скажем, от случая, когда заказчик просто заводит тикеты, например, в JIRA? Что делает этот проект столь эффективным?

Для того, чтобы это понять, достаточно взглянуть на перевод термина “backlog” (ну, или в толковый словарь для носителей языка):

backlog

  1. долг, задолженность
  2. невыполненные заказы
  3. резервы (товаров, материалов и т. п.)

На мой взгляд, значение этого термина – квинтэссенция всего процесса. Попробуем разобрать его по пунктам.

Backlog – это не список задач для разработчиков

Нет, конечно, в конечном счете разработчик работает со списком конкретных задач. Но изначально, с точки зрения product owner, список конкретных задач – это скучные подробности. В первую очередь из перевода следует, что backlog – это не список задач и даже не список user stories, которые нужно выполнить, а список недостатков системы. Под недостатками подразумеваются недостающая функциональность, недостаточное качество реализации, недостаточное соответствие ожиданиям бизнеса и так далее. Тут видно замечательное свойство термина “недостаток”, которое заключается в том, что оно обобщает артефакты нескольких подпроцессов: фича, улучшение, дефет.

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

У такого подхода, среди прочих достоинств, есть одно важное преимущество: он построен на позитивном мышлении творцов, стремящихся в своей работе к совершенству, подогревая творческий потенциал участников процесса. Ведь даже Микеланджело был приверженцем такого принципа: на вопрос о том, как он создает столь совершенные скульптуры, он ответил “Я просто беру резец и отсекаю от мрамора все лишнее”. Правда похоже? :)

Подразумевается, что приложение существует на этапе планирования

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

Выживают важнейшие требования

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

К чему это все?

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

Всех с Новым годом! :)

понедельник, 15 декабря 2008 г.

DSL, валидация и jQuery

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

Введение

Итак, напомню, что мы реализовали на данный момент:

  1. Объектная модель DSL валидации;
  2. Удобный механизм конструирования синтаксического дерева;
  3. Интерпретатор, который транслирует синтаксическую структуру дерева в логику валидации.

Для демонстрационных целей рассмотрим два альтернативных подхода - один с использованием серверных валидаторов ASP.NET, а второй будет чисто клиентским. Таким образом, мы увидим, как разделение логики DSL на декларативную и интерпретационную части помогает нам расширять его функциональность.

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

Затем мы разработаем "компилятор", который и будет превращать код, написанный на нашем DSL в логику валидации, которая будет встраиваться в форму по продуманному заранее сценарию.

Исходный код валидатора и модификации в DSL

В качестве подопытного кролика на сей раз выберем класс Person:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Comments { get; set; }
}

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

Email Constraint

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

Таким образом, пусть код валидатора будет таким:

var v = DefineValidator.For<Person>()

   .WhereProperty(p => p.FirstName).SatisfiedAs(Should.NotBeNullOrEmpty)
        .WithReason("Введите имя").AsError()
   .WhereProperty(p => p.FirstName).SatisfiedAs(Should.NotBeLongerThan(10))
        .WithReason("Слишком длинное имя")

   .WhereProperty(p => p.LastName).SatisfiedAs(Should.NotBeNullOrEmpty)
        .WithReason("Введите фамилию").AsError()
   .WhereProperty(p => p.LastName).SatisfiedAs(Should.NotBeLongerThan(10))
        .WithReason("Слишком длинная фамилия")

   .WhereProperty(p => p.Email).SatisfiedAs(Should.MatchEmailTemplate)
        .WithReason("Введите правильный емейл");

Динамическое добавление серверных валидаторов

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

<form runat="server">
    Имя:<br />
    <cc:ValidableTextBox ID="FirstName" runat="server" Type="Product"></cc:ValidableTextBox><br />
    Фамилия:<br />
    <cc:ValidableTextBox ID="LastName" runat="server" Type="Product"></cc:ValidableTextBox><br />
    Емейл:<br />
    <cc:ValidableTextBox ID="Email" runat="server" Type="Product"></cc:ValidableTextBox><br />
    Комментарии:<br />
    <cc:ValidableTextBox ID="Comments" runat="server" Type="Product"></cc:ValidableTextBox><br />
    <br />
    <asp:Button runat="server" Text="Submit" />
</form>

В приведенном фрагменте вместо стандартного контрола TextBox мы использовали его расширенную версию ValidableTextBox, в котором появилось поле Type. Для чего это нужно? Дело в том, что мы должны связать контрол на форме с конкретным свойством конкретного класса. В простейшем случае имя свойства можно использовать в качестве идентификатора для контрола, а свойство Type будет использоваться для указания типа, к которому это свойство принадлежит.

В WebForms валидаторы - это обычные серверные контролы, поэтому добавление валидаторов на форму - это обычная манипуляция коллекцией контролов:

protected void Page_Init(object sender, EventArgs e)
{
    const string maxLengthPattern = @"[\s\S]{{0,{0}}}";
    const string emailPattern = @"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*";

    AddValidator<Person>(p => p.FirstName,
        new RequiredFieldValidator(),
        "Введите имя");

    AddValidator<Person>(p => p.FirstName,
        new RegularExpressionValidator { ValidationExpression = string.Format(maxLengthPattern, 10) },
        "Слишком длинное имя");

    AddValidator<Person>(p => p.LastName,
        new RequiredFieldValidator(),
        "Введите фамилию");

    AddValidator<Person>(p => p.LastName,
        new RegularExpressionValidator { ValidationExpression = string.Format(maxLengthPattern, 10) },
        "Слишком длинная фамилия");

    AddValidator<Person>(p => p.Email,
        new RegularExpressionValidator { ValidationExpression = emailPattern },
        "Введите правильный емейл");
}

void AddValidator<T>(Expression<Func<T, object>> property, BaseValidator validator, string message)
{
    var fildName = ((MemberExpression)property.Body).Member.Name;

    var validableControl = FindControl(fildName) as IValidableControl;

    if (validableControl != null)
    {
        validator.ErrorMessage = message;
        validator.Display = ValidatorDisplay.Dynamic;
        validator.ControlToValidate = ((Control) validableControl).ID;

        var container = ((Control)validableControl).Parent.Controls;
        container.AddAt(container.IndexOf((Control) validableControl) + 1, validator);
    }
}

В ходе экспериментов с динамическим добавлением контролов мы увидели, что констрейнт StringMaxLengthConstraint разумно реализовать на базе RegularExpressionConstraint, потому что ограничение по длине поля с использованием серверных валидаторов в WebForms реализуется с помощью RegularExpressionValidator.

Показательным является то, что перекроив логику работы StringMaxLengthConstraint мы никак не повлияли на синтаксис нашего DSL.

Динамическая генерация javascript-кода для валидации

Раз уж речь пошла о том, что второй подход у нас будет ориентирован на использование ASP.NET MVC, то логично будет развить эту мысль и сказать, что код клиентской валидации мы будем писать, используя jQuery. И раз уж мы решили использовать jQuery, то стоит взглянуть на плагин Validation. Главным плюсом этого плагина является то, что вся логика описывается в декларативном стиле и может быть локализована в одном месте.

Код нашей формы будет выглядеть так:

<form action="ValidateHtmlForm.aspx" method="post" id="PersonForm">
    Имя:<br />
    <input type="text" name="Person$FirstName" /><br />
    Фамилия:<br />
    <input type="text" name="Person$LastName" /><br />
    Емейл:<br />
    <input type="text" name="Person$Email" /><br />
    Комментарии:<br />
    <input type="text" name="Person$Comments" /><br />
    <br />
    <input type="submit" value="Submit" />
</form>

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

И в этом случае код валидации будет таким:

$(document).ready ( 
    function() {
        $("#PersonForm").validate(
            {
                rules: {
                    Person$FirstName: {
                        required: true,
                        maxlength: 10
                    },
                    Person$LastName: {
                        required: true,
                        maxlength: 10
                    },
                    Person$Email: {
                        email: true
                    }
                },
                messages: {
                    Person$FirstName: {
                        required: "Введите имя",
                        maxlength: "Слишком длинное имя"
                    },
                    Person$LastName: {
                        required: "Введите фамилию",
                        maxlength: "Слишком длинная фамилия"
                    },
                    Person$Email: {
                        email: "Введите правильный емейл"
                    }
                }
            }
        )
    }
);

Как видим, в отличии от серверных контролов для проверки длины поля и его соответствия шаблону используются готовые спецификаторы maxLength и email. Далее мы увидим, как констрейнты в нашем DSL спроектированны таким образом, что они могут без труда интерпретироваться и как RegularExpressionValidator с конкретным паттерном, и как спецификаторы maxlength и email.

Автоматизация генерации кода

Стандартным решением для пододбных задач являтся использование паттерн Visitor. Именно реализацию этого паттерна все желающие могут найти в недрах исходников LINQ в виде абстрактного intenal-класса ExpressionVisitor. Итак, напомним, в нашем примере синтаксическая структура DSL описывается иерархией констрейнтов. Поэтому разработаем абстрактный Visitor с реализациями метода Visit для каждого из типов констрейнтов и добавим две реализации для каждого из типов валидации:

2_Visitors

"Кирпичиком" наших валидаторов является инетрфейс IRule. Модифицируем его так, чтобы он выполнял роль Element для паттерна Visitor:

3_ValidatorElement 

При этом реализация метода Accept для каждого из констрейнтов будет выглядеть почти одинаково:

public class StringNotNullOrEmptyConstraint : IConstraint
{
    ....
    public void Accept(IValidatorVisitor visitor)
    {
        visitor.Visit(this);
    }
}

Т.е. единственная польза от этого метода - полиморфная диспетчеризация вызовов.

Точкой входа для всей конструкции является метод VisitValidator. Этот метод получает на вход список валидаторов, которые необходимо "посетить", перебирая их, вызывает метод Accept. А сами констрейнты "возвращают" вызовы в Visitor через метод Visit, как было показано выше.

Реализация метода VisitValidator для WebFormValidatorVisitor:

public void VisitValidator(IEnumerable<IRulesGroup> rulesGroups)
{
    foreach (var validator in rulesGroups)
        foreach (var rule in validator.Rules)
            rule.Accept(this);
}

Обернув логику связывания в некие классы-хелперы, на примере JQueryValidatorVisitor код использования будет выглядеть так:

<head runat="server">
    
    <script type="text/javascript" src="jquery-1.2.6.js"></script>
    <script type="text/javascript" src="jquery.validate.js"></script>

    <%= ValidateForm("PersonForm") %>
    
</head>

Реализация метода ValidateForm для случая с использованием JQueryValidatorVisitor:

public string ValidateForm(string fortmId)
{
    var script = new StringBuilder();

    IValidatorVisitor visitor = new JQueryValidatorVisitor(script, fortmId);
    visitor.VisitValidator(CreateValidators());

    return string.Format("<script type=\"text/javascript\">\n{0}</script>", script);
}

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

public void Visit(StringNotNullOrEmptyConstraint constraint)
{
    var validator = new RequiredFieldValidator
                        {
                            ErrorMessage = _currentRule.Message,
                            Display = ValidatorDisplay.Dynamic,
                            ControlToValidate = _controlToValidate.ID
                        };
    var container = _controlToValidate.Parent.Controls;
    container.AddAt(_controlIndex + 1, validator);
}

Заключение

В последних двух постах мы увидели лишь верхушку огромного айсберга, под названием DSL. Эта тема сейчас активно разрабатывается многими крупными игроками на рынке средств разработки. Главное направление этих изысканий - упрощение разработки и внедрения DSL в приложения. Например, Microsoft на последней PDC анонсировала интересный продукт, под названием Oslo. Хороший обзор написал Мартин Фаулер. Кроме этого, старина Мартин работает над книгой по DSL. Как обычно, это будет каталог паттернов, с которым уже можно познакомиться у него на сайте. А еще, совсем недавно JetBrains выпустили бета-релиз своего нового продукта Meta Programming System, который решает схожие задачи с Microsoft Oslo. И что-то мне подсказывает, впереди нас ждет еще много интересного.

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

среда, 22 октября 2008 г.

Domain Specific Language в своем приложении - это просто

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

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

Описание предметной области

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

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

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

Рабочий пример

Определим в нашем приложении класс Product следующим образом:

public class Product
{
   public int Id { get; set; }
   public string Title { get; set; }
   public string Description { get; set; }
}

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

CREATE TABLE Product
(
   [Id] INT IDENTITY PRIMARY KEY,
   [Title] NVARCHAR(5) NOT NULL,
   [Description] NVARCHAR(20) NOT NULL
)

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

С учетом всех этих требований, реализуем метод контроллера формы, который будет проверять входные данные и сохранять их в базу таким образом:

public void Save(Product product)
{
   // Проверяем соблюдене правил

   if (string.IsNullOrEmpty(product.Title))
       ThrowValidationException("Название не может быть пустым");

   if (product.Title.Length > 5)
       ThrowValidationException("Слишком длинное название");

   if (string.IsNullOrEmpty(product.Description))
       ThrowValidationException("Описание не может быть пустым");

   if (product.Description.Length > 10 && !Confirm("Описание слишком длинное и при публикации будет обрезано до 10 симвалов. Вы согласны?"))
       return;

   // Если все правила удовлетворяются, сохраняем в базу данных
   SaveToDatabase(product);   
}

Кроме этого в коде формы для нужных полей установлены валидаторы:

Название:
<asp:TextBox ID="ProductTitle" runat="server" MaxLength="5" />
<asp:RequiredFieldValidator ID="ProductTitleRequiredValidator" runat="server"
   ControlToValidate="ProductTitle" ErrorMessage="Название не может быть пустым" />
<br />
Описание:
<asp:TextBox ID="ProductDescription" runat="server" MaxLength="10" />
<asp:RequiredFieldValidator ID="ProductDescriptionRequiredField" runat="server"
   ControlToValidate="ProductTitle" ErrorMessage="Описание не может быть пустым" />

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

Унификация логики валидации. Внутренний DSL

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

public void Save(Product product)
{
   IValidator validator = ValidationFactory.CreateValidatorFor<Product>();

   if(validator.IsValid(product))
       SaveToDatabase(product);   
}

Интерфейс IValidator предоставляет функциональность для конструирования логики из неких сущностей-терминов и затем выполняет саму валидацию:

Эта схема является грамматикой нашего будущего DSL. Нужно отметить, что в общем случае имеет смысл разделить логику синтаксиса DSL и его интерпретации. В данном примере речь идет о методах GetBrokenRules, IsValid и SatisfiedBy. Аналогом подобного разделения является пример использования класса System.Linq.Expressions.Expression для описания лямбда-функций в виде абстрактного синтаксического дерева. Это позволяет реализовывать LINQ-провадеры для доступа к разнообразным источникам данных.

Как мы видим на схеме, сама по себе она достаточно бессмысленна, пока у нас не появятся реализации всех этих интерфейсов:

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

Для реализации IConstraint мы могли бы воспользоваться паттерном Specification для созания сложных констрейнтов. Например, вместо добавления класса RangeConstraint можно было бы собрать его из ранее добавленых GreaterThanConstraint и LessThanConstraint, связав их через AndConstraint.

Итак, используя наш DSL мы можем создавать валидаторы таким образом:

var validator = new TypeValidator<Product>();

validator.AddRule(new Rule(
   new PropertyValueConstraint<Product>(
       p => p.Title, new StringNotNullOrEmptyConstraint()))
           { Message = "Заголовок не может быть пустым" });

validator.AddRule(new Rule(
   new PropertyValueConstraint<Product>(
       p => p.Title, new StringMaxLengthConstraint(5)))
           { Message = "Заголовок слишком длинный" });

validator.AddRule(new Rule(
   new PropertyValueConstraint<Product>(
       p => p.Description, new StringNotNullOrEmptyConstraint()))
           { Message = "Описание не может быть пустым" });

validator.AddRule(new Rule(
   new PropertyValueConstraint<Product>(
       p => p.Description, new StringMaxLengthConstraint(10)))
           { Message = "Описание слишком длинное", Severity = Severity.Warning });

Уже сейчас мы можем реализовать фабрику ValidatorFactory, которая будет создавать экземпляры валидаторов для нужных типов, или, используя паттерн Visitor можно компилировать структуру валидаторов в код проверки пользовательских данных на форме.

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

Улучшение синтаксиса

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

Очень удобным и быстрым способом улучшения синтаксиса является использование "текучего интерфейса" (fluent interface).

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

var validator = DefineValidator.For<Product>()
   .WhereProperty(p => p.Title).SatisfiedAs(Should.NotBeNullOrEmpty)
       .WithReason("Заголовок не может быть пустым").AsError()
   .WhereProperty(p => p.Title).SatisfiedAs(Should.NotBeLongerThan(5))
       .WithReason("Заголовок слишком длинный")
   .WhereProperty(p => p.Description).SatisfiedAs(Should.NotBeNullOrEmpty)
       .WithReason("Описание не может быть пустым")
   .WhereProperty(p => p.Description).SatisfiedAs(Should.NotBeLongerThan(10))
       .WithReason("Описание слишком длинное").AsWarning();

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

Реализация внешнего DSL.

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

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

validator for Product
  property Title should not be null or empty
     with reason "Заголовок не может быть пустым"
  property Title should not be longer than 5
     with reason "Заголовок слишком длинный"
  property Description should not be null or empty
     with reason "Описание не может быть пустым"
  property Description should not be longer than 10
     with reason "Описание слишком длинное" as warning

Такое приведение к естественному языку позволяет нам подключить к работе над проектом людей, весьма далеких от программирования: аналитиков, экспертов по предметной области, заказчиков. Последний пункт особенно важен в случае agile-разработки, когда заказчик является полноценным членом команды. Или же если в проекте применяется Domain Driven Design, то паттерн Ubiquitous Language можно реализовать в виде некого DSL.

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

В своей статье Oren Eini описывает различные способы реализации DSL в среде CLR, приводя примеры и делая вывод, что для этих целей от отдает предпочтение языку Boo. Я же в качестве альтернативы приведу пример реализации такого компилятора подстановками на языке Nemerle:

namespace Rules.Dsl
{
   [assembly: OperatorAttribute ("Rules.Dsl", "validator_for", true, 180, 181)]
   macro validator_for(typeName)
   {
       <[ DefineValidator.For.[$typeName]() ]>
   }

   [assembly: OperatorAttribute ("Rules.Dsl", "where_property", false, 160, 161)]
   macro where_property(validatorRef, propertyName)
   {
       <[ $validatorRef.WhereProperty(x => x.$(propertyName.ToString() : dyn) ) ]>
   }

   [assembly: OperatorAttribute ("Rules.Dsl", "should", false, 150, 151)]
   macro should(propertyElementRef, constraintRef)
   {
       <[ $propertyElementRef.SatisfiedAs($constraintRef) ]>
   }

   [assembly: OperatorAttribute ("Rules.Dsl", "with_reason", false, 140, 141)]
   macro with_reason(validatorRef, message)
   {
       <[ $validatorRef.WithReason($message) ]>
   }

   [assembly: OperatorAttribute ("Rules.Dsl", "as_error", true, 140, 141)]
   macro as_error(validatorRef)
   {
       <[ $validatorRef.AsError() ]>
   }

   [assembly: OperatorAttribute ("Rules.Dsl", "as_warning", true, 140, 141)]
   macro as_warning(validatorRef)
   {
       <[ $validatorRef.AsWarning() ]>
   }

   macro not_be_null_or_empty()
   syntax("not_be_null_or_empty")
   {
       <[ Should.NotBeNullOrEmpty ]>
   }

   macro not_be_longer_than(maxLength)
   {
       <[ Should.NotBeLongerThan($maxLength) ]>
   }
}

Код валидатора для класса Product на Nemerle с использованием этих макросов будет выглядеть так:

def validator =
   validator_for Product
       where_property Title should not_be_null_or_empty
           with_reason "Заголовок не может быть пустым" as_error
       where_property Title should not_be_longer_than(5)
           with_reason "Заголовок слишком длинный" as_error
       where_property Description should not_be_null_or_empty
           with_reason "Описание не может быть пустым" as_error
       where_property Description should not_be_longer_than(10)
           with_reason "Описание слишком длинное" as_warning;

Синтаксис немного отличается от спроектированного, но он так же выразителен :)

Что дальше?

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

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

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

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

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

Для всех интересующихся доступен исходный код примера.

UPDATE: О том, как встраивать этот DSL в html-форму читаем следующий пост.