пятница, 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-го сезона сериала "Числа", где преступник отстреливал тех, кто пользовался услугами "рабынь любви". Приятного просмотра, мой дорогой читатель :)