среда, 4 июля 2012 г.

MccTomskCamp 2. Continuous Testing в .NET

Выступил с докладом "Continuous Testing в .NET". Выложил видео и презентацию на хабре: http://habrahabr.ru/post/147055/

понедельник, 4 июня 2012 г.

MccTomskCamp 1. Выбор тестового фреймворка: XUnit + FluentAssertions

В Томске все довольно уныло с user group'ами, и я давно подумывал организовать нечто подобное на базе нашей компании (МЦЦ Томск). Все никак руки не доходили, а тут вот дошли. В пятницу состоялось первое собрание на тему "Выбор тестового фреймворка: XUnit + FluentAssertions". Тема не самая удачная, да и выступление получилось немного скомканным, но для начала пойдет (дальше будет лучше). Мы пока не определились с площадкой, поэтому видео и презентацию с первой встречи выкладываю здесь.

Видео:


Презентация:

Если вы живете в Томске, и хотите к нам присоединиться в роли слушателя или докладчика, добро пожаловать. Основная тема - .NET, но мы, судя по всему, будем периодически отклоняться от нее в сторону более фундаментальных вопросов и в сторону конкурирующих платформ.

среда, 14 сентября 2011 г.

BetTeamsBattle - Kick bookmaker's ass

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

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

Почему Bet Teams Battle?

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

В результате я решил использовать более абстрактное название Bet Teams Battle, а не HandiCappers Battle. Не уверен, насколько это правильное решение, и с удовольствием выслушаю ваше мнение на этот счет в комментариях. Кстати, сервис доступен еще по двум адресам: cappersbattle.com и handicappers.com, и я все еще рассматриваю вариант сделать один из них основным.

Суть проекта

Пользователь делает ставки в рамках турнира:

После того, как ставка сделана, она появляется в списке ставок:

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

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


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

Исходный код проекта открыт и лежит на Google Code, а в этой гугл-группе можно обсудить вещи, связанные с разработкой. Если хотите потыкать в меня палкой за говнокод, то сделать это можно в этой ветке, а если хотите присоединиться к разработке проекте, то в этой.

Ближайшие планы по разработке:
1. Проработка системы турниров
2. Работа над упрощением/автоматизацией открытия и закрытия ставок
3. Реализация поддержки пользовательских турниров
4. Улучшение реализации статистики

P. S. Чтобы можно было поиграться с проектом до старта первого турнира, я поднял своего рода зеркало по адресу test.betteamsbattle.com.

суббота, 16 июля 2011 г.

Интеграционные vs. Unit-тесты

Три основных подхода касательно взаимоотношения интеграционных и unit-тестов:

1. Integration-driven. Изначально делаем тест интеграционным, используя реальные реализации зависимостей. При необходимости что-нибудь застабить/замокать подменяем необходимые зависимости на (внимание!) стабы/моки соответственно.
2. Unit-driven. Изначально делаем юнит-тест, а значит стабим/мокаем все зависимости. При необходимости заменяем некоторые зависимости реальными реализациями
3. Mixed. Основую логику тестируем unit-тестами, но пишем один-два интеграционных теста на основные сценарии.

Пост навеян вопросом на StackOverflow. Мне особенно симпатичен ответ ThomasArdal (который за mixed-вариант): хоть TDD и способствует использованию второго подхода, ничто не мешает в конце написать интеграционный тест. К сожалению, я пока в основном использую первый подход (медленный, но надежный), но потихоньку буду переходить к третьему.

А что по этому поводу думают мои дорогие читатели?

среда, 13 июля 2011 г.

понедельник, 6 июня 2011 г.

Стабить или не стабить репозитории?

Для тестировании кода, который использует репозитории, есть два основных подхода:

1. Стабить и мокать вызовы репозиториев
2. Использовать реальные реализации репозиториев (а значит работать с базой)

Какой из вариантов выбрать - большой вопрос и не меньший повод для холивара.

Возьмем для примера следующий сервис:

