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-форму читаем следующий пост.

далее...

28 Сентябрь 2008 г.

Хороший дизайн должен быть SOLID: TOP-5 архитектурных принципов

Что такое хороший дизайн? По каким критериям его оценивать, и каких правил придерживаться при разработке? Как обеспечить достаточный уровень гибкости, связанности, управляемости, стабильности и понятности кода? Роберт Мартин составил список, состоящий всего из пяти правил хорошего проектирования, которые известны, как принципы SOLID.

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

SRP: Single Responsibility Principle (принцип единственной обязанности)

Не должно существовать более одного мотива для изменения данного класса.

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

Рассмотрим пример. Пусть у нас есть класс, реализующий некоторую функциональность, связанную с банковским счетом:

class Account : ActiveRecord
{
    public Guid Id{ get{ ... } }
    public string Number { get{ ... } }
    public decimal CurrentBallance { get { ... } }
    public void Deposit(decimal amount){ ... }
    public void Withdraw(decimal amount){ ... }   
    public void Transfer(decimal amount, Account recipient){ ... }
    public TaxTable CalculateTaxes(int year){ ... }
}

Как видно, класс несет ответственность за:

  1. Персистентность;
  2. Логику управление балансом;
  3. Логику расчета налогов.

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

class Account
{
    public string Number { get{ ... } }
    public decimal CurrentBallance { get { ... } }
    public void Deposit(decimal amount){ ... }
    public void Withdraw(decimal amount){ ... }
    public void Transfer(decimal amount, Account recipient){ ... }
}

class AccountRepository
{
    public Account GetByNumber(string number){ ... }
    public void Save(Account acc){ ... }
}

class TaxCalculator
{
    public TaxTable CalculateTaxes(Account acc, int year){ ... }
}

А сложность в применении данного принципа заключается в том, что прежде всего нужно научиться правильно чувствовать границы его использования. Ведь даже в приведенном примере мы превратили паттерн Active Record в антипаттерн, разом перечеркнув все примеры его успешного применения.

OCP: Open/Closed Principle (принцип открытия/закрытия)

Объекты проектирования (классы, функции, модули и т.д.) должны быть открыты для расширения, но закрыты для модификации.

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

Рассмотрим простой пример. Пусть у нас в системе есть некий класс, отвечающий за просмотр логов:

class LogViewer
{
    public IEnumerable<Transaction> GetByDate(DateTime dateTime){ ... }
}

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

class LogViewer
{
    public IEnumerable<Transaction> GetByDate(DateTime dateTime){ ... }
    public IEnumerable<Transaction> GetByUser(string name){ ... }
    public IEnumerable<Transaction> GetByDateAndUser(DateTime dateTime, string name){ ... }
}

Подобная эволюция дизайна является типичным примером нарушения принципа открытия/закрытия. А типичным решением этой проблемы мог бы стать следующий код:

class LogViewer
{
    public IEnumerable<Transaction> GetTransaction(GetSpecification spec){ ... }
}

abstract class GetSpecification
{
    public GetSpecification CombineWith(GetSpecification nextSpec){ ... }
    // ...
}

class GetByDateSpecification : GetSpecification
{
    // ...
}

class GetByUserSpecification : GetSpecification
{
    // ...
}

// Пример использования
class Client
{
    public void ShowLog()
    {
        var viewer = new LogViewer();
        var transactions = viewer.GetTransaction(
            new GetByDateSpecification()
            .CombineWith(new GetByUserSpecification()));
    }
}

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

  1. "Открыт для расширения": поведение может быть расширено путем добавления новых объектов, реализующих новые аспекты поведения;
  2. "Закрыт для модификации": в результате расширения поведения исходный или двоичный код объекта не может быть изменен.

LSP: Liskov Substitution Principle (принцип замещения Лисков)

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

Впервые этот принцип был упомянут Барбарой Лисков в 1987 году на научной конференции, посвященной объектно-ориентированному программированию.

