tag:blogger.com,1999:blog-88644353965243754312024-02-20T19:53:13.298+02:00Систематизация автоматизацииПерсональная карта памятиIgorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.comBlogger20125tag:blogger.com,1999:blog-8864435396524375431.post-48934774343048238782010-08-05T08:54:00.002+03:002011-11-29T02:37:19.295+02:00Kiev Alt.NET Group–первые шаги<p>Итак, почти неделю назад состоялась первая встреча группы с нехитрым названием <a href="http://kievalt.net/" target="_blank">Kiev Alt.NET Group</a>. По свежим следам я собирался было поделиться какими-то впечатлениями, благодарностями, написать что-то о светлом будущем, которое нас всех ожидает. Но так как <a href="http://blog.kievalt.net/post/884617100/report-ddd-and-cqrs-event" target="_blank">меня опередили</a>, то я попробую рассмотреть все уже почти в ретроспективе: почему группа собралась и чем она отличается от «официальной» <a href="http://kyiv.ineta.ru/" target="_blank">Kyiv .NET User Group</a>? Тем более что в качестве одной из реакций, кроме ожидаемого обсуждения того, что понравилось, а что нет, прозвучал термин «раскол» в отношении к самому факту появления этой группы и ее сосуществованию с другими подобными группами.</p><p>В чем же «проблема раскола» и почему появление нашей группы оправдано по нашему мнению?</p><p>Для начала я скажу – никакой проблемы раскола нет, а есть вполне закономерное и логичное развитие сообщества. А сообщество в свою очередь развивается вместе с самой платформой – сравните .NET сегодня и почти 10 лет назад: главная разница в том, что в начале своего пути, платформа была целостнее в своей простоте, против сегодняшней сложности всего ее разнообразия.</p><p>А что сообщество? С одной стороны, сообщество разрослось за счет сотрудничества разработчиков нескольких поколений (в рамках технологий, 10 лет можно рассматривать как несколько поколений, да). Разные поколения – это разный опыт, разные уровни ответственности, разные задачи, которые приходится решать. С другой стороны, накопившееся количество знаний уже ни в одну голову целиком не помещается и планка входа в технологию поднимается.</p><p>И самой естественной частью этого процесса эволюции является сегментирование, или специализация: становится все меньше мастеров на все руки и все больше специалистов в разных достаточно узких проблемных областях. И даже те области, которые раньше казались чем-то целым, начинают дробиться на какие-то свои категории.</p><p>И очевидным побочным эффектом процесса сегментации является то, что проблемы одних областей перестают интересоваться других. В изоляции видится спасение – локализация проблемы помогает найти ее решение. Конечно, в то же время изоляция влечет за собой потерю общего видения процесса, непонимание общих проблем, но это – лишь будущее, с которым еще предстоит столкнуться. </p><p>На сегодня же сегментация – естественна и необходима. </p><p>В споре рождается истина, и чем ближе спорщики друг к другу по духу, по пониманию мира, тем качественнее эта самая истина – эффективное сотрудничество лингвистов и биологов едва ли поможет физикам поймать бозон Хиггса.</p><p>И вот мы подходим к «кризису раскола» в киевском .NET-сообществе.</p><p>Ребята, коллеги, все эти группы в первую очередь – не более чем кружки по интересам, участники которых пытаются через общение (слушая, выступая, задавая вопросы) повысить свой профессиональный уровень для самых корыстных целей – для решения повседневных рабочих задач. Как писалось выше, .NET перестал быть чем-то «вообще» - он все больше превращается в набор слабо связанных инструментов и практик использования.</p><p>Именно поэтому появилась наша группа: нам захотелось обсудить наши текущие проблемы не только у кофе-машины и в узком кругу. Как показывает опыт общения, эти темы интересуют не только нас. И наша группа – это всего лишь еще одна попытка организовать тот почти сложившийся костяк людей, которые как-то общались твиттере, скайпе, в каких-то блогах и форумах. </p><p>И о какой универсальной группе может идти речь? Какими должны быть обсуждаемые темы, чтобы в нынешних условиях собрать однородное сообщество? Ведь никто не ведет блог просто «О программировании», а если что-то такое и начинается, то очень быстро скатывается на уровень научно-популярной беллетристики. </p><p>Поэтому я не понимаю рассуждений о расколе, которые звучат в среде, сформировавшейся в эпоху наивной юности, когда и дизайнеры форм работали лучше, и датасеты типизировались сильне, и хранимые процедуры работали надежнее, а все, что начиналось со слова class – было объектно-ориентированным программированием. Ведь ничто не стоит на месте!</p><p>Одна группа – это еще не сообщество, а сообщество – уже не группа. Поэтому развиваться сообщество будет по пути тесного сотрудничества между подобными группками. Существует масса тем, вызывающих общий интерес, которые могут собрать аудиторию в несколько сотен человек. Но ведь есть еще и темы, которые интересны лишь небольшому клубу озадаченных. Так зачем же заставлять друг друга скучать на таких «частных» темах, впустую убивая время?</p><p>Дабы закрыть тему и пресечь на корню все дальнейшие попытки реально расколоть сообщество, я хочу еще раз подчеркнуть: </p><ul><li>наша группа не задумывалась, как оппозиция – речь идет всего лишь о желании обсуждать специфические темы в естественно более узком кругу;</li>
<li>наша группа – это не клуб избранных, а кружок по интересам, не связанный какими-либо догмами и манифестами: единственный критерий, по которому тема будет обсуждаться – это должно быть интересно участникам;</li>
<li>наша группа не закрытое сообщество – мы с радостью готовы представить свои материалы всем желающим, готовы поучаствовать в деятельности других групп и с радостью примем желающих высказаться на нашей площадке.</li>
</ul><p>Давайте общаться и сотрудничать! Ведь все мы все равно решаем одни и те же задачи <img style="border-bottom-style: none; border-right-style: none; border-top-style: none; border-left-style: none" class="wlEmoticon wlEmoticon-smile" alt="Smile" src="http://quatrocode.com/blogspot_images/2010/08/wlEmoticon-smile.png" /></p>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0tag:blogger.com,1999:blog-8864435396524375431.post-39251361105608690412009-11-09T03:02:00.007+02:002011-11-29T02:59:30.923+02:00CQS – конец монополии систем реляционных баз данных<p>Продолжая раскапывать возможности архитектуры, основанной на принципе разделения команд и запросов, попробуем порассуждать, как все это вяжется с проектированием базы данных.</p><h4>Краткое содержание предыдущих серий</h4><p>В общем случае, используя CQS где-то на уровне кода, в 80% случаев мы придем примерно к такой схеме:</p><p><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="CQS в первом приближении" border="0" alt="CQS в первом приближении"
src="http://quatrocode.com/blogspot_images/2009/11/CQS_RDBMS_1.gif" width="401" height="296" /> </p><p>Данные из базы данных запрашиваются для отображения, а на основе увиденного пользователем генерируется поток команд, который в конце концов приводит к изменению исходных данных в БД.</p><p>Но если пойти дальше и применить CQS даже к базам данных, то мы получим две различных структуры для хранения данных: <a href="http://martinfowler.com/bliki/ReportingDatabase.html" target="_blank">Reporting Database</a> и Domain Database (да, здесь нет никакой ссылки :)). В сочетании с <a href="http://martinfowler.com/eaaDev/DomainEvent.html" target="_blank">Domain Events</a> картинка выше преобразится следующим образом:</p><p><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="CQS и Domain Events" border="0" alt="CQS и Domain Events" src="http://quatrocode.com/blogspot_images/2009/11/CQS_RDBMS_2.gif" width="529" height="384" /></p><p></p><p>То есть, все изменения состояния домена порождают некий поток событий, обработав которые мы можем изменить содержимое связанных отчетов. Ничего сложного — все это просто более продвинутая версия решения задачи с обновлением остатков товара на складе при каждом поступлении или отгрузке оного.</p><p>Итак, что мы получаем в итоге:</p><ol><li>Существует некая база данных, структура которой максимально адаптирована под конкретные задачи отображения данных; </li>
<li>База для хранения данных домена, которая не видна пользователю, со структурой, непосредственно заточенной под хранение всех этих entities и value objects. </li>
</ol><h4>Вместо базы данных — репозиторий</h4><p>Как упоминалось ранее, точками входа в наш домен и естественными логическими единицами бизнес-логики являются так называемые Aggregate Roots: корневые сущности, над которыми мы можем производить какие-то действия, которые и моделируют некое поведение модели предметной области.</p><p>Чаще всего любые манипуляции над доменом начинаются с того, что мы либо создаем новые экземпляры aggregate roots, либо, «достав» из <a href="http://martinfowler.com/eaaCatalog/repository.html" target="_blank">репозитория</a> нужный экземпляр, производим над ним какие-то манипуляции.</p><p>Таким образом типичный репозиторий будет выглядеть так:</p><pre class="brush: csharp;">interface IOrderRepository
{
void Add(Order newOrder);
Order Get(OrderId orderId);
}</pre><p>И в некоторых особых случаях репозиторий может поддерживать удаление или какие-то другие специальные методы. Все. Ничего лишнего.</p><h4>А теперь фокусы</h4><p>Подводим итоги из всего вышесказанного и пробуем сделать выводы:</p><ol><li><strong>Domain Database предназначена исключительно для доступа к ней инфраструктуры репозиториев</strong>. <br />
<br />
А значит никаких сложных sql-запросов и нестандартных структур выходных данных. Все запросы строго стандартны, все выходные структуры данных определены структурой типов самого домена. Более того, структура хранилища подразумевает наличие всего нескольких ключевых сущностей или же наборов сущностей с <em>непересекающимися структурами,</em> с которыми работают, как с <em>едиными целыми</em>. А значит нам ничего не мешает провести «денормализацию» такого рода: <br />
<br />
<img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; border-left-width: 0px" title="Денормализация" border="0" alt="Денормализация" src="http://quatrocode.com/blogspot_images/2009/11/CQS_RDBMS_3.gif" width="446" height="267" /> <br />
<br />
Где OrderSnapshot — это просто сериализованное состояние нашего aggregate root со всей его иерархией. Таким образом все наши сложные иерархии и множества объектов превращаются в самую обычную <a href="http://en.wikipedia.org/wiki/Hash_table" target="_blank">хэш-таблицу</a>, не оставляя и следа от реляционности. <br />
<br />
</li>
<li><strong>Стандартными операциями в репозитории являются Add и Get. <br />
</strong> <br />
Если рассматривать Aggregate Root как атом, с которым нам приходится работать, используя лишь методы add/get, то для этих нам <em>совершенно не обязательно </em>пользоваться именно реляционными базами данных. Мы можем обратить свой взор в сторону <a href="http://en.wikipedia.org/wiki/Document-oriented_database" target="_blank">документо-ориентированных</a>, или в сторону <a href="http://en.wikipedia.org/wiki/Object_database" target="_blank">объектных</a> баз данных. <br />
<br />
</li>
<li><strong>Для отчетов используются оптимизированные для этих целей структуры</strong> <br />
<br />
Как уже показывалось выше, «сужение» специализации базы данных позволяет нам, решая частные задачи, использовать специализированные решения, которые из за своей частной природы могут не быть совместимыми с некой «общей теорией всего». Под специализированными структурами я подразумеваю как обычную грубую <a href="http://en.wikipedia.org/wiki/Denormalization" target="_blank">денормализацию</a>, так и использование совершенно специальных решений для <a href="http://en.wikipedia.org/wiki/Data_warehouse" target="_blank">хранилищ данных</a>, всевозможных <a href="http://ru.wikipedia.org/wiki/OLAP" target="_blank">OLAP</a>-решений, и систем <a href="http://en.wikipedia.org/wiki/Data_mining" target="_blank">интеллектуального анализа данных</a>. </li>
</ol><h4>В итоге</h4><p><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; margin-left: 0px; border-left-width: 0px; margin-right: 0px" title="От общего к частному" border="0" alt="От общего к частному" align="right"
src="http://quatrocode.com/blogspot_images/2009/11/CQS_RDBMS_4.gif" width="248" height="248" />Оказывается, использование реляционных баз данных не является единственным возможным принимаемым решением при разработке сложных корпоративных приложений. </p><p>Сама по себе реляционная структура данных хороша прежде всего своей универсальностью и многозадачностью. Но любая универсальная парадигма привносит в наши жизни массу сложностей, связанных прежде всего с необходимостью поиска алгоритма перехода от <em>общих положений</em> к <em>частным случаям</em>. </p><p>Но как быть, когда <em>частные случаи</em> являют собой достаточно сложную материю, с чем мы сталкиваемся ежедневно, пытаясь смоделировать сложную предметную область?</p><p>Ответ прост — нужно максимально абстрагироваться от обобщенных теорий и использовать как можно более частные, приспособленные для конкретной задачи инструменты. Ведь мы можем с легкостью пренебречь релятивистской поправкой при расчете массы тела, движущегося со скоростями, много меньшими скорости света.</p><p>Применение принципе CQS в общем случае и помогает нам перейти от поисков генериков к решению вполне себе частных задач частными средствами. И, как мы видим, принцип этот так же удачно применим и к дизайну баз данных.</p><h4></h4><h4>Ссылки</h4><ol><li><a href="http://www.infoq.com/presentations/greg-young-unshackle-qcon08" target="_blank">Unshackle Your Domain</a> </li>
<li><a href="http://martinfowler.com/bliki/EagerReadDerivation.html" target="_blank">Eager Read Derivation</a> </li>
<li><a href="http://blog.fohjin.com/blog/2009/11/3/CQRS_a_la_Greg_Young_example_code" target="_blank">CQRS à la Greg Young example code</a> </li>
<li><a href="http://tech.groups.yahoo.com/group/domaindrivendesign/" target="_blank">Domain-Driven Design Group</a> </li>
</ol>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com13tag:blogger.com,1999:blog-8864435396524375431.post-73251265472359508452009-11-04T08:00:00.001+02:002009-11-04T08:00:05.245+02:00CQS, приватное состояние – а как же тесты?<p>... спросите вы, и для этого вопроса будут все основания.</p> <p>Для начала рассмотрим код, каким он является до рефакторинга в строну сокрытия его состояния: </p> <pre class="brush: csharp;">class Review : RootAggregate
{
public Review(string fileName, string description)
{
FileName = fileName;
Description = description;
}
public static Review Create(string fileName, string description)
{
return new Review(fileName, description);
}
public string FileName { get; private set; }
public string Description { get; private set; }
}</pre>
<p>ну и тест, который где-то там проверяет, что все свойства верно проинициализированы:</p>
<pre class="brush: csharp;">[Test]
public void Should_set_the_properties()
{
createdReview.FileName.Should().Be.EqualTo(FIRST_NAME);
createdReview.Description.Should().Be.EqualTo(DESCRIPTION);
}</pre>
<p>И именно в тестах такого рода и будет проблема, потому как при сохранении дизайна домена, мы сможем упрятать состояние, только сделав его internal. И это даже идеально подходит для случая, когда структура солюшна подразумевает наличие отдельной сборки для каждого агрегата. В любом случае, это половинчатое решение, которое в простонародье называется "хак".</p>
<p>Мы же попробуем найти именно архитектурное решение, которое не будет зависеть ни от каких инфраструктурных сиюминутных особенностей.</p>
<p>А теперь нашу модель ждет самое большое потрясение в ее короткой жизни. Для начала познакомимся с паттерном <a href="http://martinfowler.com/eaaDev/DomainEvent.html" target="_blank">Domain Event</a>. Вкратце (для нашего случая), это абстракция для событий, которые генерируются в домене и извещают подписчиков об изменении состояния этого самого домена.</p>
<p>В представленном выше коде единственное событие, которое может происходить – создание нового экземпляра Review. После небольшой модификации наш код будет выглядеть следующим образом:</p>
<pre class="brush: csharp;"> ...
private string _fileName;
private string _description;
...
public static Review Create(string fileName, string description)
{
var review = new Review(fileName, description);
_messageBus.Publish(new ReviewCreatedDomainEvent(fileName, description));
return review;
}
...
}<br />
<br />class ReviewCreatedDomainEvent
{
public string Description { get; private set; }
public string FileName { get; private set; }
public ReviewCreatedDomainEvent(string fileName, string description)
{
FileName = fileName;
Description = description;
}
}</pre>
<p>И таким образом наша задача в тестах превращается в почти тривиальную (после некоторых манипуляций с кодом на тему перехвата отправляемых сообщений):</p>
<pre class="brush: csharp;">[Test]
public void Should_set_the_properties()
{
Catch<reviewcreateddomainevent>()
.From(() => {
Review.Create(FILE_NAME, DESCRIPTION);})
.And
.Assert.That(e => e.FileName.Should().Be.EqualTo(FILE_NAME))
.Assert.That(e => e.Description.Should().Be.EqualTo(DESCRIPTION));
}</pre>
<p>Что и требовалось доказать.</p>
<p>На самом деле, решение проблем с тестированием в данном случае является лишь поводом заикнуться о Domain Events. Разработка модели на основе событий пополам с Commands-Queries Separation Principle на сегодняшний день является, пожалуй, одним из самых мощных архитектурных стилей, позволяющих разрабатывать действительно гибкие, устойчивые и масштабируемые системы.</p>
<p>В следующих постах попробуем разработать эту тему глубже.</p>
<p>Добро пожаловать в высшую лигу :)</p>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com2tag:blogger.com,1999:blog-8864435396524375431.post-51040100239711423202009-11-03T00:42:00.001+02:002009-11-03T14:31:51.357+02:00CQS – как вам сущности без публичных свойств?<p>Продолжаем исследовать тему разделения логики запросов от логики команд.</p> <p>Как уже говорилось в предыдущем посте, для моделей со сложным поведением имеет смысл отделить реализацию поведенческих аспектов от так называемой подсистемы отчетов. И мы говорили, что это даст нам возможность очень гибко разрабатывать модель для представления, не затрачивая ресурсы на решение проблем консолидации с моделью для поведения. А что это даст модели команд — начнем выяснять прямой сейчас.</p> <p>Вместо абстрактного введения, небольшая иллюстрация. Вот так может выглядеть код некого условного заказа в нашем «очищенном» домене:</p> <pre class="brush: csharp;">class Order : RootAggregate
{
readonly IList _lines = new List();
readonly DateTime _createdDate;
decimal _totalAmount;
public Order(DateTime createdDate)
{
_createdDate = createdDate;
}
public void AddProduct(string productName, int quantity, decimal price)
{
_lines.Add(new OrderLine(productName, quantity, price));
UpdateTotal();
}
private void UpdateTotal()
{
_totalAmount = _lines.Sum(x => x.Price*x.Quantity);
}
}
public class OrderLine : ValueObject
{
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public decimal Price { get; private set; }
public OrderLine(string productName, int quantity, decimal price)
{
ProductName = productName;
Quantity = quantity;
Price = price;
}
}</pre>
<p>Главная особенность приведенного кода в том, что снаружи доступ к состоянию агрегата закрыт. Т.е. вообще никаких шансов узнать из чего состоит выбранный заказ. Да и не нужно этого — ведь для оценки состояния модели у нас есть отчеты. А в модели поведения возможно только управление поведением.</p>
<p>Все было бы хорошо, если бы у этой стройной идеи не было бы исключений. Иногда состояния бывают нужны внутри самого домена. Например, для заказа на сумму, превышающую некое значение мы должны применить стандартную скидку примерно в таком духе:</p>
<pre class="brush: csharp;">var orderIsApplicableForDiscountSpecification =
new OrderIsApplicableForDiscountSpecification();
if (orderIsApplicableForDiscountSpecification.Matches(order))
order.ApplyDiscount(new StandardDiscount());</pre>
<p>где сама спецификация реализована таким образом:</p>
<pre class="brush: csharp;">class OrderIsApplicableForDiscountSpecification
{
public bool Matches(Order order)
{
return order.TotalAmount >= 10000;
}
}</pre>
<p>Таким образом появляется и свойство TotalAmount у заказа.</p>
<p>На самом деле, никакой мистики здесь нет — все вышепоказанное ни что иное, как обычная банальная инкапсуляция. Просто в свете применения принципа CQS 80% случаев использования информации о состоянии модели просто уходят в небытие и вместе с этим уходит вся необходимая инфраструктура в виде публичных свойств.</p> Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0tag:blogger.com,1999:blog-8864435396524375431.post-62570991537777873242009-10-25T01:47:00.003+03:002011-11-29T02:29:49.401+02:00Command-query separation — две стороны одной модели<style type="text/css">
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, «courier new», courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }</style> <p>Паттерн <a href="http://martinfowler.com/eaaCatalog/domainModel.html" target="_blank">Domain Model</a>, о котором многие наверняка читали в одной <a href="http://www.amazon.com/exec/obidos/ASIN/0321127420" target="_blank">небезызвестной книге</a> в сочетании с подходом <a href="http://en.wikipedia.org/wiki/Domain-driven_design" target="_blank">Domain Driven Design</a> поначалу привлекает множество разработчиков своей логичностью, простотой и универсальностью, которые видятся им в первом приближении. Но как только начинается работа над реальным приложением, то сразу появляется куча нетривиальных проблем, которые вкупе с достаточно строгой «дисциплиной» подхода не дают возможности увидеть каких-то простых и элегантных решений. И тогда проект начинает скатываться в суровую действительность с кучей костылей и инвалидных колясок.</p><span class="fullpost"> <h4>Проблема</h4><p>Разработаем небольшую но типичную CRM. У нас будет база клиентов, контакты и история общения с ними. С точки зрения UI у нас будет список клиентов, формы редактирования деталей отдельно взятого клиента, что-то для управления логом каждого клиента. Все выглядит действительно просто и предсказуемо. За один вечер мы быстренько набрасываем код и к утру заказчик получает готовое к эксплуатации приложение, а разработчик — глубокую сатисфакцию от проделанной работы.</p><p>На следующий день заказчик присылает новое требование: «Хочу видеть в списке клиентов среднее время отклика из логов». Вы кидаетесь в код класса Customer и бодро добавляете свойство AvgCallbackTime примерно такого содержимого:</p><pre class="brush: csharp;">public TimeSpan AverageCallbackTime
{
get
{
return _calls.Average(c => c.CallbackTime);
}
}</pre><p>Добавляете колонку в грид и вуаля. Но откуда-то из вашего кода начинает нести отборным code smell. На следующий день заказчик приходит с еще парой колонок, которые вы так же быстро добавляете. Но одна из колонок в гриде — название последнего заказанного товара, с которым у вас нет никакой связи у кастомера. Вернее, не было до сегодняшнего дня. Сдвинув брови, вы накидывается пару классов в духе CustomerOrder и по длинной цепочке, хитро перебирая все заказы, достаете нужный. И при этом вы стараетесь не думать о том, что приложение стало работать откровенно медленно, утешая себя мыслью о том, что преждевременная оптимизация будет злом.</p><p>И вот настает тот самый день «Х». К вам на рабочий стол ложиться задача: «Реализовать редактируемый список клиентов, активно покупающих товар в заданной категории, с адресом доставки, средней суммой чека, частотой покупок, популярным способом оплаты и фамилией ответственного менеджера. При этом пользователь должен иметь возможность отсортировать и отфильтровать список по производному (в том числе и вычисляемому) полю. Кроме этого, список должен выводиться постранично. Ну и список колонок в выборке должен формироваться на основе уровня доступа пользователя к некоторым критичным данным».</p><p>Думаю, расписывать дальнейший ход мыслей и стиль принимаемых решения будет бессмысленно :)</p><p>Итак, как же нам сохранить модель красивой и лаконичной? Что нам делать с требованиями, касающимися отображения данных и не несущественным с точки зрения каких-то законов поведения объектов?</p><h4>Двойственность природы модели домена </h4><p>Если модель является функцией приложения, то было бы логичным предположить, что на нее распространяются всевозможные архитектурные «правила хорошего тона», а в нашем конкретном случае речь пойдет именно <a href="http://en.wikipedia.org/wiki/Single_responsibility_principle" target="_blank">Single Responsibility Principle</a>. Как сформулировать эту самую единственную ответственность модели домена? Что именно имплементирует эта модель?</p><p>Согласно определению Фаулера, модель домена — это:</p><blockquote><p>An object model of the domain that incorporates both <u>behavior</u> and <u>data</u>.</p></blockquote><p>Но это определение не самое удачное с точки зрения принципа SRP, потому как при наличии двух дополнений в предложении в нем точно так же остается поле для домысливания: что же в модели домена главнее — данные или поведение?</p><p>Собственно, эта неоднозначность понятия и породила куча споров вокруг того, реализовывать ли модель в анемичной форме, или сконцентрироваться в первую очередь на поведении объектов.</p><p>Ведь для решения задач отображения сложных списков хорошо подошла бы анемичная модель, которая была бы описывала в первую очередь состояние объектов и отношения между ними в максимальной нейтральной форме так, чтобы к этой модели совершенно равноправно применялась бы как логика выборок, так и логика модификации.</p><p>Но с другой стороны, правила изменения состояний, отношения и коммуникацию между объектами гораздо легче описывать на основе каких-то «умных» сущностей, не говоря уже о том, что в зависимости от решаемой задачи (моделируемого контекста) одна и та же сущность реального мира может быть смоделирована различными способами.</p><p>Вот мы и попробуем ревизировать понятие модели домена, разбив ее по уровням ответственности таким образом, чтобы и волки были сыты, и овцы целы.</p><h4>Решение рядом</h4><p>Для начала согласимся с посылом, что данные (другими словами, состояние) являются неотъемлемой частью модели, и функция поведения — это всего лишь функция изменения соответствующих данных (состояния).</p><p>Затем, опираясь на опыт решения проблем, описанных выше, предположим, что существует некий особый класс логики, который не совсем корректно реализуем в рамках модели, которая описывает данные с точки зрения поведения (т.е. изменения состояния).</p><p>И вот тут-то мы практически вплотную подошли к тому, чтобы назвать, а затем выделить из модели ту «лишнюю» ответственность, «уродует» ее и мешает нормально развиваться. </p><p>Решение было сформулировано много лет назад <a href="http://en.wikipedia.org/wiki/Bertrand_Meyer" target="_blank">Бертраном Мейером</a>, как одно из основных правил разрабатываемого им языка программирования <a href="http://en.wikipedia.org/wiki/Eiffel_(programming_language)" target="_blank">Eiffel</a>. Принцип называется <a href="http://en.wikipedia.org/wiki/Command-Query_Separation" target="_blank">Command-query Separation Principle</a> и его самая известная формулировка выглядит так:</p><blockquote><p>Аsking a question should not change the answer</p></blockquote><p>Другими словами:</p><blockquote><p>Каждый метод должен либо выполнять какое-то действие, либо возвращать какие-то данные, но никак ни то и другое вместе. </p></blockquote><p>Этот принцип был сформулирован применительно к языку программирования и стал одним из основных правил разработки чистого кода без сторонних эффектов.</p><p>Но как же хороша его формулировка применительно к нашей модели домена, не правда ли? Есть данные, есть запросы, есть методы модификации данных. Нам остается лишь сформулировать этот принцип для архитектуры нашего приложения в целом.</p><h4>Запросы и команды</h4><p>Вкратце, на ранних этапах предлагается вынести из модели домена все операции, которые всего лишь выдают какие-то данные в отдельную подсистему. Таким образом, дизайн приложения будет выглядеть следующим образом:</p><p><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; margin-left: 0px; border-left-width: 0px; margin-right: 0px" title="Command-query separation" border="0" alt="Command-query separation"
src="http://quatrocode.com/blogspot_images/2009/10/CQS1.gif" width="621" height="483" /> </p><p>Другими словами, все данные, которые пользователь <em><strong>видит</strong></em> — подготавливаются подсистемой отчетов: все списки, детали, печатные формы. Как только пользователь подтверждает <strong><em>изменение</em></strong> каких-то данных — его команды обрабатывает модель домена.</p><h4>В чем плюсы?</h4><p>С этого места начинаем собирать урожай полученных преимуществ :)</p><ol><li>Моделью для отчетов служит самая что ни на есть настоящая модель данных со всеми своими нормальными формами, денормализациями и связками по ключам. Таким образом мы получаем возможность достать для пользователя любые данные, не спотыкаясь о репозитории, агрегаты и, прости господи, value objects; </li>
<li>В подсистеме отчетов отпадает необходимость в поддержке сложной инфраструктуры в виде каких-либо ORM, поддержки транзакций и прочего (хотя, о транзакциях <a href="http://www.nhprof.com/Learn/Alert?name=DoNotUseImplicitTransactions" target="_blank">говорят всякое</a>); </li>
<li>Если у нас нет необходимости в сложной инфраструктуре и у нас под руками настоящая схема данных, то самым удобным средством выборки данных является SQL во всех его ипостасях; </li>
<li>Никто не запрещает нам использовать специализированные репортинг-сервисы, всевозможные OLAP и прочие удобности из области Business Intelligence; </li>
<li>Вследствие упрощения инфраструктуры субъективно увеличивается производительность; </li>
<li>Модель домена становится максимально лаконичной и сфокусированной исключительно на поведенческих аспектах бизнес-логики. </li>
</ol><h4>В чем минусы?</h4><p>Не бывает идеальных решений. Главным недостатком я бы назвал дублирование и еще раз дублирование. Это даже общий недостаток, свойственный DDD-подходу, как и любым другим SOLID-решениям в целом: одни и те же данные находят множество воплощений в различных подсистемах, приходится поддерживать множество маппингов, всегда думать о способности модели к рефаткорингу. Все это, безусловно, увеличивает общую стоимость разработки новой функциональности, но в целом, очень значительно уменьшает стоимость модификаций.</p><h4>Что останется в модели?</h4><p>Рефакторинг, связанный с CQS прежде всего коснется репозиториев. В самом лучшем случае репозиторий будет предоставлять только какой-нибудь метод типа GetByID — все остальные методы выборок благополучно уйдут в отчеты.</p><p>Нужно отметить, что возможность полноценного использования всех этих репозеториев, root aggregates, entities и value objects появляется во многом лишь благодаря правильному использованию CQS.</p><p>Из модели исчезнут всевозможные гибридные сущности, задача которых была исключительно в наведении связей между «настоящими» бизнес-сущностями. Как следствие этого, границы между контактами усилятся, уменьшится связанность.</p><h4>Что в итоге?</h4><p>Идея использования отдельных подсистем для отчетов совершенно не нова. Но в данном случае мы увидели, как необходимость в такой системе переросла из всего лишь очередного функционального требования в один из основополагающих архитектурных принципов, который не менее важен, чем, скажем, разделение бизнес-логики на изолированные контексты.</p><p>В целом, CQS является, пожалуй, самой яркой иллюстрацией процесса <a href="http://en.wikipedia.org/wiki/Separation_of_concerns" target="_blank">Separation of Concerns</a>. Кроме этого, понятия <em>запрос</em> и <em>команда</em> применимы к любому аспекту архитектуры системы, связанной с обработкой данных.</p><p>Например он очень здорово отвечает на вопросы, возникающие в области SOA. Вкратце: все в курсе, что самый гибкий и удобный способ общение между компонентами — это асинхронный обмен сообщениями, но как быть со случаем, когда компонент просто должен предоставить какую-то информацию только для чтения, причем, в синхронном режиме? Об этом <a href="http://www.udidahan.com/2008/08/11/command-query-separation-and-soa/" target="_blank">интересно написал</a> <a href="http://www.udidahan.com" target="_blank">Udi Dahan</a>.</p><p>У <a href="http://domaindrivendesign.org/books#DDD" target="_blank">Эванса</a> об этом подходе ничего не сказано, и это неспроста. Дело в том, что сам по себе принцип CQS не имеет ничего общего с DDD. Этот принцип всего лишь обеспечивает некие предусловия, которые всего лишь делают возможным применение паттернов DDD. При этом никакого влияния на дизайн модели домена он не оказывает — это важно помнить и понимать.</p></span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com3tag:blogger.com,1999:blog-8864435396524375431.post-50197345772169841882009-07-15T20:44:00.007+03:002011-11-29T02:26:17.449+02:00Hexagonal/Onion Architecture - слоим приложения<p>На третий день отпуска в деревне мозг потребовал активных развлечений. Гибсоновская «<a href="http://en.wikipedia.org/wiki/The_Difference_Engine">Машина различий</a>» благополучно переварилась и очередным кандидатом на расправу был выбран порядком подзабытый блог, в черновиках которого зависло уже приличное количество тем на проработку.</p><p>Отпуск — пора легкого времяпровождения, поэтому и тему для препарирования я выберу полегче, чтобы кода поменьше, а картинок побольше. Вот и поговорим мы в очередной раз про распределение обязанностей и управление зависимостями, но на сей раз на уровне модулей приложения.</p><span class="fullpost"> <h4>Архитектура по-умолчанию</h4><p>Итак, мы начинаем новый проект. Чтобы легче было работать, пусть это будет какой-нибудь интернет-магазин со вполне себе ординарной функциональностью: какая-нибудь витрина, корзина и система заказов. Открыв любимую IDE, через несколько пассов у нас получится приложение с примерно такой структурой: </p><div style="text-align: center;"><img alt="Default Architecture" style="border-width: 0px; max-width: 800px;"
src="http://quatrocode.com/blogspot_images/2009/07/1-hexagonalonion-architecture.gif" border="0" /> </div><p>Стрелочками обозначены ссылки между проектами. </p><p>Наверняка, сначала будет разработана база данных. Затем через какой-нибудь DAL сущности из проекта BusinessLogic будут этими самыми данными манипулировать. Ну и UI-проект все это будет отображать, собирать какой-то пользовательский ввод и дергать за ниточки бизнес-логику. </p><p>Все бы хорошо, но данная схема подразумевает первичность базы данных. Ее разработка влияет на все остальные уровни. И в случае, если мы хотим смоделировать бизнес-логику, используя Domain Driven Design, это влияние будет столь значительным, что рано или поздно все попытки разработать полноценную модель скатятся в использование <a href="http://www.martinfowler.com/eaaCatalog/activeRecord.html">Active Record</a> или <a href="http://www.martinfowler.com/eaaCatalog/tableModule.html">Table Module</a>, похоронив все дальнейшие попытки использования преимуществ возможностей богатой модели. </p><p>Кроме того, сильная связь с базой данных значительным образом усложняет тестирование: при всех возможностях современных фреймворков, тестирование такого кода всегда остается одним большим компромиссом. </p><p>А что если бы появилась возможность сконцентрироваться на разработке модели домена, как на первоочередном модуле? </p><h4>Ресурсами наружу</h4><p>Решение проблемы с первоочередностью разработки модели домена (как, в общем случае, и любой другой логики, связанной с разрабатываемым приложением) была хорошо сформулирована Алистером Кокберном в виде паттерна <a href="http://alistair.cockburn.us/Hexagonal+architecture">Hexagonal Architecture</a>. </p><p>Вкратце, идея заключается в том, что все внешние процессы и ресурсы по отношению к разрабатываемой системе должны подключаться через специальные порты и адаптеры. К этим ресурсам относится файловая система, сетевые хранилища, всевозможные API третьих систем и, что имеет ключевое значение — база данных и пользовательский интерфейс.</p><p>Перерисовав структуру нашего приложения согласно вышеописанному паттерну, мы получим примерно следующее: </p><div style="text-align: center;"><img alt="Hexagonal Architecture" style="border-width: 0px; max-width: 800px;"
src="http://quatrocode.com/blogspot_images/2009/07/2-hexagonalonion-architecture.gif" border="0" /> </div><p>Таким образом срабатывает принцип обращения зависимостей, когда код различных уровней связываются исключительно через абстракции, не используя никаких деталей реализации: бизнес-логика знает лишь, что какие-то сущности куда-то сохраняются, работая с высокоуровневым репозиторием, а так же, что реакцию на пользовательский ввод нужно осуществлять через такие же высокоуровневые контроллеры. </p><p>Кажется, мы ответили на вопрос о том, как развернуть зависимости так, чтобы приоритет разработки сместился в сторону бизнес-логики с логики сохранения данных. Но как быть с логикой уровня приложения? Что делать с наддоменными сервисами, например, с сервисами управления доступом? С одной стороны, мы можем подключить такой сервис через пару порт-адаптер, но с другой стороны, мы таким образом не решим вопрос изоляции логики уровня домена от логики уровня приложения. </p><h4>Центр управляет всем</h4><p>В качестве решения этой проблемы предлагается использование так называемой «луковой архитектуры» (<a href="http://www.google.com.ua/url?sa=t&source=web&ct=res&cd=2&url=http%3A%2F%2Fjeffreypalermo.com%2Fblog%2Fthe-onion-architecture-part-1%2F&ei=HSJeSpTXDJz0nQOo55x8&usg=AFQjCNE6rW_Sxywe5yqIYzFWx8WmJE1LSQ&sig2=wJupfHeeU_2kgmkSsaGhTQ">onion architecture</a>). У этого паттерна такая же мотивация, что и у гексагональной архитектуры, но решение строится не просто на отделении абстракций от реализации, а на управлении доступом между различными уровнями архитектуры. </p><p>Если мы перерисуем структуру нашего проекта, согласно предложенной схеме, то у нас получится такой рисунок: </p><div style="text-align: center;"><img alt="Onion Atchitecture" style="border-width: 0px; max-width: 800px;"
src="http://quatrocode.com/blogspot_images/2009/07/3-hexagonalonion-architecture.gif" border="0" /> </div><p>Смысл данного подхода заключается в следующем: </p><ol><li>В ядре системы находится модуль, не связанный ни с чем. Чистая абстракция предметной области;</li>
<li>Другие модули выстраиваются вокруг ядра таким образом, чтобы их зависимости были направлены вовнутрь к ядру;</li>
<li>Зависимости в противоположную торону от ядра недопустимы;</li>
<li>Оболочка системы состоит из модулей конкретных реализаций внутренних асбтракций.</li>
</ol><p></p><p>Таким образом мы видим, что предлагаемый паттерн работает точно так же, как и гексагональная архитектура, но дополнение ко всему устанавливает правила игры для внутренней структуры многогранного ядра. </p><h4>Заключение</h4><p>Трехзвенная архитектура не давала нам ответы на вопросы о том, как нам проектировать приложение, опираясь в первую очередь на модель домена, изолировав ее от инфраструктуры и логики уровня приложения. В то время, как вооружившись принципом обращения зависимостей мы увидели, как можно строить приложения вокруг какого угодно центра. </p><p>Но важно отметить, что рассмотренные паттерны помогают нам строить так называемую горизонтальную структуру приложения, в то время как на прикладном уровне существует своя техника определения границ контекстов и их изоляцией. </p><p>Кроме того, не стоит забывать об использовании всевозможных Dependency Injection-фреймворкам, которые помогут избежать трудностей при управлении временем жизни объектов в такой вот «вывернутой» архитектуре. </p><p>Отпуск продолжается :)</p></span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com14tag:blogger.com,1999:blog-8864435396524375431.post-45489933430582552012009-02-03T05:12:00.004+02:002009-07-15T20:47:10.562+03:00Defensive Design. Откуда берутся сбои<p>Так сложилось, что если о всяких паттернах и архитектурах на собеседованиях соискатели более или менее в состоянии поддержать разговор, то как только речь заходит о вариантах использования Debug.Assert(…), сразу же начинается плавание вокруг да около. Нет, я не говорю, что никто не может сформулировать основных положений «оборонительного проектирования», просто очень редко случаются действительно «концептуальные» беседы: как правило, приходится поднимать знания, которые присутствуют где-то на уровне подсознания на более высокий уровень. Поэтому я попробую организовать небольшую шпаргалку, дабы помочь коллегам.</p> <span class="fullpost"> <h4>Введение</h4> <p>Для начала вспомним знаменитый <a href="http://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BA%D0%BE%D0%BD_%D0%9C%D1%91%D1%80%D1%84%D0%B8" target="_blank">Закону Мёрфи</a> (в общем виде) применительно к разработке ПО: <em>«если существуют условия, в которых программа ведет себя неверно, то такие условия обязательно проявятся»</em>. Это может быть неверный пользовательский ввод, сбои в железе, рассогласование протоколов или же ошибки внутри самой программы. Так вот, оборонительное проектирование и борется со всевозможными проявлениями Закона Мёрфи – эта техника <strong>обороняет</strong> систему от того, что «никогда не может случиться».</p> <p>Таким образом, получается, что залогом качества системы в равной степени является умение реагировать как на запланированные, так и на незапланированные варианты использования. Более того, <em>большинство уже используемых </em>техник разработки программного обеспечения можно выстроить в некую супертехнику для проектирования и имплементации систем, которые будут либо абсолютно невосприимчивы к нештатным ситуациям, либо влияние этих ситуация будет минимизировано.</p> <p>Например, включив телевизор в сеть, мы хотим увидеть картинку на экране. И мы ее увидим. Но если в сети случится скачок напряжения, то едва ли дым из телевизора будет признаком качества. Скорее всего, нас больше обрадует сгоревший предохранитель, который и выполнял <em>оборонную функцию</em> в данной системе.</p> <p>Как повелось, любая теория в области разработки ПО, как и в любой другой <a href="http://en.wikipedia.org/wiki/Engineering" target="_blank">инженерной дисциплине</a>, состоит из двух частей: теоретической и практической. А в результате симбиотического слияния этих двух компонентов на свет появляются <a href="http://en.wikipedia.org/wiki/Pattern" target="_blank">паттерны</a>, или же просто рекомендации, которые группируются по различным признакам.</p> <p>Ниже я попробую собрать информацию о таких вот рекомендациях, использовав в качестве группирующей характеристики источники возникновения сбоев в системе.</p> <h4>Неверные данные</h4> <p>Этот принцип в частной форме очень хорошо знаком всем UI-программистам: пользователь всегда глуп. Никогда не стоит наедятся, что он введет емейл-адрес в правильном формате или будет аккуратно следить за максимально допустимым значением для какого-то поля. Поэтому программа <strong>должна защитить себя сама</strong> от подобных глупостей. Для этого UI-программисты используют всевозможные контролы-валидаторы. А более продвинутые коллеги реализуют целые подсистемы валидации данных.</p> <p>Если же рассмотреть этот принцип в рамках концепции оборонительного проектирования, то можно сказать обобщенно, что любой программный модуль (функция, класс, сервис и т.д.) не должен никоим образом доверять входным данным. В случае же, если обнаружится, что аргументы не удовлетворяют каким-то условиям, модуль должен корректно обработать эту ситуацию.</p> <p>В общем случае, есть три правила обработки входных данных:</p> <ol> <li>Проверке подлежат все данные из внешних источников; </li> <li>Проверке подлежат все входные параметры для данного модуля; </li> <li>Должна существовать четкая стратегия обработки неверных данных. </li> </ol> <h4>Плохой дизайн, грязный код</h4> <p>В общем-то, вряд ли кто-то будет спорить, что качество дизайна и исходного кода всесторонне влияют на устойчивость системы, компрометируя систему изнутри. Прямо или косвенно, все архитектурные практики можно запросто включить в этот список:</p> <ol> <li>Неоправданная сложность – потенциальный источник проблем (<a href="http://en.wikipedia.org/wiki/You_Ain%27t_Gonna_Need_It" target="_blank">YAGNI</a>, <a href="http://en.wikipedia.org/wiki/KISS_principle" target="_blank">KISS</a>, <a href="http://en.wikipedia.org/wiki/Don%27t_repeat_yourself" target="_blank">DRY</a>); </li> <li>Повышение абстракции помогает изолировать проблемы (<a href="http://igor.quatrocode.com/2008/09/solid-top-5.html" target="_blank">SOLID</a>); </li> <li>Всестороннее тестирование, особенно на граничных и неверных условиях поможет выявить проблемы на ранних этапах (<a href="http://en.wikipedia.org/wiki/Software_testing" target="_blank">Software Testing</a>); </li> <li>Чем меньше кода пишется с нуля – тем меньше в нем возникает ошибок (<a href="http://en.wikipedia.org/wiki/Code_reuse" target="_blank">Code Reuse</a>, <a href="http://en.wikipedia.org/wiki/Metaprogramming" target="_blank">Metaprogramming</a>); </li> <li>Инспекция кода поможет выявить проблемы на уровне кодирования (<a href="http://en.wikipedia.org/wiki/Code_review" target="_blank">Code Review</a>, <a href="http://en.wikipedia.org/wiki/Code_audit" target="_blank">Code Audit</a>). </li> </ol> <h4>Legacy-код, сторонние компоненты</h4> <p>Вместе с функциональностью от старого кода или сторонних компонентов наследуются еще и их проблемы. Важно помнить об этом. Особенно, если они не были достаточно хорошо протестированы, документированы или банально плохо разработаны.</p> <p>Но даже хорошо написанный код унаследованных компонентов может стать источником проблем при использовании. Тривиальным примером является использование 16-битных инструкций в коде, который запускается на 64-битной операционной системе. Особенно, когда <a href="http://msdn.microsoft.com/en-us/library/bb756962.aspx" target="_blank">производители ОС заявляют о прекращении поддержки 16-битного API</a> :) Еще одной знаменитой проблемой такого рода была «<a href="http://en.wikipedia.org/wiki/Y2K" target="_blank">Проблема 2000</a>».</p> <p>Всем этим системным компонентам свойственны те же недостатки, что и основному коду. С той лишь разницей, что повлиять на них можно лишь косвенно, потому как чаще всего они распространяются в виде готовых библиотек или сервисов, код которых по понятным причинам невозможно исправить, или же, если к исходному коду доступ есть, то вносить сколь либо существенные изменения будет неоправданно дорого.</p> <p>В общем-то, все проблемы, связанные с legacy-кодом решаются <strong>установлением контроля над ним через изоляцию и рефакторинг</strong>. Изоляция позволяет нам максимально <strong>уменьшить зависимость</strong> и, как следствие, влияние проблемного компонента на систему в целом. А рефакторинг <strong>привносит в код недостающие элементы обороны</strong>.</p> <p>Все эти техники подробно расписаны в хрестоматийной книге <a href="http://www.amazon.com/Working-Effectively-Legacy-Robert-Martin/dp/0131177052" target="_blank">Working Effectively with Legacy Code</a> (этот же материал, но в более компактной форме <a href="http://www.objectmentor.com/resources/articles/WorkingEffectivelyWithLegacyCode.pdf" target="_blank">доступен в виде статьи</a>).</p> <h4>Безопасность</h4> <p>Пользователь может быть не только глупым, как в случае с непредумышленным вводом неверных данных, но и вредным. Существует такая категория систем, сбой в которых может быть целью для некоторых пользователей. Это тот самый случай, когда смысл термина «оборонительное проектирование» оправдан на все 100%: разрабатываемая система должна быть устойчива к целенаправленным атакам.</p> <p>Список рекомендаций для проектирования безопасных систем никогда не будет сформулирован окончательно. По сути, идет нескончаемая война между разработчиками и хакерами: война со своими стратегиями, героями, со своими победами и поражениями. И список рекомендаций было правильнее назвать <em>списком <a href="http://ru.wikipedia.org/wiki/%D0%A2%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B0" target="_blank">тактик</a></em>. Этот список будет пополняться по мере изобретения <a href="http://en.wikipedia.org/wiki/Security_exploit" target="_blank">новых способов атак на системы</a>. Приведу лишь самые основные из них:</p> <ol> <li>Любой код небезопасен, пока не доказано обратное; </li> <li><a href="http://en.wikipedia.org/wiki/Secure_input_and_output_handling" target="_blank">Безопасная обработка входных и выходных данных</a>: доверяйте только проверенным источникам, ждите неприятностей на входе, никогда не сообщайте больше информации, чем нужно; </li> <li><a href="http://en.wikipedia.org/wiki/Principle_of_least_privilege" target="_blank">Ограничивайте привилегии</a> на уровне необходимого минимума для функционирования данного программного модуля; </li> <li>Неграмотная защита сродни ее отсутствию. Поэтому нужно защищаться <strong>от известных вам </strong>типов атак и <strong>всегда продолжать изучать новые типы атак</strong>; </li> <li>Пользуйтесь <a href="http://en.wikipedia.org/wiki/Cryptography" target="_blank">криптографией</a>; </li> <li>Проводите <a href="http://en.wikipedia.org/wiki/Security_audit" target="_blank">регулярный аудит системы безопасности</a>. </li> </ol> <h4>Паранойя</h4> <p>Как ни странно это прозвучит, избыточное увлечение оборонительными техниками чревато появлению брешей в обороне. Дело в том, что сам по себе код, который пишется для этих целей, может быть не лишен всех тех недостатков, которые присущи «основному» коду: код может быть достаточно сложным, медленным и полным всяких сюрпризов. Поэтому крайне важно <strong>научиться оценивать необходимый уровень защищенности системы</strong>.</p> <p>Например, при разработке программы для работы с домашней медиа-библиотекой на 40 человеко-часов вряд ли имеет смысл проводить регулярные security audits по критериям, которые обычно предъявляются к банковским системам. Или же проводить скрупулезное тестирование с использованием эвристических алгоритмов, как если бы вы разрабатывали систему для систем жизнеобеспечения или авиадиспетчерских. Но в то же время, вполне оправдано внимание к защите приложения от некорректных входных данных.</p> <h4>Заключение</h4> <p>Мы попытались сформулировать список типичных источников сбоев в системах и составить список типичных рекомендаций по их предотвращению. Конечно, многое было опущено из за всеобъемлющей природы затронутой темы. Например, я умышленно не упомянул о роли человеческого фактора в разработке защищенных систем: политические, эмоциональные, социологические. Тема безопасности программного обеспечения так же была затронута лишь поверхностно.</p> <p>В следующих постах я продолжу «конспект», останавливаясь более детально на отдельных аспектах.</p> </span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com11tag:blogger.com,1999:blog-8864435396524375431.post-7141552915239625032009-01-19T16:19:00.001+02:002009-01-19T16:19:45.675+02:00Горячая вакансия<p>Пост немного не по теме блога, но мне кажется, что кое-кому он может быть интересен. Собственно, из заголовка видно, что у нас открылась вакансия в Киеве, поэтому, всем, кому интересно то, о чем я пишу, всем, кто хочет попробовать все это в деле – засылайте свои резюме на <a href="mailto:cv1@justapplications.co.uk">cv1@justapplications.co.uk</a>.</p> <span class="fullpost"> <p>Вкратце, мы ищем разработчика на позицию .NET Senior Developer. За этим названием скрываются требования:</p> <ul> <li>Владение техниками объектно-ориентированного проектирования и программирования </li> <li>Agile-навыки </li> <li>Знание C# 3.0, платформы .NET 3.5; </li> <li>ASP.NET (желательно ASP.NET MVC); </li> <li>MS SQL  (желателен опыт работы с NHibernate); </li> <li>Технический английский. </li> </ul> <p>Дискуссия по каждому из пунктов на собеседовании :)</p> <p>Со своей стороны мы платим хорошую зарплату и бонусы, обеспечиваем стандартным социальным пакетом, даем возможность для всяческого профессионального роста, снабжаем интересной и сложной работой, составляем компанию в пятничных посиделках.</p> <p><strong>Кто мы?</strong> “Витринная” информация на <a href="http://justapplications.co.uk/">нашем сайте</a>. Начинали пару лет назад с разработки небольшой утилитки для людей, которые торгуют на eBay. Сегодня же у нас накопился целый букет разнообразных проектов и услуг + неплохой рост. Но на сегодняшний день мы выросли до уровня, когда силами распределенной команды задачи уже не решаются так хорошо, как раньше. Поэтому сейчас мы хотим собрать всю разработку под одной крышей в славном городе Киеве.</p> <p><strong>Что за проект?</strong> Сразу скажу, что проект долгосрочный. Выше я упомянул о букете сервисов и услуг, которые мы сейчас предоставляем. Так вот, новый проект – это разработка цельного комплексного решения, которое объединит в себе уже готовые продукты и расширится новыми. Сейчас мы разрабатываем ядро этой системы, на которую потом будет навешено все остальное. В общем, все достаточно интересно. Есть где развернуться. К этой же предметной области имеют отношение такие системы, как <a href="http://www.channeladvisor.com/" target="_blank">ChannelAdvisor</a>.</p> </span> Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com3tag:blogger.com,1999:blog-8864435396524375431.post-52457159757131927042008-12-31T04:21:00.001+02:002009-01-15T21:11:07.048+02:00Дао Scrum<p>Много бумаги исписано и и мегабайтов перекачано в процессе, когда одни пытаются понять, что же такое <a href="http://www.developers.org.ua/archives/krivitsky/2008/07/17/scrum-for-developers/" target="_blank">Scrum</a>, а другие пытаются это объяснить. Главное достоинство Scrum в этом смысле заключается в том, что для того, чтобы начать его применять не нужно многого – бери готовые несложные правила, минимум артефактов, и колесики закрутились. Но понимание глубокой сути этого процесса наступает далеко не сразу.</p> <span class="fullpost"> <p>Как известно, центром любого scrum-проекта является product backlog. Это место, куда product owner записывает свои пожелания, оценивая их значимость и вместе с командой планирует итерации. Казалось бы, что может быть проще? Но чем же этот подход оказался лучше других? Чем он отличается, скажем, от случая, когда заказчик просто заводит тикеты, например, в JIRA? Что делает этот проект столь эффективным? </p> <p>Для того, чтобы это понять, достаточно взглянуть на <a href="http://lingvo.yandex.ru/en?text=backlog&lang=en&search_type=lingvo&st_translate=on" target="_blank">перевод термина “backlog”</a> (ну, или в толковый словарь для носителей языка):</p> <blockquote> <p>backlog</p> <ol> <li>долг, задолженность </li> <li>невыполненные заказы </li> <li>резервы (товаров, материалов и т. п.) </li> </ol> </blockquote> <p>На мой взгляд, значение этого термина – квинтэссенция всего процесса. Попробуем разобрать его по пунктам.</p> <h4>Backlog – это не список задач для разработчиков</h4> <p>Нет, конечно, в конечном счете разработчик работает со списком конкретных задач. Но изначально, с точки зрения product owner, список конкретных задач – это скучные подробности. В первую очередь из перевода следует, что backlog – это <strong>не список задач </strong>и даже <strong>не список user stories</strong>, которые нужно выполнить, а список <strong>недостатков системы</strong>. Под недостатками подразумеваются недостающая функциональность, недостаточное качество реализации, недостаточное соответствие ожиданиям бизнеса и так далее. Тут видно замечательное свойство термина “недостаток”, которое заключается в том, что оно обобщает артефакты нескольких подпроцессов: фича, улучшение, дефет. </p> <p>Таким образом, весь жизненный цикл продукта представляется нам как процесс бесконечного улучшения, <strong>процесс управления недостатками.</strong></p> <p>У такого подхода, среди прочих достоинств, есть одно важное преимущество: он построен на позитивном мышлении творцов, стремящихся в своей работе к совершенству, подогревая творческий потенциал участников процесса. Ведь даже Микеланджело был приверженцем такого принципа: на вопрос о том, как он создает столь совершенные скульптуры, он ответил “Я просто беру резец и отсекаю от мрамора все лишнее”. Правда похоже? :)</p> <h4>Подразумевается, что приложение существует на этапе планирования</h4> <p>Хорошо известный постулат любого agile-проесса напрямую следует из предыдущего пункта. Ведь прежде чем определить недостатки системы, да еще и определить их степень важности, нужно иметь некую текущую версию, глядя на которую и можно сказать, чего ей не достает. Нужен тот самый кусок мрамора. Ведь <strong>сложно искать недостатки в белизне чистого листа бумаги</strong>.<em> </em>Поэтому и существуют правило о доступе product-owner-а к актуальной <em>рабочей</em> версии приложения пополам с особым акцентом на коммуникации между заказчиком и разработчикам. </p> <h4>Выживают важнейшие требования </h4> <p>Получается, что <strong>процесс управления недостатками</strong> подчеркивает <strong>эволюционную природу</strong> Scrum. Планирование каждого спринта – это, по сути, <strong>естественный отбор задач</strong>, где критерием отбора является степень ее важности для приложения на данном этапе эволюции. А механизм отбора универсален – борьба за ресурсы: мы просто ограничиваем продолжительность спринта. Ну и важно спланировать спринт так, чтобы по его окончанию мы получили полноценный экземпляр приложения, который должен быть как минимум не хуже предыдущего.</p> <h4>К чему это все?</h4> <p>По сути, главное отличие Scrum от других методологий заключается в том, что он дает четкий и однозначный ответ на вопрос о том, какими критериями следует руководствоваться при планировании работ, помогая разогнать проект с низкого старта. Управление недостатками – удачная метафора. Ведь не зря перед началом любых работ важно определить цель, которую необходимо достичь. А если воспользоваться этой метафорой, то задача определения цели значительно упростится.</p> <p>Всех с Новым годом! :)</p> <p></p> </span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0tag:blogger.com,1999:blog-8864435396524375431.post-6715948798071244302008-12-15T00:00:00.005+02:002011-12-26T23:15:39.840+02:00DSL, валидация и jQuery<p>В комментариях к моему <a href="http://igor.quatrocode.com/2008/10/domain-specific-language.html" target="_blank">предыдущему посту</a> был задан ожидаемый вопрос о том, как встроить логику валидации в код формы. Об этой возможности я упомянул лишь вскользь и вот сейчас расскажу о том, каким образом эта задача решилась в одном из моих текущих проектов.</p><span class="fullpost"> <h4>Введение</h4><p>Итак, напомню, что мы реализовали на данный момент:</p><ol><li>Объектная модель DSL валидации; </li>
<li>Удобный механизм конструирования синтаксического дерева; </li>
<li>Интерпретатор, который транслирует синтаксическую структуру дерева в логику валидации. </li>
</ol><p>Для демонстрационных целей рассмотрим два альтернативных подхода - один с использованием серверных валидаторов ASP.NET, а второй будет чисто клиентским. Таким образом, мы увидим, как разделение логики DSL на декларативную и интерпретационную части помогает нам расширять его функциональность.</p><p>Ключевое решение, которое нам нужно принять на самом начальном этапе, касается непосредственно механизма динамического добавления логики валидации на форму. Нам нужно подумать, во что же мы, в конце концов, будем превращать наш код на самом низком уровне.</p><p>Затем мы разработаем "компилятор", который и будет превращать код, написанный на нашем DSL в логику валидации, которая будет встраиваться в форму по продуманному заранее сценарию.</p><h4>Исходный код валидатора и модификации в DSL</h4><p>В качестве подопытного кролика на сей раз выберем класс Person:</p><pre class="brush: csharp;">public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Comments { get; set; }
}</pre><p>Интересным тут является наличие поля Email, для которого должно срабатывать условие соответствия шаблону. Поэтому модифицируем структуру констрейнтов, приведенную в предыдущем примере:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="324" alt="Email Constraint" src="http://quatrocode.com/blogspot_images/2008/12/1_EmailConstraint.png" width="231" border="0" /></p><p>Далее это позволит нам добавить другие типы констрейнтов, логика которых может быть реализована с использованием регулярных выражений.</p><p>Таким образом, пусть код валидатора будет таким:</p><pre class="brush: csharp;">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("Введите правильный емейл");</pre><h4>Динамическое добавление серверных валидаторов</h4><p>Для начала приведем код формы, которую мы будем расширять логикой валидации:</p><pre class="brush: csharp;"><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></pre><p>В приведенном фрагменте вместо стандартного контрола TextBox мы использовали его расширенную версию ValidableTextBox, в котором появилось поле Type. Для чего это нужно? Дело в том, что мы должны <em>связать</em> контрол на форме с конкретным свойством конкретного класса. В простейшем случае имя свойства можно использовать в качестве идентификатора для контрола, а свойство Type будет использоваться для указания типа, к которому это свойство принадлежит.</p><p>В WebForms валидаторы - это обычные серверные контролы, поэтому добавление валидаторов на форму - это обычная манипуляция коллекцией контролов:</p><pre class="brush: csharp;">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);
}
}</pre><p>В ходе экспериментов с динамическим добавлением контролов мы увидели, что констрейнт StringMaxLengthConstraint разумно реализовать на базе RegularExpressionConstraint, потому что ограничение по длине поля с использованием серверных валидаторов в WebForms реализуется с помощью RegularExpressionValidator.</p><p>Показательным является то, что перекроив логику работы StringMaxLengthConstraint мы никак не повлияли <em>на синтаксис </em>нашего DSL.</p><h4>Динамическая генерация javascript-кода для валидации</h4><p>Раз уж речь пошла о том, что второй подход у нас будет ориентирован на использование <a href="http://www.asp.net/mvc/" target="_blank">ASP.NET MVC</a>, то логично будет развить эту мысль и сказать, что код клиентской валидации мы будем писать, используя <a href="http://jquery.com/" target="_blank">jQuery</a>. И раз уж мы решили использовать jQuery, то стоит взглянуть на плагин <a href="http://bassistance.de/jquery-plugins/jquery-plugin-validation/" target="_blank">Validation</a>. Главным плюсом этого плагина является то, что вся логика описывается в декларативном стиле и может быть локализована в одном месте.</p><p>Код нашей формы будет выглядеть так:</p><pre class="brush: xml;"><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></pre><p>По аналогии с серверными контролами, нам нужно связать поле ввода со свойством нужного нам типа. Для этого можно просто дать правильные имена полям, составив их из имени типа и названия свойства. </p><p>И в этом случае код валидации будет таким:</p><pre class="brush: js;">$(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: "Введите правильный емейл"
}
}
}
)
}
);</pre><p>Как видим, в отличии от серверных контролов для проверки длины поля и его соответствия шаблону используются готовые спецификаторы maxLength и email. Далее мы увидим, как констрейнты в нашем DSL спроектированны таким образом, что они могут без труда интерпретироваться и как RegularExpressionValidator с конкретным паттерном, и как спецификаторы maxlength и email.</p><h4>Автоматизация генерации кода</h4><p>Стандартным решением для пододбных задач являтся использование <a href="http://en.wikipedia.org/wiki/Visitor_pattern" target="_blank">паттерн Visitor</a>. Именно реализацию этого паттерна все желающие могут найти в недрах исходников LINQ в виде абстрактного intenal-класса ExpressionVisitor. Итак, напомним, в нашем примере синтаксическая структура DSL описывается иерархией констрейнтов. Поэтому разработаем абстрактный Visitor с реализациями метода Visit для каждого из типов констрейнтов и добавим две реализации для каждого из типов валидации:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="587" alt="2_Visitors"
src="http://quatrocode.com/blogspot_images/2008/12/2_Visitors.png" width="486" border="0" /></p><p>"Кирпичиком" наших валидаторов является инетрфейс IRule. Модифицируем его так, чтобы он выполнял роль Element для паттерна Visitor:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="348" alt="3_ValidatorElement"
src="http://quatrocode.com/blogspot_images/2008/12/3_ValidatorElement.png" width="352" border="0" />  </p><p>При этом реализация метода Accept для каждого из констрейнтов будет выглядеть почти одинаково:</p><pre class="brush: csharp;">public class StringNotNullOrEmptyConstraint : IConstraint
{
....
public void Accept(IValidatorVisitor visitor)
{
visitor.Visit(this);
}
}</pre><p>Т.е. единственная польза от этого метода - полиморфная диспетчеризация вызовов.</p><p>Точкой входа для всей конструкции является метод VisitValidator. Этот метод получает на вход список валидаторов, которые необходимо "посетить", перебирая их, вызывает метод Accept. А сами констрейнты "возвращают" вызовы в Visitor через метод Visit, как было показано выше.</p><p>Реализация метода VisitValidator для WebFormValidatorVisitor:</p><pre class="brush: csharp;">public void VisitValidator(IEnumerable<IRulesGroup> rulesGroups)
{
foreach (var validator in rulesGroups)
foreach (var rule in validator.Rules)
rule.Accept(this);
}</pre><p>Обернув логику связывания в некие классы-хелперы, на примере JQueryValidatorVisitor код использования будет выглядеть так:</p><pre class="brush: xml;"><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></pre><p>Реализация метода ValidateForm для случая с использованием JQueryValidatorVisitor:</p><pre class="brush: csharp;">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);
}</pre><p>Методы Visit в свою очередь обрабатывают каждый конкретный констрейнт, "накапливая" и возвращая результат инерпретации клиентскому коду. Например, вот так вот выглядит код одного из методов класса WebFormValidatorVisitor:</p><pre class="brush: csharp;">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);
}</pre><h4>Заключение</h4><p>В последних двух постах мы увидели лишь верхушку огромного айсберга, под названием DSL. Эта тема сейчас активно разрабатывается многими крупными игроками на рынке средств разработки. Главное направление этих изысканий - упрощение разработки и внедрения DSL в приложения. Например, <a href="http://www.microsoft.com" target="_blank">Microsoft</a> на последней <a href="http://microsoftpdc.com/" target="_blank">PDC</a> анонсировала интересный продукт, под названием <a href="http://www.microsoft.com/soa/products/oslo.aspx" target="_blank">Oslo</a>. Хороший <a href="http://martinfowler.com/bliki/Oslo.html" target="_blank">обзор</a> написал <a href="http://martinfowler.com/" target="_blank">Мартин Фаулер</a>. Кроме этого, старина Мартин работает над книгой по DSL. Как обычно, это будет каталог паттернов, с которым уже <a href="http://martinfowler.com/dslwip/" target="_blank">можно познакомиться</a> у него на сайте. А еще, совсем недавно <a href="http://www.jetbrains.com" target="_blank">JetBrains</a> выпустили бета-релиз своего нового продукта <a style="white-space: nowrap" href="http://www.jetbrains.com/mps/index.html" target="_blank">Meta Programming System</a>, который решает схожие задачи с Microsoft Oslo. И что-то мне подсказывает, впереди нас ждет еще много интересного.</p><p>Как всегда, полную реализацию можно изучить, <a href="http://quatrocode.com/blogspot_files/DSL2.zip" target="_blank">скачав рабочий пример</a>.</p></span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com2tag:blogger.com,1999:blog-8864435396524375431.post-43337140690518598272008-10-22T21:16:00.006+03:002011-12-26T23:12:45.643+02:00Domain Specific Language в своем приложении - это просто<p>Как показывает практика, львиную долю времени в проектах по разработке и сопровождению корпоративных приложений занимает поддержка системы в актуальном состоянии в процессе изменения существующих и появления новых требований: появляется новый тип отчета, новые поля в базе данных для каких-то сущностей, меняются форматы данных, используемых для какого-то внешнего сервиса и т.д. Вся эту рутина влияет на процесс разработки на протяжении всего его жизненного цикла.</p><p>Один из подходов, который упрощает задачи разработки бизнес-логики в таких условиях является использование специальных проблемно-ориентированных языков (DSL). Теория таких языков - это <a href="http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F" target="_blank">самостоятельная большая тема</a> для изучения. Я же хочу на примере шаг за шагом показать все типичные этапы разработки DSL для валидации данных. И это будет действительно просто :)</p><span class="fullpost"> <h4>Описание предметной области</h4><p>Задача валидации данных хорошо известна всем всем разработчикам. Будь то данные, которые пользователи вводят на формах, сохраняют в базу, или отправляют на внешние сервисы - все они должны удовлетворять каким-то специальным правилам, быть правильно отформатированными, быть обязательными и т.д. </p><p>Данные проверяются разными способами по одинаковым правилам в разных частях приложения. Кроме того, различным способом обрабатываются и ошибки. А в других случаях они просто предотвращаются.</p><p>Стандартной проблемой в данном случае является дублирование знаний о правилах валидации вследствие того, что их встраивают в сам механизм проверки. И решением этой проблемы было бы введение унифицированного механизма описания правил, которые бы потом применялись на различных уровнях. Например, на основании этих правил генерировались бы валидаторы для полей на веб-формах, проверялись входные данные в методах веб-сервисов и выходные перед сохранением в базу данных.</p><h4>Рабочий пример </h4><p>Определим в нашем приложении класс Product следующим образом:</p><pre class="brush: csharp;">public class Product
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
}</pre><p>Экземпляры этого класса будут сохраняться в базу данных в таблицу Product:</p><pre class="brush: sql;">CREATE TABLE Product
(
[Id] INT IDENTITY PRIMARY KEY,
[Title] NVARCHAR(5) NOT NULL,
[Description] NVARCHAR(20) NOT NULL
)</pre><p>Кроме того, данные о продуктах будут публиковаться в какой-то сторонней системе через веб-сервис, в спецификации к которой сказано, что длина поля Description не должна превышать 10 символов. В противном случае оно будет обрезано. </p><p>С учетом всех этих требований, реализуем метод контроллера формы, который будет проверять входные данные и сохранять их в базу таким образом:</p><pre class="brush: csharp;">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);
}</pre><p>Кроме этого в коде формы для нужных полей установлены валидаторы:</p><pre class="brush: xml;">Название:
<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="Описание не может быть пустым" /></pre><p>Как мы видим, знания о правилах валидации у нас присутствуют ровно в трех экземплярах (база данных, контроллер, валидаторы на форме) по количеству мест проверки: валидаторы на форме проверяет пользовательский ввод, затем контроллер, не доверяя валидаторам на форме все перепроверяет, и в заключении база данных, которая вообще никому не доверяет, выполняет еще одну проверку.</p><h4>Унификация логики валидации. Внутренний DSL </h4><p>Целью наших изысканий является приведение кода контроллера к такому виду:</p><pre class="brush: csharp;">public void Save(Product product)
{
IValidator validator = ValidationFactory.CreateValidatorFor<Product>();
if(validator.IsValid(product))
SaveToDatabase(product);
}</pre><p>Интерфейс IValidator предоставляет функциональность для конструирования логики из неких сущностей-терминов и затем выполняет саму валидацию:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="382" alt="" src="http://quatrocode.com/blogspot_images/2008/10/DSL_Syntax.png" width="684" border="0" /> </p><p>Эта схема является грамматикой нашего будущего DSL. Нужно отметить, что в общем случае имеет смысл разделить логику синтаксиса DSL и его интерпретации. В данном примере речь идет о методах GetBrokenRules, IsValid и SatisfiedBy. Аналогом подобного разделения является пример использования класса System.Linq.Expressions.Expression для описания лямбда-функций в виде абстрактного синтаксического дерева. Это позволяет реализовывать LINQ-провадеры для доступа к разнообразным источникам данных.</p><p>Как мы видим на схеме, сама по себе она достаточно бессмысленна, пока у нас не появятся реализации всех этих интерфейсов:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="413" alt="" src="http://quatrocode.com/blogspot_images/2008/10/DSL_Implementations.png" width="553" border="0" /> </p><p>А это уже и есть сам DSL. Комбинируя экземпляры этих классов по правилам на предыдущей схеме, мы сможем описать логику валидации для нужных нам типов.</p><p>Для реализации IConstraint мы могли бы воспользоваться паттерном <a href="http://en.wikipedia.org/wiki/Specification_pattern" target="_blank">Specification</a> для созания сложных констрейнтов. Например, вместо добавления класса RangeConstraint можно было бы собрать его из ранее добавленых GreaterThanConstraint и LessThanConstraint, связав их через AndConstraint.</p><p>Итак, используя наш DSL мы можем создавать валидаторы таким образом:</p><pre class="brush: csharp;">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 });</pre><p>Уже сейчас мы можем реализовать фабрику ValidatorFactory, которая будет создавать экземпляры валидаторов для нужных типов, или, используя паттерн Visitor можно компилировать структуру валидаторов в код проверки пользовательских данных на форме. </p><p>Такой тип DSL называется <strong>внутренним</strong>: язык, имеет четкую структуру, связанную с данной предметной областью, механизм интерпретации и он реализован внутри основного языка программирования с использованием его синтаксиса.</p><h4>Улучшение синтаксиса</h4><p>Приведенный выше фрагмент кода описания структуры валидатора обладает недостатком: притом, что он действительно выполняет декларацию, он совершенно не выглядит декларативно. Забегая вперед, скажу, что кроме того, что хорошая читаемость кода - это важный фактор, облегчающий его сопровождение, так же - это очень облегчит реализацию внешнего DSL.</p><p>Очень удобным и быстрым способом улучшения синтаксиса является использование "<a href="http://en.wikipedia.org/wiki/Fluent_interface" target="_blank">текучего интерфейса</a>" (fluent interface).</p><p>Добавив несколько вспомогательных классов и методов-расширений, мы приведем код создания валидатора к следующему виду:</p><pre class="brush: csharp;">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();</pre><p>Важным моментом тут является то, что обновленный синтаксис максимально <strong>приближен к естественному языку</strong> и нам практически ничего не стоит разработать внешний DSL.</p><h4>Реализация внешнего DSL.</h4><p><strong>Внешним</strong> называется DSL, который имеет свой собственный синтаксис, для него реализован парсер, свой интерпретатор или компилятор. Программы на таких языках можно встраивать в другие языки. Классическим примером такого языка является язык регулярных выражений.</p><p>Перед тем, как начать разработку внешнего DSL, попробуем трансформировать код создания валидатора, приведенный выше в синтаксис, который был бы удобен нам, если бы у нас не было синтаксических правил основного языка:</p><pre>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</pre><p>Такое приведение к естественному языку позволяет нам подключить к работе над проектом людей, весьма далеких от программирования: аналитиков, экспертов по предметной области, заказчиков. Последний пункт особенно важен в случае agile-разработки, когда заказчик является полноценным членом команды. Или же если в проекте применяется Domain Driven Design, то паттерн <a href="http://domaindrivendesign.org/discussion/messageboardarchive/UbiquitousLanguage.html" target="_blank">Ubiquitous Language</a> можно реализовать в виде некого DSL.</p><p>Тут стоит вернутся к предыдущему разделу и еще раз подчеркнуть, что реализация компилятора для такого DSL может быть реализована простым <strong>методом замены</strong>, благодаря введенным синтаксическим улучшениям.</p><p>В своей <a href="http://www.infoq.com/articles/dsl-on-the-clr" target="_blank">статье</a> <a href="http://ayende.com/Blog/" target="_blank">Oren Eini</a> описывает различные способы реализации DSL в среде CLR, приводя примеры и делая вывод, что для этих целей от отдает предпочтение языку <a href="http://boo.codehaus.org/" target="_blank">Boo</a>. Я же в качестве альтернативы приведу пример реализации такого компилятора подстановками на языке <a href="http://nemerle.org/" target="_blank">Nemerle</a>:</p><pre class="brush: csharp;">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) ]>
}
}</pre><p>Код валидатора для класса Product на Nemerle с использованием этих макросов будет выглядеть так:</p><pre class="brush: csharp;">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;</pre><p>Синтаксис немного отличается от спроектированного, но он так же выразителен :)</p><h4>Что дальше?</h4><p>Код на внешнем DSL можно вынести за пределы кода приложения, сохранив его на уровне конфигурации. В этом случае им можно будет пользоваться в основном, подключив компилятор Nemerle, компилируя их на лету.</p><p>В приведеном в самом начале статьи примере "дефектного" кода мы определили три места, где дублируются данные о правилах валидации. И до сих пор мы говорили о решении проблемы дублирования только на уровне кода самого приложения (контроллеры и формы), но напрочь забыли о базе данных.</p><p>Но если у нас есть внешний DSL и механизм его динамического встраивания в основной код приложения, то мы запросто можем написать утилиту, которая будет генерировать код валидаторов на основании метаданных нашей базы данных. </p><p>Или же если у нас структура базы данных генерируется на основании описанной модели предметной области, то мы так же можем использовать код валидаторов для определения типов и размерностей полей в таблицах.</p><p>Таким образом, в нашей системе не осталось никакого дублирования, связанного с логикой валидации. Но наверняка есть логика другого уровня, для которой так же можно было бы разработать DSL. Например, правила расчетов каких-то хитрых скидок, налогов, workflow, сложная конфигурация и т.д. Для каждого из этих случаев может быть реализован свой DSL.</p><p>Для всех интересующихся доступен <a href="http://quatrocode.com/blogspot_files/DslSample.zip">исходный код примера</a>.</p><p><em>UPDATE: О том, как встраивать этот DSL в html-форму <a href="http://igor.quatrocode.com/2008/12/dsl-jquery.html" target="_blank">читаем следующий пост</a>.</em></p></span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com15tag:blogger.com,1999:blog-8864435396524375431.post-16700399627372199472008-09-28T08:54:00.006+03:002010-04-12T13:27:24.505+03:00Хороший дизайн должен быть SOLID: TOP-5 архитектурных принципов<p>Что такое хороший дизайн? По каким критериям его оценивать, и каких правил придерживаться при разработке? Как обеспечить достаточный уровень гибкости, связанности, управляемости, стабильности и понятности кода? <a title="Роберт Мартин" href="http://www.objectmentor.com/omTeam/martin_r.html" target="_blank">Роберт Мартин</a> <a title="Principles Of OOD" href="http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod" target="_blank">составил</a> список, состоящий всего из пяти правил хорошего проектирования, которые известны, как принципы SOLID.</p> <p>Достичь такой лаконичности удалось, использовав небольшую хитрость: дело в том, что термин SOLID - это аббревиатура, которая в свою очередь состоит из аббревиатур, за каждой из которых прячется целый класс паттернов. Ниже мы рассмотрим каждый из них.</p> <span class="fullpost"> <h4>SRP: Single Responsibility Principle (принцип единственной обязанности)</h4> <blockquote> <p>Не должно существовать более одного мотива для изменения данного класса.</p> </blockquote> <p>Об этом принципе говорят, что он является одним из простейших для понимания, но достаточно сложным для того, чтобы легко научиться им правильно пользоваться.</p> <p>Рассмотрим пример. Пусть у нас есть класс, реализующий некоторую функциональность, связанную с банковским счетом:</p> <pre class="brush: csharp;">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){ ... }
}</pre>
<p>Как видно, класс несет ответственность за:</p>
<ol>
<li>Персистентность; </li>
<li>Логику управление балансом; </li>
<li>Логику расчета налогов. </li>
</ol>
<p>Все эти "миссии" и являются теми самыми мотивами, которые влияют на жизненный цикл класса. Проведя рефакторинг, можно получить следующий код:</p>
<pre class="brush: csharp;">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){ ... }
}</pre>
<p>А сложность в применении данного принципа заключается в том, что прежде всего нужно научиться правильно чувствовать границы его использования. Ведь даже в приведенном примере мы превратили паттерн Active Record в антипаттерн, разом перечеркнув все примеры его успешного применения.</p>
<h4>OCP: Open/Closed Principle (принцип открытия/закрытия)</h4>
<blockquote>
<p>Объекты проектирования (классы, функции, модули и т.д.) должны быть открыты для расширения, но закрыты для модификации.</p>
</blockquote>
<p>Другими словами, нужно избегать случаев, когда появление новых требований к функциональности влечет за собой модификацию существующей логики, стараясь реализовать возможность ее расширения.</p>
<p>Рассмотрим простой пример. Пусть у нас в системе есть некий класс, отвечающий за просмотр логов:</p>
<pre class="brush: csharp;">class LogViewer
{
public IEnumerable<Transaction> GetByDate(DateTime dateTime){ ... }
}</pre>
<p>В один прекрасный день возникла необходимость реализовать возможность выборки транзакций по имени пользователя. Класс может быть модифицирован следующим образом:</p>
<pre class="brush: csharp;">class LogViewer
{
public IEnumerable<Transaction> GetByDate(DateTime dateTime){ ... }
public IEnumerable<Transaction> GetByUser(string name){ ... }
public IEnumerable<Transaction> GetByDateAndUser(DateTime dateTime, string name){ ... }
}</pre>
<p>Подобная эволюция дизайна является типичным примером нарушения принципа открытия/закрытия. А типичным решением этой проблемы мог бы стать следующий код:</p>
<pre class="brush: csharp;">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()));
}
}</pre>
<p>Итак, можно сказать, что объект, спроектированный по принципу открытия/закрытия обладает следующими атрибутами:</p>
<ol>
<li>"Открыт для расширения": поведение может быть расширено путем добавления новых объектов, реализующих новые аспекты поведения; </li>
<li>"Закрыт для модификации": в результате расширения поведения исходный или двоичный код объекта не может быть изменен. </li>
</ol>
<h4>LSP: Liskov Substitution Principle (принцип замещения Лисков)</h4>
<blockquote>
<p>Функции, которые используют ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.</p>
</blockquote>
<p>Впервые этот принцип <a title="Keynote address - data abstraction and hierarchy" href="http://portal.acm.org/citation.cfm?id=62141" target="_blank">был упомянут</a> <a href="http://www.pmg.csail.mit.edu/~liskov/">Барбарой Лисков</a> в 1987 году на научной конференции, посвященной объектно-ориентированному программированию.</p>
<p>Этот принцип является <em>важнейшим</em> критерием для оценки качества принимаемых решений при построении иерархий наследования. Сформулировать его можно в виде простого правила: тип S будет подтипом Т <em>тогда и только тогда</em>, когда каждому объекту oS типа S соответствует некий объект oT типа T таким образом, что для всех программ P, реализованных в терминах T, поведение P не будет меняться, если oT заменить на oS.</p>
<p>Классическим примером нарушения этого принципа является построение иерархии такого рода:</p>
<pre class="brush: csharp;">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());
}
}</pre>
<p>Этот пример заставляет задуматься о том, что такое "декларация типа" в терминах объектно-ориентированного языка программирования, который мы используем. Достаточно ли нам описать интерфейс объекта с помощью обычного абстрактного класса со списком методов, типами параметров и возвращаемого значения? Каким образом мы можем декларировать требования к значениям параметров метода и свойства, которыми будет обладать возвращаемое значение? Как нам описать исключения, которые может сгенерировать метод во время выполнения? Как нам описать изменение состояния объекта на разных этапах его жизненного цикла?</p>
<p>Задавая себе эти вопросы и находя ответы, можно спроектировать систему, которая действительно будет удовлетворять принципу замещения Лисков.</p>
<h4>ISP: Interface Segregation Principle (принцип изоляции интерфейса)</h4>
<blockquote>
<p>Клиент не должен вынужденно зависеть от элементов интерфейса, которые он не использует.</p>
</blockquote>
<p>Другими словами этот принцип можно сформулировать так: зависимость между классами должна быть ограничена как можно более узким интерфейсом.</p>
<p>Пример нарушения этого принципа:</p>
<pre class="brush: csharp;">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());
}
}</pre>
<p>Решение этой проблемы заключается в проектировании грамотной иерархии интерфейсов для уменьшения такой зависимости:</p>
<pre class="brush: csharp;">>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(){ ... }
}</pre>
<p>Еще одним признаком потенциального нарушения этого принципа является наличие громоздких интерфейсов. Попробуйте реализовать MembershipProvider для ASP.NET, унаследовавшись от стандартного базового класса и вы поймете о чем речь :) </p>
<h4>DIP: Dependency Inversion Principle (принцип обращения зависимости)</h4>
<blockquote>
<p>Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.</p>
</blockquote>
<p>Перед тем, как перейти к описанию этого принципа, попробуем сформулировать критерии, по которым можно оценивать качество дизайна. Открыв исходный код очередного проекта, мы видим громадные классы, запутанные вызовы, сложные иерархии, обработчики каких-то неочевидных событий. Но если вы посмотрите на историю развития этого проекта, то вы увидите, что в самом начале все было почти идеально: можно было легко проследить зависимости между классами, понять, как они влияют друг на друга и к чему может привести то или иное изменение в коде. Но со временем эта паутина зависимостей становилась все гуще, превращая реализацию очередного изменения требований в настоящий кошмар. </p>
<p>Поэтому можно сделать вывод, что основная причина, по которой проекты так быстро "стареют" и, как правило, даже умирают, заключается в том, что у разработчиков нет возможности безболезненно менять код каких-то компонентов без боязни нарушить работу других. И большинство проблем проектирования, которые выявляются на ранних этапах так и не решаются, накапливаясь лавинообразно.</p>
<p>Дизайн таких систем можно охарактеризовать следующими признаками: </p>
<ul>
<li>Жесткость - изменение одной части кода затрагивает слишком много других частей; </li>
<li>Хрупкость - даже незначительное изменение в коде может привести к совершенно неожиданным проблемам; </li>
<li>Неподвижность - никакая из частей приложения не может быть легко выделена и повторно использована. </li>
</ul>
<p>Принцип обращения зависимости - это очень мощный инструмент, который в сочетании с другими SOLID-принципами позволяет разрабатывать дизайн систем так же легко, как если бы он собирался из конструктора LEGO.</p>
<p>Как обычно, для начала рассмотрим проблемный пример кода. Пусть нам нужно разработать класс OrderProcessor, которые выполняет одну простую вещь: рассчитывает стоимость заказа, учитывая возможную скидку и добавляя налоги, в зависимости от страны, в которой выполняется заказ. В первом приближении у нас может получиться такой код:</p>
<pre class="brush: csharp;">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;
}
}</pre>
<p>Возвращаясь к принципу единственной обязанности, перечислим все обязанности, которые выполняет класс OrderProcessor:</p>
<ul>
<li>Знает, как вычислить сумму заказа; </li>
<li>Знает, как и каким калькулятором вычислить сумму скидки; </li>
<li>Знает, что означают коды стран; </li>
<li>Знает, каким образом вычислить сумму налога для той или иной страны; </li>
<li>Знает формулу, по которой из всех слагаемых вычисляется стоимость заказа. </li>
</ul>
<p>Для того, чтобы разрешить эту проблему, выделим из этого класса две стратегии, которые будут отвечать за расчеты скидки и налогов и перенесем туда соответствующую логику, а зависимость между классами установим через абстракции этих стратегий, реализованных в виде интерфейсов. После всех этих манипуляций мы увидим такой код</p>
<pre class="brush: csharp;">// Интерфейсы стратегий
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;
}
}</pre>
<p>Принцип обращения зависимостей лежит в основе архитектур многих каркасов приложений. Для автоматизации процесса управления зависимостями разработано множество утилит. <a title="Мартин Фаулер" href="http://martinfowler.com/" target="_blank">Мартин Фаулер</a> в <a title="Inversion of Control Containers and the Dependency Injection pattern" href="http://martinfowler.com/articles/injection.html" target="_blank">своей статье</a> рассмотрел всевозможные паттерны реализации механизма работы этого принципа.</p>
<h4>Заключение</h4>
<p>Все эти принципы, безусловно, многим покажутся банальными и простыми. Но как показывает практика, их строгое соблюдение может оказаться очень сложной задачей для разработчиков, которые не очень хорошо разбираются в принципах ООП. Но с другой стороны можно сказать, что этот простой список из пяти пунктов может послужить своеобразной дорожной картой для процесса изучения тонкостей объектно-ориентированного дизайна.</p>
<p>Отдельного внимания заслуживает подход к разработке через тестирование (TDD) и его связь с SOLID-принципами. Для многих именно незнание этих принципов является той самой непреодолимой стеной, которая мешает начать использовать TDD. Но во многом, это мнение является ошибочным, потому как разработка юнит-тестов сама по себе не является сложной задачей и начать писать тесты может каждый на любом этапе своего развития. Главное начать. И уже потом, в процессе поиска оптимальных способов разработки через тестирование будет достигнуто глубокое понимание всех тонкостей качественного проектирования.</p>
</span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com29tag:blogger.com,1999:blog-8864435396524375431.post-35197480179279464302008-09-10T15:23:00.003+03:002011-11-29T01:42:10.384+02:00Expression Trees и оптимизация Reflection<p>В последней версии .NET Framework среди новых возможностей было добавлено средства <a href="http://ru.wikipedia.org/wiki/Метапрограммирование" target="_blank">метапрограммирования</a> под названием <a href="http://msdn.microsoft.com/en-us/library/bb397951.aspx" target="_blank">Expression Trees</a>. На базе этой технологии, а именно основываясь на том принципе, что выражения на "обычном" языке программирования могут автоматически преобразовываться в синтаксические деревья, была разработана технология LINQ.</p><p>Но в этом посте речь пойдет о другой области применения возможности динамически собирать expression trees и компилировать их в работоспособный код. И эта область - оптимизация Reflection.</p><span class="fullpost"> <p>Как известно, платой за гибкость при использовании рефлексии является производительность. Но в случае, когда она применяется к некому фиксированному набору метаданных, ее легко <a href="http://www.rsdn.ru/article/dotnet/reflectionspeed.xml" target="_blank">оптимизировать</a>.</p><p>Но динамические Expression Trees предоставляют нам еще достаточно элегантный способ оптимизации. Суть его заключается в том, что для доступа к свойствам экземпляров известного класса мы будем генерировать соответствующий строго типизированный код в виде лямбда-функции, которая будет обращаться к ним напрямую и которую мы будем кэшировать для последующего повторного использования.</p><p>Сам по себе метод создания нужной лямбда-функции достаточно прост:</p><pre class="brush: csharp;">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();
}</pre><p>Если поставить в этом методе точку останова и посмотреть на строковое представление переменной getterExpression, то мы увидим, во что оно будет скомпилировано: </p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="132" alt="getterExpression" src="http://quatrocode.com/blogspot_images/2008/09/screenshot001.png" width="533" border="0" /></p><p>Обернем всю логику доступа к свойству класса в некий ReflectionHelper, который в дальнейшем можно будет расширить методами для вызова методов, инициализации свойств и т.д. Этот класс будет реализовывать метод GetPropertyValue следующим образом:</p><pre class="brush: csharp;">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);
}</pre><p>Для проверки того, насколько эта логика эффективна, разработаем небольшой тест:</p><pre class="brush: csharp;">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));
}</pre><p>Ну и результаты говорят сами за себя:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="343" alt="screenshot002" src="http://quatrocode.com/blogspot_images/2008/09/screenshot002.png" width="681" border="0" /></p><p>Как видим, такой способ оптимизации более чем жизнеспособен :)</p></span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com2tag:blogger.com,1999:blog-8864435396524375431.post-64390875014344292122008-09-07T20:27:00.012+03:002011-12-26T23:10:46.442+02:00Интеграция данных: REST + LINQ = ADO.NET Data Services<p>Одним из интересных аспектов SOA является <a href="http://en.wikipedia.org/wiki/Data_integration" target="_blank">интеграция данных</a>. Выдержка из Wikipedia:</p><blockquote>Интеграция данных – это процесс комбинирования данных из разнообразных источников и предоставление их пользователи в неком унифицированном виде. Необходимость в разработке этого процесса появляется в различных областях. Например, в коммерческой (когда двум схожим компаниям необходимо объединить свои базы данных) и в научной (объединение данных из нескольких разных хранилищ биометрической информации). </blockquote><p>Необходимость в таком виде интеграции возникла в одном из текущих проектов. Со стороны разработчика хотелось получить максимально прозрачный API, не зависящий от способа реализации и предоставляющий удобный и максимально "родной" механизм работы с данными.</p><span class="fullpost"> <h3>Введение</h3><p>Главный вопрос, на который пришлось найти ответ - это технология открытия доступа к удаленной базе данных. Выбор пал на технологию <a href="http://en.wikipedia.org/wiki/REST" target="_blank">REST</a>. Вкратце, она позволяет нам получать нужные данные, формируя запросы в виде GET-параметров к сервису. Дополнительно к этому хотелось бы получить транслятор с LINQ к REST, используя LINQ как раз как тот самый унифицированный и "родной" механизм, который позволит связать воедино данные из любых источников, для которых реализована соответствующая библиотека Linq2***.</p><p>Поиски соответствующей библиотеки были недолгими: не так давно Microsoft выпустило в составе .NET 3.5 SP1 свой проект <a href="http://msdn.microsoft.com/en-us/data/bb931106.aspx" target="_blank">Astoria</a>, назвав его ADO.NET Data Services. С одной стороны эта библиотека позволяет легко создавать сервисы данных, открывающие доступ к данным на сервере посредством протокола REST. А с другой стороны, предоставляет удобные средства интеграции этих сервисов с клиентскими приложениями, оборачивая всю рутину по доступу к сервису в автосгенерированные прокси-классы. Ну и конечно же эти клиентские прокси-классы реализуют LINQ-транслятор. Кроме того, сервисы данных реализуются на базе стека WCF, предоставляя широкие возможности по тонкой низкоуровневой настройке.</p><h3>Реализация сервиса</h3><p>Для начала добавим в веб-проект сервис данных:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" alt="Add New Item - ADO.NET Data Service" src="http://quatrocode.com/blogspot_images/2008/09/1_add_new_item.png" border="0" /> </p><p>Исходный код сервиса будет таким:</p><pre class="brush: csharp;">public class WebDataService : DataService< /* TODO: put your data source class name here */ >
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(IDataServiceConfiguration config)
{
// TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
// Examples:
// config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
// config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
}
}</pre><p>Если вы обратитесь к спецификации класса DataService<T>, то увидите, что к типу Т не предоставляются такие требования, как реализация какого-либо инетрфейса. Все дело в том, что необходимые требования невозможно описать в виде какой-либо простой модели, которая бы не усложнила разработку этих сервисов. Но требования, тем не менее, есть:</p><ol><li>Сервис данных может опубликовывать все IQueryable-свойства данного источника данных. <br />
Наш источник данных может выглядеть таким образом: <pre class="brush: csharp;">public class DomainDataContext
{
private readonly Supplier[] _suppliers;
private readonly Product[] _products;
public DomainDataContext()
{
// Инициализация _suppliers и _products тестовыми данными
}
public IQueryable<Supplier> Suppliers
{
get { return _suppliers.AsQueryable(); }
}
public IQueryable<Product> Products
{
get { return _products.AsQueryable(); }
}
}</pre></li>
<li>Классы-сущности должны быть "идентифицируемыми".  <br />
Это правило в данном случае означает, что в классах обязательно должны быть свойства, заканчивающиеся на ID: <pre class="brush: csharp;">public class Product
{
public string ProductID { get; set; }
public string SupplierID { get; set; }
public string ProductName { get; set; }
}
public class Supplier
{
public string SupplierID { get; set; }
public string CompanyName { get; set; }
public string ContactName { get; set; }
public string Phone { get; set; }
} </pre></li>
</ol><p>И это не удивительно, что в качестве источников данных можно использовать модели Linq2Sql и ADO.NET Entity Framework.</p><p>Посмотрев на наш сервис в браузере, мы увидим такой XML:</p><pre class="brush: xml;"><?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<service xml:base="http://localhost:7593/WebDataService.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
<workspace>
<atom:title>Default</atom:title>
</workspace>
</service></pre><p>Никаких упоминаний о наших Products и Suppliers. Мы забыли сконфигурировать наш сервис, указав доступ к коллекциям объектов и операциям. Но так как наш сервис не предоставляет никаких методов для манипуляции данными, нам нужно сконфигурировать только доступ к коллекциям в источнике данных:</p><pre class="brush: csharp;">public static void InitializeService(IDataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
}</pre><p>Открываем сервис в браузере и видим наши коллекции:</p><pre class="brush: xml;"><?xml version="1.0" encoding="windows-1251" standalone="yes"?>
<service xml:base="http://localhost:7593/WebDataService.svc/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
<workspace>
<atom:title>Default</atom:title>
<collection href="Suppliers">
<atom:title>Suppliers</atom:title>
</collection>
<collection href="Products">
<atom:title>Products</atom:title>
</collection>
</workspace>
</service></pre><p>Итак, наш сервис готов к использованию.</p><h3>Реализация клиента</h3><p>Так как наш сервис данных построен на основе WCF, то подключение сервиса ничем не отличается от подключения любого другого WCF-сервиса. Самое же интересное начинается во время его использования.</p><p>В качестве примера реализуем логику поиска поставщиков по названию товара. Локально поиск будет проводиться по базе Northwind с использованием Linq2Sql, а потом результат локального поиска будет объединяться с поиском через сервис данных.</p><p>Локальная схема данных будет выглядеть следующим образом:</p><p><img style="border-top-width: 0px; border-left-width: 0px; border-bottom-width: 0px; border-right-width: 0px" height="313" alt="Local data schema" src="http://quatrocode.com/blogspot_images/2008/09/2_northwind.png" width="542" border="0" /> </p><p>Поисковый запрос к локальной базе достаточно тривиален:</p><pre class="brush: csharp;">var localData = new LocalDataDataContext();
var localSuppliers = from p in localData.Products
join s in localData.Suppliers on p.SupplierID equals s.SupplierID
where p.ProductName == productName
select
new
{
Company = s.CompanyName,
PhoneNumber = s.Phone
};</pre><p>Попробуем сделать то же самое для поиска по удаленному сервису данных:</p><pre class="brush: csharp;">var serviceData = new DomainDataContext(new Uri("http://localhost:7593/WebDataService.svc"));
var dataServiceSuppliers = from p in serviceData.Products
join s in serviceData.Suppliers on p.SupplierID equals s.SupplierID
where p.ProductName == productName
select
new
{
Company = s.CompanyName,
PhoneNumber = s.Phone
};</pre><p>При попытке получить данные по этому запросу мы получим исключение "The method 'Join' is not supported.". По сути, это <a href="http://russian.joelonsoftware.com/Articles/LeakyAbstractions.html" target="_blank">Законы дырявых асбстракций</a> в действии :) Все дело в том, что не вся функциональность Linq однозначно транслируется в REST-запросы. REST-сервисы предоставляют лишь несколько параметров запросов: expand, orderby, skip, top, filter. Как видно, никаких inner join и даже агрегатов, типа банального count тут нет. </p><p>Для решения этой проблемы разобьем наш запрос на несколько подзапросов: сначала найдем все товары, удовлетворяющие данному, а потом получим список поставщиков этих товаров.</p><pre class="brush: csharp;">var serviceData = new DomainDataContext(new Uri("http://localhost:7593/WebDataService.svc"));
var matchedProducts = from p in serviceData.Products
where p.ProductName == productName
select p;</pre><p>Следующий фрагмент кода динамически соберет условие для параметра filter так, чтобы получилось что-то в таком духе:</p><pre class="brush: csharp;">Func<ServiceReference1.Supplier, bool> filterCondition =
s => s.SupplierID == "001" || s.SupplierID == "002";</pre><p>Такой вариант фильтра однозначно транслируется в строку для параметра запроса filter.</p><pre class="brush: csharp;">var param = Expression.Parameter(typeof(ServiceReference1.Supplier), "s");
Func<string, IEnumerable<string>, Expression> buildOrElseExpression = null;
Func<string, Expression> buildEqualExpression =
val => Expression.Equal(Expression.Property(param, "SupplierID"), Expression.Constant(val));
Func<IEnumerable<string>, Expression> buildTailExpression = tail => tail.Count() == 1
? buildEqualExpression(tail.First())
: buildOrElseExpression(tail.First(), tail.Skip(1));
buildOrElseExpression =
(head, tail) => Expression.OrElse(
buildEqualExpression(head),
buildTailExpression(tail));
// Вычитываем данные из сервиса и получаем отключенный список идентификаторов поставщиков
var serviceSupplierIDs = matchedProducts.ToArray().Select(p => p.SupplierID);
var filterCondition =
Expression.Lambda<Func<ServiceReference1.Supplier, bool>>(
buildTailExpression(serviceSupplierIDs), param);
// Получаем отключенную копию результатов поиска для дальнейших манипуляций
var serviceSuppliers = serviceData.Suppliers.
Where(filterCondition).
ToArray().
Select(s => new
{
Company = s.CompanyName,
PhoneNumber = s.Phone
});</pre><p> </p><p>Ну и наконец-то финальное объединение:</p><pre class="brush: csharp;">// Перед объединением получаем отключенную копию данных.
// В противном случае Linq2Sql попытается транслировать этот метод в sql-инструкцию UNION
var allSuppliers = localSuppliers.ToArray().Union(serviceSuppliers);</pre><h3>Выводы</h3><p>Конечно же, ADO.NET Data Services - это решение многих проблем интеграции данных внутри системы. Но использование этой системы накладывает и ряд требований:</p><ol><li>Модель данных для таких сервисов должна быть подготовлена специальным образом так, чтобы избежать множества повторяющихся простых запросов из-за ограничения протокола REST. В общем случае, эта подготовка сведется к денормализации исходной модели данных, к укрупнению классов сущностей и введению всяких агрегатных полей с данными из дочерних сущностей. <br />
</li>
<li>Для вычисления агрегатных функций (таких, как count(), sum() и т.д.) нужно будет реализовать специальные методы вне контекста сервисов данных. <br />
</li>
<li>Все операции проекции должны проводиться на отсоединенной выборке. Таким образом, с учетом требований в п.1 такими запросами не стоит злоупотреблять по причиные избыточности данных, передаваемых через транспортный уровень. Для решения этой проблемы можно было бы расширить нашу модель одинаковыми сущностями с различными уровнями детализации. </li>
</ol><p>Наверное, стоит наедятся, что в будущем многие из недостающих сегодня возможностей будут реализованы. Но даже то, что у нас есть на сегодняшний день - это большой шаг вперед в области интеграции приложений. </p><p><a href="http://quatrocode.com/blogspot_files/RESTProto.zip" target="_blank">Исходный код тестового приложения</a></p></span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com2tag:blogger.com,1999:blog-8864435396524375431.post-3622428092438042742008-07-24T13:43:00.007+03:002011-11-28T23:14:22.403+02:00NHibernate и валидация<p>Замечательный проект затеял <a href="http://darioquintana.com.ar/blogging/">Dario Quintana</a> в следующей версии библиотеки дополнений <a href="http://sourceforge.net/projects/nhcontrib/">NHibernate Contrib</a>:</p><p><a href="http://sourceforge.net/project/showfiles.php?group_id=216446&package_id=275108&release_id=597674" target="_blank"><img style="border-width: 0px;" alt="NHV-logo-white-background" src="http://quatrocode.com/blogspot_images/2008/07/NHV-logo.png" width="500" border="0" height="122"></a></p><span class="fullpost"> <p>Его предназначение - декларирование и централизованная проверка правил валидации где-нибудь поближе к модели домена.</p><p>Например, правила можно описывать в виде атрибутов:</p><pre class="brush: csharp;">public class User
{
public virtual int Id
{
get { return id; }
}
[NotEmpty, NotNull]
public virtual string UserName
{
get { return userName; }
set { userName = value; }
}
[Email]
public virtual string Email
{
get { return email; }
set { email = value; }
}
[Past]
public DateTime CreatedDate
{
get { return createdDate; }
set { createdDate = value; }
}
[Min(18, Message="You are to young!")]
public int Age
{
get { return age; }
set { age = value; }
}
[CreditCardNumber]
public string CreditCardNumber
{
get { return creditCardNumber; }
set { creditCardNumber = value; }
}
///...
}</pre><p>Или же, если вам нужна возможность гибкой настройки этих правил, скажем, для согласования с констрейнтами в базе данных, то правила можно определить в файле конфигурации:</p><pre class="brush: xml;"><nhv-mapping xmlns="urn:nhibernate-validator-1.0">
<class name="NHibernate.Validator.Demo.Winforms.Model.Customer, NHibernate.Validator.Demo.Winforms">
<property name="FirstName">
<not-empty/>
<not-null/>
</property>
<property name="Email">
<email/>
</property>
<property name="Zip">
<pattern regex="^[A-Z0-9-]+$" message="Examples of valid matches: 234G-34DA | 3432-DF23"/>
<pattern regex="^....-....$" message="Must match ....-...."/>
</property>
</class>
</nhv-mapping></pre><p>Ну и пример того, как происходит процесс валидации: </p><p>Для начала создаем экземпляр валидатора:</p><pre class="brush: csharp;">NHVConfiguration nhvc = new NHVConfiguration();
nhvc.Properties[Environment.ApplyToDDL] = "false";
nhvc.Properties[Environment.AutoregisterListeners] = "true";
nhvc.Properties[Environment.ValidatorMode] = "UseAttribute";
nhvc.Mappings.Add(new MappingConfiguration("NHibernate.ValidatorDemo.Model", null));
ValidatorEngine validator = new ValidatorEngine();
validator.Configure(nhvc);</pre><p>Ну и потом пользуемся им:</p><pre class="brush: csharp;">public void Create([DataBind("user")] User user)
{
InvalidValue[] errors = validator.Validate(user);
if (errors.Length > 0)
{
Flash["errors"] = errors;
RedirectToAction("index");
}
else
{
repository.Create(user);
}
}</pre><p>Процесс валидации можно попробовать сделать более прозрачным, например, используя фильтры в ASP.NET MVC, или реализовав какие-то аспекты прямо на уровне модели домена.</p><p>P.S. Пост написан мо мотивам <a href="http://www.codinginstinct.com/2008/05/nhibernate-validator.html" target="_blank">этого поста</a>. </p></span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0tag:blogger.com,1999:blog-8864435396524375431.post-74694258766007141272008-07-07T22:40:00.010+03:002011-12-26T23:06:23.255+02:00Распределение сессий ASP.NET с помощью memcached<p>Для многих разрабатываемых сайтов, расчитанных на успех, и, как следствие, на большой трафик, одним из важных требований является удобное горизонтальное масштабирование и высокий уровень доступности приложения. При решении этих задачи приходится решать задачи уровня системы и уровня приложения. На системном уровне все решается настройкой всевозможных кластеров, систем резервного копирования, диспетчеров нагрузки и т.д. И очень важно эти вынести эти системные задачи за пределы кода приложения, иначе добавление лишнего сервера в кластер может превратиться бесконечный бой со своим кодом.</p><p>В общем случае, главной проблемой, мешающей клонированию приложения или сервиса на многие сервера с сохранением общей функциональности является использование состояний клиента. Другими словами - использование сессий. Эта проблема связана с чисто программной реализацией сессий поверх несессионного протокола HTTP, когда идентификатор сеанса отправляется в cookies каждому клиенту, а потом по нему сервер, обслуживающий запросы "достает" нужные данные. И по умолчанию, данные сессий хранятся локально в памяти или файловой системе сервера. А в случае, когда приложение "живет" на нескольких серверах, то идентификатор сеанса каждый раз будет обрабатываться разными серверами и сессии утратят свой смысл.</p><p>В качестве решения этой проблемы в ASP.NET предлагается использовать особый режимы StateServer и SQLServer, когда состояние будет храниться на удаленном сервере, либо вообще в базе SQL Server. У первого способа главным недостатком является то, что сам по себе сервер состояний не масштабируется вообще. В случае же использования режима SQLServer и размещении базы данных состояний на отдельном кластере, мы получим масштабируемую систему, но в этом случае сильно потеряем в производительности. Конечно же, когда идет о масштабируемости, локальной производительностью можно пренебречь, посчитав, что ее можно увеличить за счет включения лишних боксов в кластер. Но что делать начинающим стартаперам, которые пока еще считают деньги? Как получить систему, относительно быструю, как в случае со StateServer, и масштабируемую, как в режиме работы с SQL Server?</p><span class="fullpost"> <p>Ответ прост - использование систем распределенной памяти вобще и <a href="http://www.danga.com/memcached/">memcached</a> в частности, как распространенный, проверенный, простой и бесплатный продукт. Познакомиться с ним можно в статье <a href="http://www.insight-it.ru/unix-way/obzor-memcached/">Обзор memcaced</a>. А в этом посте мы попробуем решить с его помощью проблемы с сессиями в ASP.NET.</p><p>Вкратце, нам хотелось бы получить систему, которая бы прозрачно интегрировалась с уже готовым приложением с минимальными изменениями в коде, достаточно быструю и надежную, не теряющую данные, как при перезагрузке отдельных приложений, так и при перезагрузке серверов и всего кластера целиком. И все это реализуется достаточно просто :) Итак, приступим:</p><ol><li>Так как среда обитания наших приложений - это Windows, то скачиваем <a href="http://jehiah.cz/projects/memcached-win32/">отсюда</a> или <a href="http://www.splinedancer.com/memcached-win32/">отсюда</a> бинарники, скомпилированые для win32. Распаковываем его куда-нибудь и запустим единственный exe-файл. Сервер memcached запущен :) Весь процесс настройки производится через параметры запуска. Ознакомиться с ними можно запустив сервер с параметром -h. </li>
<li>Теперь нам нужно чем-то подключаться к этому сервису. И в качестве клиента предлагается использовать <a href="http://www.codeplex.com/EnyimMemcached/">enyim.com Memcached Client</a>, и его можно использовать уже в составе готовых <a href="http://www.codeplex.com/memcachedproviders">Memcached Providers</a>, что максимально упрощает использование memcached. Скачав и разархивировав билиотеку куда-нибудь, добавляем в наш проект ссылки на файлы Enyim.Caching.dll, MemcachedProviders.dll и log4net.dll. </li>
<li>В коде приложения необходимо найти все классы, экземпляры которых помещаются в сессию и отметить их атрибутом Serializable. </li>
<li>Осталось дело за малым - сконфигурировать наше приложение должным образом: <ul><li>В разделе configSections регистрируем секции: <pre class="brush: xml;"><sectionGroup name="enyim.com">
<section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />
</sectionGroup>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/></pre></li>
<li>Добавляем секцию настройке клиента memcached: <pre class="brush: xml;"><enyim.com>
<memcached>
<servers>
<add address="127.0.0.1" port="11211" />
</servers>
<socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00" />
</memcached>
</enyim.com></pre>Ключевым элементом здесь является секция servers, в которой перечисляются все memcached серверы, обслуживающие приложение. Самым распространенным вариантом использования является установка экземпляра memcached на каждый из серверов, на котором работает приложение. </li>
<li>А теперь настраиваем работу сессий: <pre class="brush: xml;"><sessionState cookieless="true" regenerateExpiredSessionId="true" mode="Custom" customProvider="MemcachedSessionProvider">
<providers>
<add name="MemcachedSessionProvider" type="MemcachedProviders.Session.SessionStateProvider, MemcachedProviders"
connectionStringName="SqlSessionServices"
dbType="SQL"
writeExceptionsToEventLog="false" />
</providers>
</sessionState></pre>Интересной здесь является строка dbType="SQL" - она указывает, какого типа СУБД будет использоваться для бэкапов данных сессии. Для отключения режима бэкапа данных нужно просто указать dbType="None". </li>
<li>Заключительным этапом будет настройка параметров подключения к базе данных для бэкапов. Для этого в секцию connecitonStrings нужно добавить такую запись: <pre class="brush: xml;"><add name="SqlSessionServices" connectionString="..."/></pre>(троеточие следует заменить на реальную строку подключения). </li>
<li>Опционально можно добавить найстройки для log4net. </li>
</ul></li>
</ol><p>Осталось запустить приложение.</p><p>При обращении к сессии, провайдер обращается к клиенту memcached, который в свою очередь посылает асинхронные запросы на данные всем memcached-серверам, используюя идентификатор сессии, как ключ к данным. И вот здесь происходит самое инетресное: если это не первый запрос к сессии и какие-то данные там уже размещены, но предыдущий запрос обслуживал другой сервер, то клиент, а за ним и провайдер получат данные от другого сервера. А если же предыдущий запрос обслуживался текущим сервером, то он получит данные от от процесса, работающего на этом же сервере, чтобы будет гораздо быстрее, чем если бы он получал их по сети. Таким образом, в случае, если нагрузка равномерно распределяется между серверами, то гарантированное попадание в локальный сервис будет примерно в каждом втором случае. А если же сюда добавить условие, что как браузеры, так и сетевое оборудование оптимизирует работу с соединениями, "придерживая" их в пулле и используя так называемые sticky connections, то количество попаданий будет стремиться к 100%. </p><p>В случае, если ни один из сервисов не вернет данных по запросу, провайдер обратится в базу данных. Этот же сценарий будет срабатывать в случае, если ни один из memcached-сервисов не будет запущен. Можно сказать, что в этом случае сессии будут работать так же, как в режиме SQLServer.</p><p>При записи данных в сессию они в первую очередь будут сохранены в памяти первого сервиса в списке и в фоновом режиме будут сохранены в базе данных. Таким образом, если в какой-то момент времени весь кластер целиком будет перезагружен, то все клиентские сессии будут подтянуты из бэкапа и пользователи смогут работать, как ни в чем не бывло.</p><p>Практической выгодой от использования этого сервиса является то, что теперь сессией можно пользоваться без оглядки на то, что приложения будут перегружены и все данные пропадут, или же, что это будет ударом по производительности. В качестве типичного варианта использования можно предложить хранить в сессии данные по корзине в инетрнет-магазине.</p><p>Недостатоком этой конфигурации является то, что она не работает в случае, когда не доступен SQL Server. Если указать режим бэкапа dbType="None", то в случае недоступности memcached-сервисов будет использоваться стандартный режим работы сессий, когда все данные хранятся в памяти рабочего процесса. Очень хотелось бы, чтобы провайдер стал умнее и стал использовать стандартный режим в случае, если оба хранилища не доступны. Но код провайдера доступен, логика его работы более чем проста и добавление этой функциональности не является проблемой.</p><br />
<p><a href="http://quatrocode.com/blogspot_files/MemcachedTest.zip" target="_blank">скачать пример</a><br />
</p><br />
</span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0tag:blogger.com,1999:blog-8864435396524375431.post-13087676907112056562007-12-17T12:26:00.002+02:002008-07-11T00:39:58.143+03:00IIS 7.0 и Custom Errors<p>Разработчики, начавшие использовать IIS 7.0, могут столкнуться с интересной проблемой, которая не была актуальна для систем, работающих на базе IIS 6.0. Суть проблемы заключается в том, что если для приложения разработан свой обработчик ошибок, обрабатывающий разные типы исключений и выполняющий редирект на разные страницы, то этот редирект будет проигнорирован и пользователь увидит стандартный "желтый экран" IIS.</p> <p>Решение же проблемы оказалось куда банальнее, чем сама проблема :)</p> <span class="fullpost"> <p>Исследования выявили интересное нововведение в классе HttpRequest, которое появилось в версиях .NET 3.5, 3.0 SP1 и 2.0 SP1 - свойство TrySkipIisCustomErrors. Оно указывает IIS7, игнорировать ли стандартный обработчик ошибок, разрешая пользовательскую обработку, или обрабатывать ошибки сугубо стандартными средствами. При этом, "ручное" выставление этого свойства ни к чему не приводит. По все видимости, это свойство играет сугубо сервисную роль и используется самой инфраструктурой ASP.NET. Да и завязка на наличие SP у .NET как-то не очень вселяет оптимизм. </p> <p>Впрочем, все решилось достаточно изящно. В документации к этому свойству нашлось: </p> <blockquote> <p><em>The TrySkipIisCustomErrors property is used only when your application is hosted in IIS 7.0. When running in Classic mode in IIS 7.0 the TrySkipIisCustomErrors property default value is true. When running in Integrated mode, the TrySkipIisCustomErrors property default value is false.</em></p> </blockquote> <p>Дейтсвительно, стоило для приложения указать использование Classic Pool вместо Default Pool, и все заработало точно так же, как на старом добром IIS 6.0 :)</p> </span> Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0tag:blogger.com,1999:blog-8864435396524375431.post-87828109531872571722007-04-26T14:24:00.002+03:002009-11-03T14:58:18.189+02:00Метапрограммирование в Nemerle.<p> Развитие идей <a href="http://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%B0%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5" target="_blank">метапрограммирования</a> всегда было неким Граалем для индустрии программного обеспечения. Известно правило, что на сложность решаемых задач напрямую зависит от уровня абстракции, на котором строится его решение. Согласитесь, сложно было бы разрабатывать <a href="http://ru.wikipedia.org/wiki/%D0%AD%D0%BA%D1%81%D0%BF%D0%B5%D1%80%D1%82%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0" target="_blank">экспертные сестемы</a> в терминах ассемблера. Поэтому-то и были разработаны технологии <a href="http://ru.wikipedia.org/wiki/%D0%9B%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5" target="_blank">логического программирования</a>, реализующие достаточную терминологическую базу для решения задач в области принятия решений. Вот техника метапрограммирования как дает в руки разработчиков неплохое средство повышения уровня абстракции при разработке систем путем расширения выразительных способностей компилятора. </p> <p> Язык программирования <a href="http://nemerle.org/Main_Page" target="_blank">Nermele</a> во многом <a href="http://rsdn.rsdn.ru/forum/?group=nemerle" target="_blank">известен</a> именно благодаря своей <a href="http://rsdn.rsdn.ru/article/nemerle/nemerleMacros.xml" target="_blank">мощной системе метапрограммирования</a>, которая достаточно органично вписывается в мир .NET, позволяя использовать язык со всеми его возможностями уже "здесь и сейчас". Язык предоставляет возможность разработки расширений к процессу компиляции с помощью максросов, написанных на этом же языке и подключаемых к нужному проекту как обычная библиотека.</p> <span class="fullpost"> <p> Для того, чтобы опробовать все эти возможности, достаточно установить последний билд компилятора и интеграции с Visual Studio. Скачать это можно с <a href="http://rsdn.rsdn.ru/article/nemerle/Nemerle.VsIntegration-ru.xml" target="_blank">сайта проекта интеграции</a>. Установив Nemerle и интеграцию, в студии вам будет доступна новая группа проектов. </p> <p> Пример, который мы рассмотрим будет достаточно простым и хорошо всем занкомым. Предположим, вы смоделировали предметную область и подошло время воплотить эту модель в коде. Каждая сущность этой модели будет представленная в виде неких персистентных классов. Кроме логики домена вам необходимо реализизовать т.н. логику приложения, которая включает в себя механизмы ORM, Concurrency и Audit Trail. Логика приложения привносит в каждый класс вашей модели свой специфичный набор полей:</p> <ul> <li>Каждый класс должен иметь поле идентификатора. Конечно, эта задача решается путем наследования от некого супер-класса. Но наследование может сыграть злую роль в задачах передачи данных за границы приложения. Например, в случаях какой-то экзотической сериализации. В случае распределенного приложения вообще лучше всего иметь классы с минимальной детализацией для передачи между подсистемами. Поэтому идеальным случаем было бы добавление поля идентификатора к каждому классу в домене; </li> <li>В задаче совместнго доступа нам нужно поле с временной меткой записи, по которой мы будем определять перед каждой операцией записи, была ли сущность изменена кем-то еще; </li> <li>Для простейшей системы аудита нам необходимо добавить поля, в которых будет хранится информация о том, кто запись создал и кто ее в последний раз изменял. </li> </ul> <p> Итак, мы имеем 6 однотипных полей, которые мы должны добавлять в каждый создаваемый класс. Это и есть та самая рутина, которая не требует принятия каких-либо сложных решений и которую, благодаря этому, очень легко автоматизировать.</p> <p> Пусть мы имеем следущий класс:</p> <pre class="brush: csharp;">public class Enumeration
{
mutable value : string;
mutable description : string;
}</pre>
<p>
Его аналогом в C# был бы следующий код:</p>
<pre class="brush: csharp;">public class Enumeration
{
string value;
string description;
}</pre>
<p>
В этом классе не хватает публичных свойств к заданным полям. Это еще одна из рутинных
операций, которые подлежат автоматизации. Для решения этой задачи мы воспользуемся
готовым макросом, которые поставляется с компилятором:</p>
<pre class="brush: csharp;">public class Enumeration
{
[Accessor (flags = WantSetter)]
mutable value : string;
[Accessor (flags = WantSetter)]
mutable description : string;
}</pre>
<p>
Все просто: максросами-атрибутами вы указываете компилятору на то, что к данным
полям необходимое сгенерировать свойства, причем, свойства должны быть с set-аксессором.
Скомпилируем этот код и посмотрим рефлектором на то, как бы этот код выглядел на
C#:</p>
<pre class="brush: csharp;">public class Enumeration
{
// Fields
private string description;
private string value;
// Properties
public string Description
{
[DebuggerStepThrough]
get
{
return this.description;
}
[DebuggerStepThrough]
set
{
this.description = value;
}
}
public string Value
{
[DebuggerStepThrough]
get
{
return this.value;
}
[DebuggerStepThrough]
set
{
this.value = value;
}
}
}</pre>
<p>
Но вернемся к нашим полям. С точки зрения разработчика было бы удобвно наделить
класс Enumeration всем необходимым свойствами с помощью похожего макроса-атрибута:</p>
<pre class="brush: csharp;">[PersistentObject]
public class Enumeration
{ ....</pre>
<p>
И выглядеть этот макрос будет следующим образом (для краткости мы будем добавлять
только идентификатор):</p>
<pre class="brush: csharp;">using Nemerle;
using Nemerle.Utility;
[MacroUsage (MacroPhase.WithTypedMembers, MacroTargets.Class)]
macro PersistentObject (t : TypeBuilder)
{
t.Define(<[ decl:
[Accessor]
mutable id : Guid;
]>);
}</pre>
<p>
Как видите, ничем сложным разработка макросов не является. Изучим этот пример детальнее.</p>
<p>
Атрибутом MacroUsage мы указываем компилятору на то, что макрос применяется в виде
атрибута к классу и применяется на этапе, когда класс "наделен" всеми
свойствами классов-предков. Так же мы можем "встроиться" в процесс компиляции
до того, как класс будет унаследован.</p>
<p>
Обязательным первым параметром макросов, которые применяются как атрибуты к классам
является переменная типа TypeBuilder. Эта переменная указывает на объект, умеющий
манипулировать целевым типом. Если бы наш макрос применялся к полям, то первый параметр
был бы типа FieldBuilder.</p>
<p>
Единственной операцией в нашем макросе является вызов методв Define у переменной
типа TypeBuilder. Этот метод принимает в качестве параметра экземпляр синтаксического
дерева, соответствующего коду, который мы хотим добавить в наш класс. Мы можем,
конечно же создать это экземпляр вручную, добавляя все синтаксические элементы,
как узлы этого дерева. Но Nemerle предоставляет нам возможность избежать этого,
позволяя нам описать не код, строящий дерево, а код, который и должен быть представлен
в виде дерева. Эта возможность называется "цитированием". Для цитирования
используется подобная конструкция:</p>
<pre class="brush: csharp;">def ast : PExpr =
<[
/* Код, который будет представлен в виде дерева */
]></pre>
<p>
Обратите внимание, что мы внутри макроса использовали другой макрс, а именно, известный
нам Accessor. Таким образом класс  Enumeration будет расширен приватным полем
и свойствам к нему.</p>
<p>
Забегая вперед, хочется отметить, что возможности макросистемы Nemerle позволяют
добавлять новые классы в сборки, основанные на метаданных. Например, мы можем расширить
наш макрос PersistentObject так, чтобы он для каждого класса генерировал все необходимые
классы DAO и даже подготавливал базу данных. И все это на этапе компиляции!...</p>
</span>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0tag:blogger.com,1999:blog-8864435396524375431.post-25832158478492186802007-03-12T21:19:00.003+02:002011-11-28T02:01:39.550+02:00Выходные с Nemerle<div dir="ltr" style="text-align: left;" trbidi="on">Наконец-то дошли руки до популярного ныне в своих кругах языка программирования <a href="http://nemerle.org/" target="_blank">Nemerle</a>. За развитием этого языка я слежу давно, наблющаю за активным обсуждением этого языка на сайте <a href="http://www.rsdn.ru/" target="_blank">RSDN</a>. Пробовал писать на этом языке какие-то вещи, но дальше примеров дело не заходило. На сей раз к этому языку меня подтолкнуло любопытство, связанное с парадигомой <a href="http://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5" target="_blank">функционального программирования</a>. Как практику, мне необходимо было выбрать язык программирования для своих экспериментов, а Nemerle изначально был заявлен, как поддерживающий функциональное программирование, да и один из его предков <a href="http://en.wikipedia.org/wiki/ML_programming_language" target="_blank">язык ML</a> функционален по определению. Ну и хотелось иметь язык, который нативно впишется в среду .NET. Ну и его "гибридность" теоретически может позволить "быстрый ввод" и легкое "переключение" между парадигмами в зависимости от решаемых задач. Кроме того, система <a href="http://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%B0%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5" target="_blank">метапрограммирования</a>, <a href="http://nemerle.org/Macros" target="_blank">реализованная</a> в этом языке, делают его настоящим комбайном, выразительные способности которого ограничиваются исключительно фантазией пишущих на нем. К слову стоит отметить, что большая часть исходников Nemerle написана на самом Nemerle. <br />
<span class="fullpost"> </span><br />
<span class="fullpost">Итак, в качестве первого "функционального" примера я решил попробовать реализовать функцию для <a href="http://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0" target="_blank">быстрой сортировки</a>. Этот пример очень показателен для функционального программирования, так как очень здорово иллюстрирует идею о том, что функциональные программы близки внешне к описанию формулы в математической нотации, описывающей результат, чем к пошаговому описанию алгоритма, приводящего к заданному результату в случае <a href="http://ru.wikipedia.org/wiki/%D0%98%D0%BC%D0%BF%D0%B5%D1%80%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5" target="_blank">императивного программирования</a>. Чтобы проверить эту идею, в принципе, достаточно ознакомится с реализацией этого алгоритма на разных языках в этой <a href="http://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0">статье</a>. Сравните реализацию на <a href="http://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0#C" target="_blank">С</a> и на <a href="http://ru.wikipedia.org/wiki/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0#Haskell" target="_blank">Haskell</a>. Впечатляет? </span><br />
<span class="fullpost">Итак, в общем случае метод сортировки описывается следующей формулой:</span><br />
<span class="fullpost"><i> quickSort([]) = []</i> <br />
<img alt="qs" border="0" height="19" src="http://quatrocode.com/blogspot_images/2007/03/qs.png" style="border-bottom-width: 0px; border-left-width: 0px; border-right-width: 0px; border-top-width: 0px;" width="613" /> </span><br />
<span class="fullpost">Нам ничего не остается, кроме как просто описать эту формулу в несколько иной нотации, а другими словами - реализовать на языке Nemerle:</span><br />
<pre class="brush: csharp;"><span class="fullpost">def qs(l)
{
match(l)
{
| [] => [];
| x :: xs => qs( $[ y | y in xs, y < x ] ) + [x] + qs( $[y | y in xs, y >= x] );
}
}</span></pre><span class="fullpost">Неправда ли, очень похоже на исходную формулу?</span><br />
<span class="fullpost">Ну и пример использования:</span><br />
<pre class="brush: csharp;"><span class="fullpost">System.Console.WriteLine(qs([1, 9, 6, 96, 9, 6, 69, 698, 98, 69, 89, -228, -234, -27, -23, -23, 63407075, 003, 069, 036, 306, 3096]));
</span></pre><span class="fullpost">Результатом сортировки будет:</span><br />
<span class="fullpost">[-234, -228, -27, -23, -23, 1, 3, 6, 6, 9, 9, 36, 69, 69, 69, 89, 96, 98, 306, 698, 3096, 63407075]</span><br />
<span class="fullpost">Ура, первый эксперимент удался! Будем продолжать наши опыты и результат сразу же опубликовывать.</span><br />
<span class="fullpost">В качестве источника научной информации в данном эксперементе использовался цикл лекций "<a href="http://ru.wikibooks.org/wiki/%D0%9E%D1%81%D0%BD%D0%BE%D0%B2%D1%8B_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F" target="_blank">Основы функционального программирвоания</a>".</span></div>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com1tag:blogger.com,1999:blog-8864435396524375431.post-38082512355593431192007-03-12T21:12:00.003+02:002011-11-28T23:00:12.435+02:00Создание собственного дистрибутива Microsoft SQL Server Express<div dir="ltr" style="text-align: left;" trbidi="on">Все разработчики desktop-приложений, работающих с базами данных, всегда сталкивались с проблемами установки этой самой базы данных на машины пользователей. Сценариев, которые можно реализовать для решения данной задачи достаточно много:<br />
<ul><li>Использование файловых баз данных, таких, как Microsoft Access, SQLite, Paradox - разворачивание базы данных представляет собой обычное копирование неких эталонных файлов в какое-нибудь общедоступное место; </li>
<li>Использование существующих серверов баз данных - в этом случае нужно либо в момент установки, либо в момент запуска приложения настроить соединение, сохранить параметры и создать структуру базы данных; </li>
<li>Установка собственных серверов баз данных вместе с приложением - этот вариант я и рассмотрю ниже. </li>
</ul><span class="fullpost"> </span><br />
<span class="fullpost">В качестве сервера, который мы будем "встраивать" в наше приложение мы выберем <a href="http://msdn.microsoft.com/vstudio/express/sql/" target="_blank">Microsoft SQL Server Express</a>. По функциональным возможностям эта редакция сервера аналогична <a href="http://www.microsoft.com/sql/editions/standard/default.mspx" target="_blank">SQL Server 2005 Standard Edition</a>, с некоторыми ограничениями по размеру базы данных, количеству используемой памяти и процессоров. Но для небольших приложений всех этих возможностей будет более чем достаточно. Нужно отметить, что эта редакция намного удобнее в использовании предыдущей версии <a href="http://www.microsoft.com/sql/prodinfo/previousversions/msde/prodinfo.mspx" target="_blank">MSDE 2000</a>. </span><br />
<span class="fullpost">Рассмотрим следующий сценарий развёртывания:</span><br />
<ol><li><span class="fullpost">Пользователь устанавливает приложение самым обычным образом (пусть даже просто вручную копирует бинарные файлы); </span></li>
<span class="fullpost">
<li>При первом запуске приложение проверяет наличие сконфигурированной базы данных и, если таковая не обнаруживается, то пользователю предлагается установить и настроить сервер баз данных; </li>
<li>Если пользователь соглашается, то запускается процесс установки сервера баз данных с заданными параметрами безопасности, названием экземпляра сервера и паролем для пользователя sa; при этом процесс установки должен быть максимально скрыт от пользователя для того, чтобы не нагружать его лишней информацией о приложении; </li>
<li>На только что установленном сервере создаем базу данных, выполнив установочный DDL-скрипт. </li>
</span></ol><span class="fullpost">Способы определения наличия уже установленной базы данных и последующее создание новой базы мы отложим и рассмотрим подробнее третий пункт нашего сценария. </span><br />
<span class="fullpost">Для осуществления нашего плана мы должны иметь в наличии некий исполняемый файл, запустив который из нашего приложения мы и получим по завершению установленный экземпляр SQL Server с заданными параметрами.</span><br />
<span class="fullpost">Итак, рассмотрим процесс по шагам.</span><br />
<ol><li><span class="fullpost">Скачиваем дистрибутив с сайта <a href="http://msdn.microsoft.com/vstudio/express/sql/" target="_blank">Microsoft SQL Server Express</a>. </span></li>
<span class="fullpost">
<li>Распаковываем установочные файлы. <br />
На этом сайте доступны версии, упакованные в некий файл вида SQLEXPR32_RUS.EXE, а нам нужно получить доступ непосредственно к файлу Setup.exe, который можно будет запустить с заданными параметрами. В этом месте мы должны выполнить фокус: дело в том, что перво-наперво скачанный инсталятор просто распаковывает содержимое инсталляционного пакета во временную директорию. Наша задача эти файлы оттуда перехватить. Задача это достаточно проста: после запуска инсталляции этот путь можно получить из такого окошка: <br />
<br />
<img alt="Распаковка файлов" border="0" height="128" src="http://quatrocode.com/blogspot_images/2007/03/1_extracting_files.png" style="border-bottom: 0px; border-left: 0px; border-right: 0px; border-top: 0px;" width="336" /> <br />
<br />
После того, как запустится непосредственно сам инсталлятор из архива, копируем содержимое этот папки, потому как после завершения работы инсталлятора - эта папка будет удалена. <br />
</li>
<li>Конфигурация инсталлятора. <br />
В директории с инсталлятором вы найдете файл template.ini, в котором можно прописать все необходимые параметры установки по умолчанию. Дело в том, что правильно сделанный инсталятор должен уметь принимать все параметры установки из командной строки для того, чтобы всю эту установку автоматизировать. Помогает это в тех случаях, когда, например, от конечного пользователя нужно спрятать всю рутину, связанную с этим процессом, доверившись параметрам по умолчанию. Ведь даже опытные пользователи в 90% случаев установки в основном нажимают кнопку Next, не изменяя параметров по умолчанию. <br />
В этом файле можно указать все параметры, которые запрашиваются в диалогах. А для того, чтобы запустить установку с этими параметрами нужно указать инсталлятору, откуда брать эти параметры. Это делается с помощью такого запуска: <br />
<br />
<code>setup.exe /qb /settings c:\template.ini</code> <br />
<br />
<ul><li>Ключ /qb указывает инсталлятору, что нужно принять все параметры и не дать пользователю их изменить через всякие диалоги. Таким образом пользователь будет отслеживать процесс установки, но не сможет ничего изменить. Альтернативный этому ключ /qn указывает инсталлятору, что процесс установки нужно проводить полностью в фоновом режиме. </li>
<li>Ключ /settings указывает, что параметры установки следует взять из указанного файла. </li>
</ul>Альтернативой этому способу является указание всех параметр прямо в командной строке: <br />
<br />
<code>setup.exe /qn ADDLOCAL=SQL_Engine SAPWD=password SECURITYMODE=SQL</code> <br />
<br />
Таким образом можно установить экземпляр сервера по умолчанию с заданным паролем для пользователя sa в совершенно невидимом режиме. Завершающем штрихом в этом пункте будет создание некого Install.bat со строкой запуска установки с заданными параметрами. Но мы на этом не остановимся. <br />
</li>
<li>Выбор архиватора. <br />
Если вы помните, скачанный дистрибутив был представлен в виде исполняемого файла SQLEXPR32_RUS.EXE, размер которого был во много раз меньше развёрнутого инсталлятора. Наша задача - получить в результате нечто подобное, с той лишь разницей, что запуск нашего дистрибутива сразу начнёт установку с заданными параметрами. Опустив сравнительный анализ разных архиваторов, остановимся на замечательном архиваторе <a href="http://www.7-zip.org/" target="_blank">7-zip</a>. Для нашей задачи он показался мне наиболее подходящим. <br />
Следующим шагом будет создание архива максимальной степени сжатия примерно с такими параметрами: <br />
<br />
<img alt="Добавить в архив" border="0" height="477" src="http://quatrocode.com/blogspot_images/2007/03/2_add_to_archive.png" style="border-bottom: 0px; border-left: 0px; border-right: 0px; border-top: 0px;" width="543" /><br />
<br />
Размер архива получился около 36 мегабайт, что очень близко к оригинальному дистрибутиву. <br />
</li>
<li>Создание SFX-архива. <br />
В предыдущем пункте мы сознательно не создали SFX-архив сразу, потому что нам нужна не просто возможность распаковать архив на произвольной машине, а распаковать в директорию по умолчанию и запустить после установки на выполнение установку с заданными параметрами. Все эти задачи делаются легко и просто с помощью существующих SFX-модулей. Детали того, как это делается с помощью стандартных средств 7-zip можно найти по <a href="http://oszone.net/3171" target="_blank">этому</a> адресу. Но мы воспользуемся альтернативным модулем, найти который можно по <a href="http://7zsfx.solta.ru/" target="_blank">этому</a> адресу. <br />
Из скачанного пакета альтернативных модулей выбираем файл 7ZSD_LZMA.sfx и помещаем его рядом с архивом, который мы хотим "оживить". Если у вас есть желание, то можно с помощью утилиты <a href="http://www.angusj.com/resourcehacker/" target="_blank">ResourceHacker</a> и этой заметки поменять иконку у SFX модуля на <img alt="Иконка" border="0" height="16" src="http://quatrocode.com/blogspot_images/2007/03/3_icon.gif" style="border-bottom: 0px; border-left: 0px; border-right: 0px; border-top: 0px;" width="16" />, предварительно извлечённую с помощью все того же <a href="http://www.angusj.com/resourcehacker/" target="_blank">ResourceHacker</a>-а из инсталлятора Microsoft SQL Server Express. Затем размещаем в этой же директории текстовый файл Config.txt такого содержимого: <br />
<br />
<code>;!@Install@!UTF-8! <br />
RunProgram="setup.exe /qn ADDLOCAL=SQL_Engine SAPWD=password SECURITYMODE=SQL" <br />
GUIMode="2" <br />
;!@InstallEnd@!</code> <br />
<br />
После чего в командной строке выполняем следующую команду: <br />
<br />
<code>copy /b 7ZSD_LZMA.sfx + config.txt + SQL2005E.7z SQL2005E.exe</code> <br />
<br />
Файл SQL2005E.exe, полученный в результате этой операции и будет нашим дистрибутивом, который можно распространять со своим приложением. <br />
</li>
<li>Интеграция в приложение. <br />
Этот пункт, пожалуй, самый простой. В зависимости от того, каким образом вы собираетесь устанавливать Microsoft SQL Server Express (в момент установки приложения или после проверки при каждом запуске) и будет проходить интеграция. Это может быть либо Custom Action для Windows Installer, либо в коде вашего приложения будет присутствовать подобный код: <pre class="brush: csharp;">ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = filePath;
Process p = new Process();
p.StartInfo = psi;
p.Start();
p.WaitForExit();
if (p.ExitCode != 0)
throw new ApplicationException("Ошибка при установке");</pre></li>
</span></ol><span class="fullpost">Более подробную информацию о встраивании Microsoft SQL Server Express в свои приложения можно найти в статье <a href="http://msdn2.microsoft.com/en-us/library/bb264562.aspx" target="_blank">Embedding SQL Server Express into Custom Applications</a>.</span><br />
</div>Igorhttp://www.blogger.com/profile/10232785741897411593noreply@blogger.com0