public class ParametersService : IParametersService
{
private readonly IParametersRepository _parametersRepository;

public ParametersService(IParametersRepository parametersRepository)
{
_parametersRepository = parametersRepository;
}

public Parameter GetParameterOrAddNew(string parameterName)
{
var parameter = _parametersRepository.GetParameterOrDefault(parameterName);
if (parameter == null)
parameter = _parametersRepository.Add(parameterName);
return parameter;
}
}


А теперь напишем тест (возьмем для примера NUnit и RhinoMocks) для первого:


private IParametersService _parametersService;

public void SetUp(IParametersRepository parametersRepository)
{
_parametersService = new ParametersService(parametersRepository);
}

[Test]
public void GetParameterOrDefault_NoParameter_ParameterIsAdded()
{
//создаем Stub + Mock
var parametersRepository = MocksRepository.GenerateMock<IParametersRepository>();
//Эмулируем отсутствие параметра
parametersRepository.Stub(pr => pr.GetParameterOrDefault("testParameterName")).Return(null);

SetUp(parametersRepository);

var parameter = _parametersService.GetParameterOrAddNew("testParameterName");

//Проверяем, что метод Add был вызван
parametersRepository.AssertWasCalled(pr => pr.Add("testParameterName"));
}

и второго:


private IParametersService _parametersService = new ParametersService(new ParametersRepository);

[Test]
public void GetParameterOrDefault_NoParameter_ParameterIsAdded()
{
var parameter = _parametersRepository.GetParameterOrDefault("testParameterName");
//Проверяем, что такого параметра нет
Assert.IsNull(parameter);

var parameter = _parametersService.GetParameterOrAddNew("testParameterName");

Assert.IsNotNull(parameter);
Assert.AreEqual("testParameterName", parameter.Name);
}


Какую из реализаций выбрать?

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

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

В .NET распространены два подхода к реализации паттерна репозиторий:
1 (классический). Инкапсуляция запроса в рамках метода репозитория. Методы репозитория возвращают сущность/коллекцию сущностей.
2. Легковесный репозиторий, который возвращает IQueryable, который, в свою очередь, уже вне репозитория обрастает фильтрами, сортировками, пейджингом и т. д. посредством паттерна Specification

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

Вторая же реализация больше подходит ко второму подходу к тестированию. Во-первых, раз мы возвращаем IQueryable, то находимся в зоне действия не LINQ To Objects, а LINQ To Entities, LINQ To NHibernate и т. д. Если же мы застабим репозиторий, то будем работать с LINQ To Objects, а значит наш код будет тестировать не то поведение, которое будет в продакшене. Во-вторых, ввиду того, что репозиторий не возвращает конечный результат выполнения запроса (как в первой реализации), такую реализацию репозитория не очень удобно стабить.

Нельзя не отметить, что производительность второго подхода гораздо ниже. Но давайте не будем забывать, что это тесты, а не продакшен. Говоря о производительности, мы должны ответить всего на один вопрос: достаточно ли высока производительность для того, чтобы не тормозить работу программиста при прогонке тестов? Вопреки распространенному мнению, зачастую ответ на этот вопрос будет положительным. Ведь локально мы прогоняем тесты лишь для модуля/модулей, над которым непосредственно в данный момент работаем. А сделать так, чтобы тесты всех модулей не тормозили на build-сервере, - задача решаемая (ведь build-сервер - это мощный сервер с современным железом, а не старый списанный десктоп, да?). Если производительность второго подхода вас не устраивает, но использовать первый подход не хочется, можно попробовать использовать in-memory базы данных: они практически выравнивают производительность двух подходов.

Производительность - не единственная проблема второго подхода: необходимо решить вопрос очистки базы данных после каждого теста. Для этого можно и накатывать после каждого теста базу (медленно решение), и откатываться к некоторому snapshot'у (работает быстрее, но поддерживается не всеми СУБД), и использовать транзакций без их последующего коммита. Я предпочитаю последний вариант за его универсальность и удовлетворяющую моим требованиям произвидительность. К тому же, благодаря TransactionScope реализовать этот подход очень просто. Правда, как я отмечал в своей недавней статье про TransactionScope, этот подход не работает, если в коде используется TransactionScope в режиме RequiresNew или Suppress. Заинтересовавшимся вопросом восстановления состояния базы данных, рекомендую серию постов на английском языке: раз, два, три

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

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