Этот принцип является важнейшим критерием для оценки качества принимаемых решений при построении иерархий наследования. Сформулировать его можно в виде простого правила: тип S будет подтипом Т тогда и только тогда, когда каждому объекту o1 типа S соответствует некий объект o2 типа T таким образом, что для всех программ P, реализованных в терминах T, поведение P не будет меняться, если o1 заменить на o2.

Классическим примером нарушения этого принципа является построение иерархии такого рода:

class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int CalculateRectangleArea()
    {
        return Width*Height;
    }
}

class Square : Rectangle
{
    public override int Height
    {
        get{ return base.Height; }
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }

    public override int Width
    {
        get{ return base.Width; }
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }
}

class Program
{
    private static Rectangle CreateRecatgle()
    {
        return new Square();
    }

    static void Main()
    {
        Rectangle r = CreateRecatgle();
        r.Width = 3;
        r.Height = 2;

        Assert.AreEqual(6, r.CalculateRectangleArea());
    }
}

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

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

ISP: Interface Segregation Principle (принцип изоляции интерфейса)

Клиент не должен вынужденно зависеть от элементов интерфейса, которые он не использует.

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

Пример нарушения этого принципа:

abstract class ServiceClient
{
    public string ServiceUri{ get; set; }
    public abstract void SendData(object data);
    public abstract void Flush();
}

class HttpServiceClient : ServiceClient
{
    public override void SendData(object data)
    {
        var channel = OpenChannel(ServiceUri);
        channel.Send(data);
    }

    public override void Flush()
    {
        // Метод ничего не делает, но присутствует в классе
    }
}

class BufferingHttpServiceClient : ServiceClient
{
    public override void SendData(object data)
    {
        Buffer.Write(data);
    }

    public override void Flush()
    {
        var channel = OpenChannel(ServiceUri);
        channel.Send(Buffer.GetAll());
    }
}

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

abstract class ServiceClient
{
    public string ServiceUri{ get; set; }
    public abstract void SendData(object data);
}

abstract class BufferingServiceClient : ServiceClient
{
    public abstract void Flush();
}

class HttpServiceClient : ServiceClient
{
    public override void SendData(object data){ ... }
}

class BufferingHttpServiceClient : BufferingServiceClient
{
    public override void SendData(object data){ ... }
    public override void Flush(){ ... }
}

Еще одним признаком потенциального нарушения этого принципа является наличие громоздких интерфейсов. Попробуйте реализовать MembershipProvider для ASP.NET, унаследовавшись от стандартного базового класса и вы поймете о чем речь :)

DIP: Dependency Inversion Principle (принцип обращения зависимости)

Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

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

Дизайн таких систем можно охарактеризовать следующими признаками:

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

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

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

public class OrderProcessor
{
    public decimal CalculateTotal(Order order)
    {
        decimal itemTotal = order.GetItemTotal();
        decimal discountAmount = DiscountCalculator.CalculateDiscount(order);

        decimal taxAmount = 0.0M;

        if (order.Country == "US")
            taxAmount = FindTaxAmount(order);
        else if (order.Country == "UK")
            taxAmount = FindVatAmount(order);

        decimal total = itemTotal - discountAmount + taxAmount;

        return total;
    }

    private decimal FindVatAmount(Order order)
    {
        return 10.0M;
    }

    private decimal FindTaxAmount(Order order)
    {
        return 12.0M;
    }
}

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

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

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

// Интерфейсы стратегий
public interface IDiscountCalculator
{
    decimal CalculateDiscount(Order order);
}

public interface ITaxStrategy
{
    decimal FindTaxAmount(Order order);
}

// Реализация стратегий
public class DiscountCalculatorAdapter : IDiscountCalculator
{
    public decimal CalculateDiscount(Order order)
    {
        return DiscountCalculator.CalculateDiscount(order);
    }
}

