среда, 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 при помощи регулярных выражений.

четверг, 21 апреля 2011 г.

Чем хороши StandUp-митинги

Натолкнулся на отличную статью, которая, базируясь на понятии пропускной способности коммуникации (communication bandwidth), кратко и емко объясняет, чем StandUp-митинги и другие формы face-to-face коммуникации лучше электронной почты, звонков, ежедневных письменных отчетов и т. д.

У нас в компании периодически проходили споры на этот счет и, думаю, фраза communication bandwidth поможет мне победить в следующем холиваре :)

Дубликация данных при выполнении JOIN-запросов

Давайте представим, что у нас есть две таблицы: Users (UserId, UserName, UserPhoto) и Articles (ArticleId, UserId, ArticleText). А теперь сделаем выборку пользователей со статьями при помощи Join:

SELECT UserId, UserName, UserPhoto, ArticleId, ArticleText
FROM Users as u INNER JOIN Articles as a ON u.UserId = a.UserId

Структура ответа на этот запрос будет следующей:

UserId1 UserName1 UserPhoto1 ArticleId1 ArticleText1
UserId1 UserName1 UserPhoto1 ArticleId2 ArticleText2


Обратите внимание на дублирующиеся UserName1 и UserPhoto1. А ведь в одном из этих полей может храниться большой blob. А теперь представьте, что будет при большем количестве JOIN'ов!

Меня удивляет, что упоминания этого нюанса нет в каждой серьезной статье про JOIN большими красными буквами. Более того, я ни разу не встречал такого упоминания. Скажете, что это очевидно? Как сказать... Это неочевидно как минимум при использовании ORM (особенно для тех программистов, кто не работал с голым SQL).

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

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


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

  2. Выполнение первого пункта в рамках хранимой процедуры с возвращением нескольких ResultSet'ов. Как и в первом варианте будет выполнено два запроса, но экономим один round trip.

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

  4. Реализовать запрос, который агрегирует выборку для каждого пользователя в один кортеж (например, в формате CSV). Преимущества и недостатки аналогичны пункту 3.


Если вдруг дубликация при JOIN-выборках станет узким местом в производительности вашего приложения, теперь вы знаете, что делать :)

суббота, 9 апреля 2011 г.

Google Analytics и Entity Framework

Заглянул в Google Analytics - оказывается, большинство читателей приходит в мой блог в поисках информации об Entity Framework. Неудивительно, ведь по запросу "Entity Framework" мой блог на первой странице в Google сразу за Википедией и MSDN.

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

Полицейские инновации

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

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

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

Вероятно, у вас зародилось сомнение, а не коррумпировано ли местное отделение полиции? Долой сомнения! Падение престижа полиции, вызванное тем, что проституция процветает чуть ли не напротив ее здания (буквально метрах в ста), однозначно компенсируется в валютном эквиваленте. Очевидность этих рассуждений сделала описанную далее встречу довольно неожиданной.

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

Для меня загадка, какую цель поставили перед этим патрулем. Выполнить квартальный план? Очистить улицу перед проверкой? При всей сомнительности данного мепроприятия, было бы невежливо с моей стороны не отметить высокую экологичность и инновационность (я почти уверен, что путь им прокладывал навигатор на базе Глонасс) данного подхода "борьбы" с проституцией.

Интересно, как происходила дискуссия между полицейскими и женщинами, попадавшимися им на пути.

- Гражданка, что вы делаете здесь в столь поздний час?
- * Артистично поднимает голову вверх * Лейтенант, да вы только посмотрите, какой отсюда вид на Малую Медведицу!

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

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

Разговоры о легализации идут очень давно, но дальше разговоров дело не идет. Мой прогноз следующий: пока у власти Единая Россия, закон о легализации проституции принят не будет.

На чем строятся рейтинги Единой России? "На махинациях, подтасовках и манипуляциях", - скажете вы и вас посадят будете правы. Но если говорить о реальном рейтинге (а чем он выше, тем проще рисовать 50% на каждых выборах), то формируют его возрастные, инертно мыслящие люди. И я сомневаюсь, что они смогут адекватно воспринять закон, который легализует проституцию. А зачем Единой России рисковать своими рейтингами?

Еще нельзя недооценивать влияние РПЦ. Церковь имеет тенденцию периодически забывать о том, что она отделена от государства, и ее возгласы в духе "не по-христиански это" вряд ли помогут продвижению этого закона.

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