PP. S. Добавил кросспост на Хабр: http://habrahabr.ru/blogs/net/120755/

четверг, 2 июня 2011 г.

Опишите TFS одним предложением

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

It is like someone took the 5 worst tools they could find for source control (VSS excluded, of course,) testing, issue tracking, automated build and code analysis and put them all in one product

А как бы вы описали TFS?

воскресенье, 29 мая 2011 г.

Впечатления от конференции DevCon

На этой неделе проходила, пожалуй, самая крупная Microsoft-oriented российская конференция - DevCon.

Два дня - четверг и пятница - получились для меня очень насыщенными. Четверг начался с 4-х часового полета из Томска в Москву. Затем от Внуково мы с коллегой еще несколько часов добирались на такси до дома отдыха, где проходила конференция. И лишь благодаря часовым поясам (-3) успели к началу.

Организаторы (Microsoft Россия) постарались на славу. Было все: приглашенные звезды из Америки, отличные выступления, концерт вечером первого дня, отличное проживание и питание и т. д. Вообще программа была очень насыщенной и вечером первого дня (особенно на фоне перелета, переезда, да еще и болезни) голова просто раскалывалась.

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

Например, был показан CSS3 тест, который в IE10 Preview 1 работал быстрее, чем в текущей версии Google Chrome (в котором, вероятно, была отключена GPU Acceleration). Непонятно, зачем такие дешевые приемчики обыгрывать на профессиональной аудитории.

Или еще одна сценка. Вызывают на сцену парня. Он в течение двух минут рассказывает, что их компания занимается разработкой игр, причем в IE 9 их игры работают в 4 раза быстрее, чем в Google Chrome - и уходит. Опять какая-то желтизна. Я бы понял, если бы это был пресс-релиз для журналистов, но на профессиональной конференции разработчиков подобные вещи вызывают недоумение.

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

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

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

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

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

В целом, я остался доволен, а негативные моменты спишем на мою вредность и придирчивость.

По результатам конференции, вероятность того, что после окончания учебы (защищаюсь через 2 недели) я уеду из Томска, повысилась до 90 процентов.

To Tuple Or Not To Tuple

Tuple (кортеж) - последовательность из конечного числа элементов.