public class USTaxStrategy : ITaxStrategy
{
    public decimal FindTaxAmount(Order order){ ... }
}

public class UKTaxStrategy : ITaxStrategy
{
    public decimal FindTaxAmount(Order order){ ... }
}

// Облегченный код
public class OrderProcessor
{
    private readonly IDiscountCalculator _discountCalculator;
    private readonly ITaxStrategy _taxStrategy;

    public OrderProcessor(IDiscountCalculator discountCalculator, 
                          ITaxStrategy taxStrategy)
    {
        _taxStrategy = taxStrategy;
        _discountCalculator = discountCalculator;
    }

    public decimal CalculateTotal(Order order)
    {
        decimal itemTotal = order.GetItemTotal();
        decimal discountAmount = _discountCalculator.CalculateDiscount(order);

        decimal taxAmount = _taxStrategy.FindTaxAmount(order);

        decimal total = itemTotal - discountAmount + taxAmount;

        return total;
    }
}

Принцип обращения зависимостей лежит в основе архитектур многих каркасов приложений. Для автоматизации процесса управления зависимостями разработано множество утилит. Мартин Фаулер в своей статье рассмотрел всевозможные паттерны реализации механизма работы этого принципа.

Заключение

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

Отдельного внимания заслуживает подход к разработке через тестирование (TDD) и его связь с SOLID-принципами. Для многих именно незнание этих принципов является той самой непреодолимой стеной, которая мешает начать использовать TDD. Но во многом, это мнение является ошибочным, потому как разработка юнит-тестов сама по себе не является сложной задачей и начать писать тесты может каждый на любом этапе своего развития. Главное начать. И уже потом, в процессе поиска оптимальных способов разработки через тестирование будет достигнуто глубокое понимание всех тонкостей качественного проектирования.

далее...

10 Сентябрь 2008 г.

Expression Trees и оптимизация Reflection

В последней версии .NET Framework среди новых возможностей было добавлено средства метапрограммирования под названием Expression Trees. На базе этой технологии, а именно основываясь на том принципе, что выражения на "обычном" языке программирования могут автоматически преобразовываться в синтаксические деревья, была разработана технология LINQ.

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

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

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

Сам по себе метод создания нужной лямбда-функции достаточно прост:

private static Func<object, object> CreateGetter(object entity, string propertyName)
{
    var param = Expression.Parameter(typeof (object), "e");
    Expression body = Expression.PropertyOrField(Expression.TypeAs(param, entity.GetType()), propertyName);
    var getterExpression = Expression.Lambda<Func<object, object>>(body, param);
    return getterExpression.Compile();
}

Если поставить в этом методе точку останова и посмотреть на строковое представление переменной getterExpression, то мы увидим, во что оно будет скомпилировано:

getterExpression

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

readonly Dictionary<PropertyGetterKey, Func<object, object>> propertyGetters = new Dictionary<PropertyGetterKey, Func<object, object>>();

public object GetPropertyValue(object entity, string propertyName)
{
    Func<object, object> getter;

    var key = new PropertyGetterKey {Type = entity.GetType(), PropertyName = propertyName};

    if (propertyGetters.ContainsKey(key))
        getter = propertyGetters[key];
    else
    {
        getter = CreateGetter(entity, propertyName);
        propertyGetters.Add(key, getter);
    }

    return getter(entity);
}

Для проверки того, насколько эта логика эффективна, разработаем небольшой тест:

var entities = new List<Class1>();

for (var i = 0; i < 20; i++)
    entities.Add(new Class1 { Property1 = "Value" + i });

foreach (var entity in entities)
{
    var start = DateTime.Now.Millisecond;
    var val = ReflectionHelper.Instance.GetPropertyValue(entity, "Property1");
    Console.WriteLine("{0} - {1}", val, (DateTime.Now.Millisecond - start));
}

Ну и результаты говорят сами за себя:

screenshot002

Как видим, такой способ оптимизации более чем жизнеспособен :)

далее...