На этой саркастической нотке я откланяюсь. Тем же, кто жаждет продолжения банкета, рекомендую посмотреть 13-й эпизод 6-го сезона сериала "Числа", где преступник отстреливал тех, кто пользовался услугами "рабынь любви". Приятного просмотра, мой дорогой читатель :)

пятница, 25 марта 2011 г.

C# и вывод generic-типов при создании объекта

C# умеет выводить типы при вызове generic-методов (например, Tuple.Create(5, 5)), однако при вызове конструктора типы нужно задавать явно (например, new Tuple<int, int>(5, 5))

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

Сегодня я решил разобраться, почему дела обстоят именно так: может быть, я зря негодую, и на текущую реализацию есть объективные причины. Оказалось, Eric Lippert (один из самых известных разработчиков C#; после Хейлсберга, конечно) уже отвечал на этот вопрос на stackoverflow. Вкратце, Эрик говорит, что нет никаких преград перед реализацией вывода типов для конструктора, более того, эта фича уже давно в ToDo-списке, но с очень низким приоритетом. Кроме того, он соглашается, что отсутствие вывода для конструкторов на фоне его наличия для методов порождает inconsistency, но при этом говорит, что цена порой оказывается важнее фичи.

Готов поспорить, что у команды C# самый высокий бюджет среди всех команд-разработчиков компиляторов. На фоне этого говорить о цене этой маленькой фичи как-то странно. Хотя тут вспоминается старый пост Эрика на тему того, что значит "реализовать фичу" согласно стандартам Microsoft. И все равно я разочарован.

суббота, 19 марта 2011 г.

Кроссфункциональная команда

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

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

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

Agile-методологии пропагандируют создание кроссфункциональных команд. И это, на мой взгляд, прекрасно (хотя и не дается даром). Однако интересно, что в agile к кроссфункциональности неназвязчиво примешивается взаимозаменямость. Здесь я позволю себе процитировать Хенрика *наше все* Книберга:

Cross-functional just means that the team as a whole has all skills needed to build the product, and that each team member is willing to do more than just their own thing.

Выходит, в контексте agile кроссфункциональной должна быть не только команда в целом, но и каждый ее член (до определенной степени). Причем, как мне кажется, кроссфункциональность/взаимозаменямость членов команды вытекает не только из кроссфункциональности команды, но и из других принципов agile вроде team committment и общего codebase. Хотя, очевидно, есть некоторая грань (редкий тестер сможет выполнять работу пиарщика), которая индивидуальна для каждой команды и от которой зависит вердикт по исходному кейсу.

Интереса ради, я решил узнать мнение о рассматриваемом кейсе на programmers.stachexchange.com. Ожидаемо, мнения разделились: от

Yes, it is normal for developers to become testers if it is required to get the work done for an iteration

до

I don't think developers being testers fits the cross-functional definition. That seems more like a jack-of-all-trades type scenario where developers also test, take sales calls, go out and buy coffee to restock the office, etc, which may be typical in a startup. A cross-functional scenario seems more like a situation where, for instance, you might be a developer, and I might be in marketing, but we're working together as a team to build and market the product.


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

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

пятница, 18 февраля 2011 г.

Сколько нужно времени, чтобы стать отличным программистом?

На programmers.stackexchange.com состоялась интересная дискуссия о том, сколько лет нужно на то, чтобы стать отличным программистом.

Первые три комментария радуют глаз:

x: А не все равно? Пока мне платят...
y: Но выдающимся программистам платят больше!
x: Но посредственные менеджеры начального уровня получают еще больше

Жизненно, да? Проблема не нова, и для ее решения некоторые IT-фирмы стараются делать равноправными две иерархии: технологическую и менеджерскую (Брукс в "Мифическом человеко-месяца" писал, что в IBM в 60-х годах это уже практиковалось). Мне не приходилось работать в фирмах, применяющих такой подход... видимо, поэтому вышеприведенная дискуссия меня так зацепила.

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

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

Ничто так не успокаивает,

как выполняющиеся один за другим зелененькие юнит-тесты. А зря

пятница, 28 января 2011 г.

26 января 2011 года

, а я читаю статью "Секреты VB6"

четверг, 20 января 2011 г.

Techcrunch как мотиватор-демотиватор

techcrunch.com оказывает на меня двоякое влияние. С одной стороны, читаешь статьи о Facebook, Google, Amazon, Ebay и ужасаешься, какой же ерундой по сравнению с ними ты занимаешься. С другой, эти статьи довольно сильно мотивируют на активную деятельность.

Уж не знаю, больше ли techcrunch.com меня мотивирует или демотивирует, но не читать его не могу: уж больно любопытно :)