Кортежи особенно популярны в функциональных языках программирования, где поддерживается их удобное создание, декомпозиция, pattern matching и т. д. (подробнее на примере F#). В .NET 4 появился тип Tuple, тем самым сделав массовым (до этого те, кто были в теме, писали/генерировали Tuple самостоятельно) использование кортежей и в императивных языках программирования. Однако ввиду отсутствия поддержки на уровне языка Tuple в C# превращается в простое хранилище разнородных элементов.

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

public void Process(Tuple<int, int> tuple)
{
...
var item = Retrieve(tuple.Item1);
...
}

или с

public void Process(IdAndValue idAndValue)
{
...
var item = Retrieve(idAndValue.Id);
...
}
?
Я бы предпочел второй вариант: у свойств есть имена, несущие некоторую семантику, что снижает сложность поддержки и уменьшает вероятность допустить ошибку. Согласитесь, по ошибке вызвать tuple.Item2 вместо tuple.Item1 гораздо проще, нежели idAndValue.Value вместо idAndValue.Id. Именно такой банальной ошибкой, которая не была отловлена тестами (потому что в тестах она была с легкостью повторена) и навеян этот пост.

Использование Tuple в C# вполне себе может быть уместно, но злоупотреблять этим не стоит.

понедельник, 23 мая 2011 г.

Следуете ли вы принципам SOLID при объектно-ориентированном дизайне и программировании?

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

вторник, 17 мая 2011 г.

Linq To Entities vs. Linq To Objects на примере группировки

LINQ - удобная, красивая, но при этом довольно коварная абстракция. Самые неожиданные вещи обычно происходят на стыке какой-либо реализации LINQ и LINQ To Objects. Сегодня на одном примере я рассмотрю совместную работу LINQ To Entities (Entity Framework) и LINQ To Objects.

За основу возьмем метод репозитория, который принимает на вход список идентификаторов клиентов и возвращает сгруппированный по этим идентификаторам набор заказов (таблица Orders содержит поля OrderId, OrderDate и CustomerId):

public IDictionary<long, List<Order>> GetOrdersByCustomersIds(IList<long> customersIds)
{
using (var ctx = new RepositoryContext())
{
return ctx.Orders.
Where(o => customersIds.Contains(o.Id)).
GroupBy(o => o.CustomerId).
ToDictionary(o => o.Key, o => o.ToList());
}
}


Минуточку! А как это работает? Ведь при выполнении GROUP BY запроса мы можем выбрать лишь поля, по которым происходит группировка, а также агрегированные значения. Стандартным решением этой проблемы является JOIN данных таблицы и результатов группировки. Примерно так:

SELECT o1.*, MinTotal
FROM Orders as o1
INNER JOIN
(SELECT o2.CustomerId, Min(o2.Total) as MinTotal
FROM Orders o2
GROUP BY o2.CustomerId) as o3
ON o1.CustomerId = o3.CustomerId
Where o1.CustomerId in (1, 2, 3, 4, 5)

Что-то в этом духе и должен сгенерировать EF-провайдер. Давайте убедимся в этом. У меня под рукой был MySQL .NET Connector (официальный ADO.NET-провайдер для MySQL), поэтому я воспользовался им и получил следующий сгенерированный запрос (передав на вход список из идентификаторов от 1 до 5):

SELECT `Project2`.`C1`,
`Project2`.`CustomerId`,
`Project2`.`C2`,
`Project2`.`CustomerId1`,
`Project2`.`Id`,
`Project2`.`OrderDate`
FROM
(SELECT `Distinct1`.`CustomerId`,
1 AS `C1`,
`Extent2`.`CustomerId` AS `CustomerId1`,
`Extent2`.`Id`,
`Extent2`.`OrderDate`,
CASE WHEN (`Extent2`.`CustomerId` IS NULL) THEN (NULL) ELSE (1) END AS `C2`
FROM
(SELECT DISTINCT `Extent1`.`CustomerId`
FROM `orders` AS `Extent1`
WHERE ((1 = `Extent1`.`Id`) OR (2 = `Extent1`.`Id`)) OR (((3 = `Extent1`.`Id`) OR (4 = `Extent1`.`Id`)) OR (5 = `Extent1`.`Id`))) AS `Distinct1`
LEFT OUTER JOIN `orders` AS `Extent2`
ON (((1 = `Extent2`.`Id`) OR (2 = `Extent2`.`Id`)) OR (((3 = `Extent2`.`Id`) OR (4 = `Extent2`.`Id`)) OR (5 = `Extent2`.`Id`))) AND (`Distinct1`.`CustomerId` = `Extent2`.`CustomerId`)) AS `Project2`
ORDER BY `CustomerId` ASC, `C2` ASC

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

Стоп! А зачем мы используем группировку на уровне базы данных? Группировка оправдана в случае использования функций агреграции (как в приведенной выше ручной реализации запроса). В нашем же случае группировка - лишь удобное представление полученных данных. Давайте слегка модифицируем метод репозитория и перенесем процесс группировки на уровень LINQ To Objects:

public IDictionary<long, List<Order>> GetOrdersByCustomersIds(IList<long> customersIds)
{
using (var ctx = new RepositoryContext())
{
return ctx.Orders.
Where(o => customersIds.Contains(o.Id)).
AsEnumerable().
GroupBy(o => o.CustomerId).
ToDictionary(o => o.Key, o => o.ToList());
}
}

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

SELECT `Extent1`.`CustomerId`,
`Extent1`.`Id`,
`Extent1`.`OrderDate`
FROM `orders` AS `Extent1`
WHERE ((1 = `Extent1`.`Id`) OR (2 = `Extent1`.`Id`)) OR (((3 = `Extent1`.`Id`) OR (4 = `Extent1`.`Id`)) OR (5 = `Extent1`.`Id`))

Определенно этот запрос эффективнее предыдущего.

Вот, собственно, и все. Ничего особенного - лишь хотел заострить ваше внимание на коварности перехода от LINQ To X к LINQ To Objects после того, как сам попал в эту ловушку. Будьте бдительны!

P. S. Несмотря на то, что я использовал MySQL .NET Connector, категорически не рекомендую применять этот провайдер в продакшене: это не провайдер, а коцентрированный сгусток багов, которые не фиксятся годами.

PP. S. Кросспост на Хабре: http://habrahabr.ru/blogs/net/119624/

Entity Framework и MySQL

Если вы вдруг соберетесь использовать Entity Framework в связке MySQL, ни за что, на при каких обстоятельствах не используйте родной провайдер MySQL .NET Connector. Это не ADO.NET-провайдер, а кишащее критичными багами, которые не фиксятся годами, недоразумение (по крайней мере в области поддержки Entity Framework).

Из сторонних альтернатив я бы посоветовал продукт dotConnect for MySQL от компании DevArt. Он платный, но стоит вполне адекватных денег.

Я некоторое время назад сделал неправильный выбор, остановившись на стандартном провайдере. Надеюсь, вы не повторите моей ошибки.

вторник, 10 мая 2011 г.

Как не нужно прятать вещи

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

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

Обыскал всю квартиру. На несколько раз. Посмотрел везде, даже в самых невероятных местах: в холодильнике, микроволновой печи и т. д. :) Начал строить разные догадки. Может, сквозняком унесло? Или вместе с мусором выбросил? Да нет, ну бред же!

Я уже был готов строить конспирологические теории - но мой взгляд упал на красивую коробочку D-Link. И тут все встало на свои места. Давеча я купил рутер D-Link и разложил его содержимое на диване, а после установки и настройки сложил документацию рутера обратно в коробку. И, как вы понимаете, в эту документацию затесалось то самое учебное пособие. И если бы коробка не была такой яркой, или если бы она лежала не на видном месте, никогда бы я не нашел пропажу, и не успел бы сделать задание вовремя. Мир бы от этого не перевернулся, но наши отношения с деканатом на фоне моего повального непосещения учебы и так держатся на "добром" слове.

Happy end.

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

Новый шаблон

Я таки преодолел свою лень и поставил новый, более широкий шаблон. Надеюсь, теперь читать исходники будет удобнее.

P. S. Перешел на темную сторону :)

среда, 4 мая 2011 г.

Приходилось ли вам нарушать GPL?

Запустил на Хабре опрос "Приходилось ли вам нарушать GPL?"

Мой вариант - "да". Стыдно ли мне? Да.

вторник, 3 мая 2011 г.

C# и синтаксический сахар

Вот смотрю я сейчас на код своего проекта и вижу десятки тестов, содержащих строчки вроде этой:

DictionaryAssert.AreMultiEquivalent(
new Dictionary<long, IList<long>>()
{
{
ConstantParameterIds.BookFormat,
new List<long>() {ConstantBookFormatValueIds.Paperback}
}
},
valuesDictionary);

Очень много лишних символов, не находите? На 5 минут представлю себя в роли архитектора C# и немного пофантазирую.

Сначала добавим вывод generic-типов для конструкторов:

DictionaryAssert.AreMultiEquivalent(
new Dictionary()
{
{
ConstantParameterIds.BookFormat,
new List() {ConstantBookFormatValueIds.Paperback}
}
},
valuesDictionary);

Я уже недавно сетовал на отсутсвие этой фичи в C#, и упоминал статическую фабрику в качестве workaround'а (new Tuple(5, 5) vs. Tuple.Create(5, 5)). Однако этот workaround не распространяется на случай использования инициализации через фигурные скобочки (как такая инициализация по-умному называется?).

Теперь введем специальный синтаксис для создания словарей и списков:

DictionaryAssert.AreMultiEquivalent(
{
ConstantParameterIds.BookFormat: {ConstantBookFormatValueIds.Paperback}
},
valuesDictionary);

Ну и заодно офтопом введем специальный синтаксис для кортежей:

var tuple = (5, 5); // syntax sugar for "new Tuple<int, int&пt;(5, 5)"

Закругляюсь, пока Хейлсберг не заметил :)

Как по-вашему, код стал лучше или хуже?

Update
В комментариях Shaddix предложил интересный вариант:

DictionaryAssert.AreEqual(
new []
{
Tuple.Create(ConstantParameterIds.BookFormat, new[] {ConstantBookFormatValueIds.Paperback})}
},
valuesDictionary.ToMultiArrayOfTuples());
, где ToMultiArrayOfTuples - метод расширения:

public Tuple<T, K>[] ToMultiArrayOfTuples(this IDictionary<T, List<K>> dictionary)
{
return dictionary.Select(x => Tuple.Create(x.Key, x.Value.ToArray())).ToArray();
}
Лично мне на этот тест приятнее смотреть: ничто не не отвлекает взгляд от того, какие данные мы ожидаем. Причем, в отличие от моих вышеизложенных предложений, этот код не является фантазией, а решает задачу минимизации синтаксической избыточности в рамках существующих возможностей C#.

пятница, 29 апреля 2011 г.

Допиливаем HtmlAgilityPack. Часть 2

У класса HtmlNode есть свойство NextSibling, возращающее следующий элемент того же уровня (далее - сиблинг). Однако зачастую нужно получить следующий сиблинг с определенным тегом. Даже в самой простой ситуации:

anchor1
NextSibling для a1 вернет пробельный символ, а не a2. Поэтому я посчитал целесообразным добавить метод NextSiblingWithName (предложите лучшее название):

public HtmlNode NextSiblingWithName(string name)
{
var node = _nextnode;
while (node != null)
{
if (node.Name == name)
return node;

node = node.NextSibling;
}

return node;
}

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

var nodes = document.DocumentNode.SelectNodes("xpath query");
if (nodes.Count == 0)
throw new Exception("Expected at least one node");

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

public static List CountIsEqualTo(this List list, int count)
{
if (list.Count != count)
throw new Exception(String.Format("Expected count = {0} but was {1}", count, list.Count));

return list;
}

public static List CountIsNotEqualTo(this List list, int count)
{
if (list.Count == count)
throw new Exception(String.Format("Expected count != {0} but was {1}", count, list.Count));

return list;
}

public static List CountIsNotZero(this List list)
{
return CountIsNotEqualTo(list, 0);
}

public static List CountIsMoreThan(this List list, int count)
{
if (list.Count <= count)
throw new Exception(String.Format("Expected count > {0} but was {1}", count, list.Count));

return list;
}

public static List CountIsEqualOrMoreThan(this List list, int count)
{
if (list.Count < count)
throw new Exception(String.Format("Expected count >= {0} but was {2}", count, list.Count));

return list;
}

Самый популярный метод в моих парсерах - CountIsNotZero:

var nodes = document.DocumentNode.SelectNodes("xpath query").CountIsNotZero();

Отмечу, что для случаев, когда возвращается один элемент, все же удобнее использовать методы SelectSingleNode, SelectSingleNodeOrDefault, SelectFirstNode, SelectFirstNodeOrDefault, которые мы реализовали в первой части.

вторник, 26 апреля 2011 г.

Dependency Inversion Principle и инкапсуляция

При всей моей любви к Dependency Inversion Principle, меня всегда немного смущало то, как он соотносится с инкапсуляцией.

Давайте разберемся на примерах. И вот первый из них:

public class Car : ICar
{
private readonly IEngine _engine;

public Car(IEngine engine)
{
_engine = engine;
}

...
}

Есть машина, и у нее есть двигатель. Причем на разные машины могут ставиться разные двигатели. Пока все хорошо, да? Тогда перейдем ко второму примеру:

public class MyValidator : IValidator
{
private readonly IChecksumGenerator _checksumGenerator;

public MyValidator(IChecksumGenerator checksumGenerator)
{
_checksumGenerator = checksumGenerator;
}

...
}

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

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

Update.
В комментариях возникли сомнения по поводу того, нарушает ли второй пример инкапсуляцию. Чтобы разобраться с этим вопросом, давайте рассмотрим второй пример без инъекции IChecksumGenerator (то, как это обычно и делается без SOLID'а):

public class MyValidator : IValidator
{
private readonly IChecksumGenerator _checksumGenerator;

public MyValidator()
{
_checksumGenerator = new MyChecksumGenerator();
}

...
}

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

Update 2.
Я задал такой же вопрос на stackoverflow, и Mark Seemann (автор книги Dependency Injection in .NET) дал очень точный диагноз моему коду - нарушение Liskov Substitution Principle. Действительно! Если мой валидатор будет работать только с определенными наследниками IChecksumGenerator, нужно инжектить зависимость по более специфичному интерфейсу. И тогда любая реализация данного интерфейса, переденная в качестве зависимости, отработает корректно. Ума не приложу, почему у меня из головы вылетел LSP, ведь это так просто и очевидно!

Мораль: Все принципы Object-Oriented Design очень тесно связаны - вспоминая один, не забывайте о других.

понедельник, 25 апреля 2011 г.

Расширяем Regex

90% моей работы с регулярными выражениями происходит по следующему сценарию:
var match = Regex.Match(input, pattern);
if (!match.Success)
throw new Exception("Can't match input to pattern");
...

Написав в очередной раз эти 3 строчки, я понял, что дальше так жить нельзя - нужно реализовать метод MatchForSure (наверное, не самое удачное название - предложите свой вариант).

Печально, но C# не поддерживает extension methods, которые можно было бы вызывать как статические методы. Поэтому мы можем лишь сделать обертку или вспомогательный класс, например, вот так:
public static class RegexExtensions
{
public static Match MatchForSure(string input, string pattern)
{
var match = Regex.Match(input, pattern);
if (!match.Success)
throw new Exception(String.Format("Can't match {0} to regex pattern {1}", input, pattern));
return match;
}
}

Три строчки из начала поста сократятся до одной:

var match = RegexExtensions.MatchForSure(input, pattern);
...

Если же вы предпочитаете использовать Regex через инстанс, то можно реализовать полноценный extension method:
public static class RegexExtensions
{
public static Match MatchForSure(this Regex regex, string input)
{
var match = regex.Match(input);
if (!match.Success)
throw new Exception(String.Format("Can't match {0} to regex pattern {1}", input, pattern));
return match;
}
}

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

var match = new Regex(pattern).MatchForSure(input);
...

Мораль в стиле капитана очевидность: Любая повторяющаяся логика должна быть выявлена и выделена в отдельную сущность. Чем раньше, тем лучше.

воскресенье, 24 апреля 2011 г.

Допиливаем HtmlAgilityPack. Часть 1

HtmlAgilityPack - пожалуй, самый популярный парсер HTML под .NET. В транке SVN'а лежит версия 1.4, но я бы посоветовал использовать версию 2 из бранчи. Я внес некоторые изменения в эту ветку и зарепортил их в виде патча. Пока неизвестно, попадут эти изменения в репозиторий или нет, поэтому вкратце опишу их в этом посте: может, тебе, мой дорогой читатель, эти изменения тоже пригодятся.

Центром библиотеки является класс HtmlDocument. В юнит-тестах у меня сплошь да рядом встречалась такая коснтрукция

[Test]
public void Test1()
{
var doc = new HtmlDocument();
doc.LoadHtml(SomeResource.SomePredefinedHtmlString);
...
}

Чтобы избавиться от лишней строчки, я добавил в HtmlDocument статический метод CreateFromHtml:

public static HtmlDocument CreateFromHtml(string html)
{
var document = new HtmlDocument();
document.LoadHtml(html);
return document;
}

Тогда тест приобретет следующий вид:

[Test]
public void Test1()
{
var doc = HtmlDocument.CreateFromHtml(SomeResource.SomePredefinedHtmlString);
...
}

Каждая нода документа позволяет выполнить XPath-запрос посредством методов SelectNodes и SelectSingleNode. Давайте остановимся на текущей реализации второго метода:

public IHtmlBaseNode SelectSingleNode(string xpath)
{
if (xpath == null)
{
throw new ArgumentNullException("xpath");
}

HtmlNodeNavigator nav = new HtmlNodeNavigator(_ownerdocument, this);
XPathNodeIterator it = nav.Select(xpath);
if (!it.MoveNext())
{
return null;
}

HtmlNodeNavigator node = (HtmlNodeNavigator)it.Current;
return node.CurrentNode;
}

Хм... Так не пойдет. Single - это один и только один. Давайте исправим логику метода SelectSingleNode, а для удобства реализуем еще и SelectSingleNodeOrDefault, SelectFirstNode и SelectFirstNodeOrDefault - в лучших традициях LINQ:

public IHtmlBaseNode SelectSingleNode(string xpath)
{
var it = CreateXPathNodeIterator(xpath);

if (!it.MoveNext()) // 0
throw new InvalidOperationException("Sequence contains no elements");

var nodeNavigator = (HtmlNodeNavigator)it.Current;
var currentNode = nodeNavigator.CurrentNode;

if (it.MoveNext()) // >1
throw new InvalidOperationException("Sequence contains more than one element");

return currentNode;
}

public IHtmlBaseNode SelectSingleNodeOrDefault(string xpath)
{
var it = CreateXPathNodeIterator(xpath);

if (!it.MoveNext()) // 0
return null;

var nodeNavigator = (HtmlNodeNavigator)it.Current;
var currentNode = nodeNavigator.CurrentNode;

if (it.MoveNext()) // >1
throw new InvalidOperationException("Sequence contains more than one element");

return currentNode;
}

public IHtmlBaseNode SelectFirstNode(string xpath)
{
var it = CreateXPathNodeIterator(xpath);

if (!it.MoveNext()) // 0
throw new InvalidOperationException("Sequence contains no elements");

var nodeNavigator = (HtmlNodeNavigator)it.Current;
return nodeNavigator.CurrentNode;
}

public IHtmlBaseNode SelectFirstNodeOrDefault(string xpath)
{
var it = CreateXPathNodeIterator(xpath);

if (!it.MoveNext()) // 0
return null;

var nodeNavigator = (HtmlNodeNavigator)it.Current;
return nodeNavigator.CurrentNode;
}

private XPathNodeIterator CreateXPathNodeIterator(string xpath)
{
if (xpath == null)
{
throw new ArgumentNullException("xpath");
}

var nav = new HtmlNodeNavigator(_ownerdocument, this);
return nav.Select(xpath);
}

Обратите внимание на то, что методы SelectNodes, SelectSingleNode, SelectSingleNodeOrDefault, SelectFirstNode и SelectFirstNodeOrDefault возвращают объекты типа IHtmlBaseNode. Однако зачастую после выполнения запроса приходится приводить IHtmlBaseNode к HtmlNode (тег), AttributeNode (атрибут) или HtmlTextNode (текст). Давайте сделаем так, чтобы можно было задавать тип возвращаемой ноды/нод. Для примера возьмем SelectSingleNode, сделаем его generic и создадим ряд методов-оберток:

public T SelectSingleNode(string xpath) where T: IHtmlBaseNode
{
var it = CreateXPathNodeIterator(xpath);

if (!it.MoveNext()) // 0
throw new InvalidOperationException("Sequence contains no elements");

var nodeNavigator = (HtmlNodeNavigator) it.Current;
var currentNode = (T)nodeNavigator.CurrentNode;

if (it.MoveNext()) // >1
throw new InvalidOperationException("Sequence contains more than one element");

return currentNode;
}

public IHtmlBaseNode SelectSingleNode(string xpath)
{
return SelectSingleNode(xpath);
}

public HtmlNode SelectSingleHtmlNode(string xpath)
{
return SelectSingleNode(xpath);
}

public HtmlAttribute SelectSingleAttributeNode(string xpath)
{
return SelectSingleNode(xpath);
}

public HtmlTextNode SelectSingleTextNode(string xpath)
{
return SelectSingleNode(xpath);
}

Аналогичный рефакторинг необходимо провести и для оставшихся четырех методов.

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