Страницы

четверг, 22 ноября 2012 г.

CardFlow. Приёмочный тест.

Любую разработку стоит начинать с приёмочного теста. Чем приёмочный тест отличается от модульных тестов? В первую очередь приёмочный тест – это тестирование всей функциональности системы с точки зрения конечного пользователя. Скажем, если вы нажали кнопку добавить в корзину – то необходимо проверить содержимое корзины на соответствие добавленному товару. Пользователь – это не обязательно человек. Если речь идёт про RESTful-сервис – то пользователь это другой сервис. Важная особенность – тестируют по принципу чёрного ящика, все действия с сервисом только через внешние интерфейсы или внешнее API. Хотя при этом надо использовать заглушки на внешние системы – лишняя недетерминированность ни к чему. Кроме того, заглушки могут эмулировать особые случаи поведения внешних систем (ошибки, разрывы связи и т.д).

Обычно приёмочные тесты пишутся на языке Gerkin. С этим же языком и работает SpecFlow, с помощью которого я и собираюсь писать тест. Для тех кто не знаком со SpecFlow, рекомендую посмотреть примеры. Кода немного, и можно быстро понять что к чему, даже не читая документацию.

Первый тест лучше писать для  минимальной реализации, постепенно докручивая в нём фишки. В моём случае это простейшее действие – создание новой Kanban-доски с указанными параметрами. У меня получилось так:

Как начать реализовывать систему – расскажу в следующий раз.

PS. Намучившись с форматированием кода решил окончательно переехать на gist’ы от github. Получается гораздо быстрее, и чище исходник блога.

воскресенье, 18 ноября 2012 г.

CardFlow. EventStore.

По жизненным обстоятельствам пришлось временно оставить профессию блоггера, но к счастью у меня оно снова есть. За это время утекло много воды, пришло много интересных идей (о которых вечно не хватает времени написать), я стал счастливым обладателем Windows 8 (честно счастливым, но об этом в другом раз). А еще пришло понимание, что свободного времени много не будет. Всё это приводит меня к неизбежности мысли уменьшить масштаб учебного проекта – оставить только создание и редактирование Kanban-досок.

В прошлый раз я остановился на поиске шины для приложения. Пришло время сделать еще один выбора – Event Store.  Но выбор был простым:

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

Что это значит? Для тестов жизненно необходимы три вещи Arrange, Act, Assert. Все шага должны выполняться быстро. Очень быстро. В вопросе хранения данных это означает возможность запустить хранилище в том же процессе. Совсем идеально хранить данные в памяти. Такой режим есть например в RavenDB. Да даже Microsoft озаботилась тестированием, и сотворила IIS Express для тестирования. В случае Грега Янга мы имеем только отличный сервер, что для быстрых тестов совершенно не годиться.

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

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

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

пятница, 28 сентября 2012 г.

Эффективное тестирование. Design for testability.

 

Перед тем, как вернутся к CardFlow хотелось бы сделать большое отступление на тему тестов.   Тесты – важнейший элемент развития кода. Быть может, даже более важный, чем важный рабочий код системы. Подушка безопасности, которая предохраняет программы от ошибок программистов. Как сохранить тесты полезными и рабочими? Я полагаю, здесь есть три важные ортогональные концепции:

  • Поддерживаемость. (Maintainability).
  • Детерминизм. (Determinism).
  • Скорость. (Speed)

  Изначально, я хотел сделать этот список нумерованным, и отсортировать по убыванию важности. Но не смог расставить приоритеты (поэтому расставил в порядке убывания количества букв). Каждая из этих концепций чрезвычайно важна, и пренебрежение любой из них делает тесты обузой. Будь я Мартином Фаулером, ввёл бы специальный акроним – MDS, а тесты написанные с соблюдением этих концепций – MDS Complaint .

  Как должен выглядеть тест, чтобы называться MDS-Complaint:

  1. 100% детерминирован (любите отлаживать обрыв ethernet-кабеля через в дебаггере? я тоже не люблю).
  2. Не существует другого теста на тестируемую функциональность. (DRY, DRY, DRY).
  3. Выполняется менее чем за 20 миллисекунд (1000 тестов менее чем за 20 секунд – мой идеал.).
  4. Зеленеет после рефакторинга кода, если функционал не был повреждён (скажи прощай mock’ам). 
  5. Содержит не более 10 строк кода в самом тесте, и не более 10 в setup-методе. (не люблю долго вникать в суть теста).

  Достичь MDS-совместимости тестов не так уже легко. К счастью, всё (хорошо, почти всё) уже украдено до нас. Вот мой краткий анализ болевых точек, на которые надо обращать внимание при написании тестов. 

Поддерживаемость.

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

  1. Организация тестов – в первую очередь, расположение. Если вы за 10 секунд не смогли найти нужный вам тест в проекте – что-то не так. Местоположение тестов должно быть интуитивно понятно, и подчинено неким соглашениям.
  2. Логика в тестах – очень страшное зло. Логика в тестах зачастую дублирует логику тестируемого кода, поэтому ошибки будут повторятся, и тест будет зелёным. Это страшно на самом деле. 
  3. Mocks – многие мне не поверят, но я абсолютно убеждён: в 95% случаев тесты с mock-объектами не тестируют ничего, кроме правильности установки ожиданий. Малейший рефакторинг приводит к необходимости вносить изменения в ожидания, причём зачастую в нескольких местах. Хотя система по-прежнему остаётся рабочей. Это чрезвычайно досадная печалька.
  4. Количество тестов. Как и любой другой код, тесты требуют поддержки, рефакторинга, создание дополнительных классов и т.д. Мысль проста – меньше тестов – меньше затрат на поддержку (и выше скорость выполнения).
  5. Имена тестов – Как и для методов, классов, интерфейсов и т.д. лучший комментарий для теста – его имя. Если вы не не можете написать хорошее имя для теста – скорее всего он тестирует слишком много всего сразу. Хорошенько подумайте, прежде чем его написать.

Детерминизм.

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

  1. Изоляция - Состояние системы, доставшееся после предыдущих тестов должно быть очищено, и новый тест работает с чистого листа. Всегда. Что это означает на практике? Возьмите перед тестом новый экземпляр объекта, полностью очистите базу данных, удалите файл и т.д. Исключение можно сделать лишь для объектов, состояние которых не влияет на тесты ( скажем, прочтённая из конфигурации строка подключения) ..
  2. Многопоточность. Асинхронные/многопоточные тесты – это всегда пляски с бубном в целях детерминизма. В таких тестах всегда приходится заниматься либо периодическим опросом, либо играться с примитивами синхронизации. И тот и другой способ сильно бьют по удобству поддержки и скорости выполнения тестов. Правильный вариант – тесты должны быть однопоточные, хотя иногда их сложно написать в таком ключе.  
  3. Удалённые сервисы. - Как минимум раз в неделю такой тест будет падать из-за проблем с сетью или самим удалённым сервисом. Исходя из своей практики, тесты с удалённым сервисом никогда не бывают более чем 90% детерминированы. Поэтому никогда не используйте удалённый сервис. Только заглушку, реализующую   интерфейса адаптера к удалённому сервису.
  4. Текущее время. – Настоящее реальное время в тестах ни к чему, поэтому под рукой всегда надо иметь хорошую реализацию фиктивных часов. Один из таких примеров я демонстрировал ранее.

Скорость.

  1. Многопоточность. Из-за склонности многопоточных тестов к периодическому опросу (polling), тесты часто содержат нечто вроде Thread.Sleep(1000). Целая секунда потерянного на сон потока! Неуж-то в это время нельзя заняться чем-то более полезным? Можно! Не пишите таких тестов. Чуть менее вредительский вариант – использование примитивов синхронизации, вроде ManualResetEvent, но в целом проблемы одинаковы – мы тупим в ожидании.
  2. Удалённые сервисы. Как и говорил ранее – межпроцессные вызовы выполнятся на 4-6 порядков медленнее внутрипроцессных. А хорошее тестирование содержит десятки-сотни вызовов удалённых сервисов. Если один вызов длиться 100мс, и тесты содержат 90 вызовов – то нам предстоит полторы минуты тупить в монитор.  Непозволительно долго и бессмысленно. Хорошая заглушка спасёт нас.
  3. Файловая система. Операции с файловой системой очень просты и понятны, писать для них заглушку не хочется. Но жесткие диски (даже SSD) способны изрядно притормозить процесс тестирования, особенно если в фоне на жёсткий диск записывается еще и фильм из интернета Улыбка Что делать если фильм мешает тестам? Правильно – нафиг такие тесты жесткие диски. Виртуальный диск спасёт и скорость, и фильм!
  4. Базы данных. База данных обычно тоже является удалённым сервисом, но отличается  весьма высокой детерминированностью. Чего нельзя сказать о скорости. Поэтому при выборе БД и технологии доступа к ней стоит присмотреться в возможности перевода БД во внутрипроцессный режим (SQLite, SQL Server Compact, RavenDB). А еще лучше и хранить данные в памяти, как это умеет SQLite. Кроме чистой скорости мы выиграем еще и возможность гонять тесты параллельно, без риска испортить тестам контекст.

Справделивости ради, я не единственный, кто проводил такой анализ. Фаулер тоже писал о проблемах тестирования. К сожалению, написал поздно – я набил все шишки самостоятельно. Зато еще раз убедился в правильности собственных выводов.

Design For Testablity.

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

четверг, 27 сентября 2012 г.

CardFlow. Как выбрать шину сервисов.

Сделать второй шаг к коду оказалось гораздо сложнее. Как и написано во всех agile-книжках, я собирался использовать подход test first. А первый тест подразумевает отправку сообщения в шину, и получение какого-то результата. Здесь поджидала самая большая трудность – выбрать шину. Для начала надо было определиться с  требованиями к идеальной шине:

  • Xcopy-развёртывание.
  • Возможность работы во внутрипроцессном режиме.
  • Поддержка контейнеров (забегая вперёд - их поддерживают все шины).
  • Простая конфигурация. Идеально – без конфигурации.
  • Не требующий администрирования транспорт.

А теперь посмотрим, как им соответствуют кандидаты. Выбор (к счастью) не так уж велик, и рассмотреть стоит всего трёх кандидатов:

  • NServiceBus – идеальный вариант для рабочих .net систем. Поддерживает все модные концепции сообщения, команды, события, саги, контейнеры и всё-всё-всё.   Документация в идеальном состоянии – хорошо структурирована, описывает все аспекты, всё по делу. Но есть пара ложек дёгтя. Во-первых, транспорт MSMQ – для рабочих система пойдёт, но настраивать MSDTC в учебных целях совсем не хочется.  Во-вторых – требует лицензии. Конечно можно добыть express-лицензию, но… это ведь дополнительные усилия. А усилия я экономлю. Улыбка
  • MassTransit – тоже близка к идеалу, но… в качестве транспорта может использовать только MSMQ или RabbitMQ. Про MSQM я уже писал, а RabbitMQ требует установки отдельной среды выполнения. Есть и специальный тестовый транспорт – Loopback, но на реале не попробуешь. Документация неплоха, хотя до NServiceBus не дотягивает. Если бы не транспорт – это мой выбор. Может быть свободное время, попробую написать транспорт на Rhino.Queues.
  • Rhino Service Bus – документация откровенно куцая, информации вообще немного, и ту надо собирать по крупицам. Для быстро понимания советую прочитать пару статей Oren Eine, а потом почитать wiki на github’e про изменения в версии 2.0. В качестве транспорта MSMQ и Rhino.Queues. Последняя не требует никаких административных действий, и сразу готова к работе. И это решающий плюс. Ах да, есть один минус в карму – использует противный log4net, но это я могу пережить.

MassTransit отвергнут. В учебных целях не хочется возиться с администрированием. Если бы у меня была настоящая рабочая система – я бы разорился на NServiceBus. А пока снова возьмусь за Rhino.ServiceBus.

четверг, 6 сентября 2012 г.

CardFlow. Первый шаг к коду.

  Каждый .NET разработчик знает правило формирования имен – “Компания.Проект.Сборка”. Чаще всего это название идёт напрямую в имя сборки. Получается нечто подобное:

image

Два проекта – это еще ничего. Но когда их число подходит к 10 искать в этом нагромождении букв нужный проект уже сложно и долго. Инструменты вроде Resharper’а конечно спасают ситуацию быстрыми клавишами навигации. Но всё равно, периодически надо ковырять свойство конкретного проекта приходиться искать его глазами. Это раздражает. К счастью, бесполезное сочетание  “Компания.Проект” можно смело выкинуть на помойку:

image

Однако имена сборок всё равно надо сделать соответствующими шаблону “Компания.Проект.Сборка”. Проще пареной репы, просто пропишите их в свойствах проекта:

image

Есть и еще одна полезная мелочь. По-умолчанию, студия задаёт выходные папки (bin) проектов в подпапке каждого проекта. В результате, в процессе сборки проекта мегабайты сборок могут копироваться туда-сюда, сильно замедляя процесс. Кроме того, напрасно тратиться дисковое пространство. Это звучит глупо по современным меркам HDD, но я использую виртуальный диск, поэтому проблема свободного места для меня по прежнему актуальна. Замените Output path по-умолчанию на “..\bin” в Release и Debug конфигурации проектов, и жизнь станет немного легче:

image

Мелочи, а способны экономить время и нервы (а они в современной жизни на вес золота). Подмигивающая рожица

пятница, 31 августа 2012 г.

CardFlow. Гранит науки.

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

  • CQRS/ES, в том числе хочется побаловаться с Event Store
  • Полная асинхронность. Интересно с точки зрения более полного (и скромного!) использование машинных ресурсов. Да и ES с таким подходом хорошо дружит.
  • Для связи использовать какую-либо MQ/ESB (разницу между MQ и ESB хорошо расписал Udi Dahan) Скорее всего это будет Rhino ESB. Бесплатна, и готова к работе. Документация конечно хромает, но необходимое по интернету насобирать можно.
  • Проектирование с учётом тестирования. Каждый этап. В идеале – возможность запускать приёмочные тесты как в полностью синхронном внутрипроцессном режиме, так и в полностью рабочей среде.
  • Работа с удалёнными сервисами. Интересно в разрезе написания тестов, и реальной работы. Какой именно удалённый сервис будет пока не придумал, но думаю в пути мысль возникнет.
  • XCOPY-развёртывание, привет TopShelf.
  • BDD в общем, и SpecFlow в частности.
  • Управление зависимостями с помощью Nuget. Не идеален, но лучше, нежели таскать сборки руками.

Пока хватит. В следующий раз приступим к реализации. Гибкие методологии рекомендуют начинать с приёмочного теста. Он и станет первой строчкой кода в проекте.

четверг, 30 августа 2012 г.

CardFlow.

Почитывая книгу Leading Lean Software Development, увидел пример Kanban Board. Идея сразу запала в душу, и спустя пару дней навязала желание сделать учебный проект именно Kanban Board. Писать, что такое Kanban, и причем тут доска не буду. Лучше почитайте книгу выше, или другую специализированную литературу. Но одно стоит заметить: Kanban – переводится как карточка, и методология крутиться вокруг движения карточек. Что логично приводит к подходящему названию – CardFlow.

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

  1. Редактирование доски: количество, расположение слотов, переходы между ними.
  2. Создание и передвижение карточек по доске.

Для полноценного менеджера задач конечно недостаточно: нет участников, нет авторизации, много чего нет… Но хватит, чтобы побаловаться с CQRS и Event Sourcing Подмигивающая рожица

Я постараюсь описать каждый шаг разработки, и почему он сделан именно так.

воскресенье, 29 июля 2012 г.

Положи Windows Service на полку.

  Кому приходилось писать сервисы windows следуя заветам Microsoft осознаёт тяжесть (“корпоративность”) подхода. Долго, муторно, неудобно для тестирования, для отладки требуется ultimate версия студии. Возникает ощущение бега с препятствиями. Видимо схожие мысли посетили и авторов Mass Transit и Fubu Project. Они уже решили проблему создания сервисов, убрав все препятствия с трассы. Спешите освоить -  Topshelf.  

  Раздел документации на сайте не блещет полнотой, что поначалу несколько расстраивает. Хорошая новость: ее достаточно. Topshelf может работать в двух режимах:

  • installing - создание полноценной службы windows.
  • shelving – хостинг сервисов в отдельном домене приложения.

 

Installing

  Этот режим используется для создания полноценной службы windows, которой можно управлять через Service Manager. Идеологически Topshelf выполняет две операции:

  • Берёт на себя сложности связанные инфраструктурными проблемами хостинга windows-сервиса.
  • Хостинг бизнес-сервиса, выполняющего работу самого сервиса.

Эти две функции нам и надо прописать в коде:

static class Program
{
    static void Main()
    {
        HostFactory.Run(x =>
        {
            //Настройка windows-сервиса. 
            x.UseNLog();
            x.BeforeStartingServices(() => Console.WriteLine("[FooHost] Preparing to start host services"));
            x.AfterStartingServices(() => Console.WriteLine("[FooHost] All services have been started"));
            x.AfterStoppingServices(() => Console.WriteLine("[FooHost] All services have been stopped"));
            x.SetServiceName("FooHost");
            x.SetDisplayName("FooHost");
            x.SetDescription("FooHost ");
            //Сейчас запускаем как Local System, но можно запустить под любым пользователем. 
            x.RunAsLocalSystem();
            x.EnableDashboard();
            //Настройка нашего бизнес-сервиса.  
            x.Service<BarService>(y =>
            {
                y.SetServiceName("FooService");
                y.ConstructUsing(() => new FooService());
                y.WhenStarted(foo => foo.Start());
                y.WhenStopped(foo => foo.Stop());
            });
        });
        Logger.Shutdown();
    }
}

Не буду подробно останавливаться на коде, он говорит сам за себя. Добавлю: Topshelf поддерживает работу с двумя наиболее популярными логгерами – log4net и NLog2. Расширенное описание командной строки есть в исходнихах.


Преимущество режима  по сравнению с обычным сервисом:



  • Нет нужды наследовать и вручную писать ServiceInstaller и ServiceBase, что повышает тестопригодность.
  • Возможность запускать сервис в режиме консоли “из коробки”, что облегчает его отладку.
  • Внятное (даже без документации) API.

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


Shelving


В большинстве случаев нам не нужен полноценный windows-сервис, но мы вынуждены его писать, чтобы удовлетворить требования Windows. Отсюда вытекает логичная мысль: сделать инфраструктурный windows-сервис, который бы мог хостить бизнес-сервисы. Эту идею и реализует Topshelf.


Для начала нам понадобиться Topshelf.Host.exe. Его нет в nuget, но можно найти в исходниках проекта (я использую несколько модифицированную версию). Topshelf.Host.exe является таким же windows-сервисом, как и описанный в предыдущем разделе, и следовательно его можно запускать и как сервис, и как консоль. После запуска сервис автоматически создать подпапку Services. Topshelf мониторит эту папку, и при обнаружении изменений по необходимости подгружает


  Для дальнейшей работы необходимо реализовать Bootstrapper для своего сервиса, и сконфигурировать параметры запуска. У меня это выглядит так:

public class MazayBootstrapper : Bootstrapper<MazayService>
{
    public void InitializeHostedService(IServiceConfigurator<MazayService> cfg)
    {
        cfg.SetServiceName("Mazay");
        cfg.Named("Mazay");
        cfg.HowToBuildService(x => new MazayService(SectionHandler.Instance.Folders));
        cfg.SetServiceName("Mazay");
        cfg.WhenStarted(x => x.Start());
        cfg.WhenStopped(x => x.Stop());
    }
}

  После этого надо создать подпапку с названием сервиса в папке Service\FooService, и скопировать в нее наш сервис. Согласно описанию, сервис должен был заработать. Но заработал  только после продолжительных мытарств. Что еще раз напомнило мне о древней мудрости:  если ничего не помогает – прочитайте наконец документацию. Если и это не помогает – внимательно прочитайте документацию Улыбка Дело оказалось в двух проблемах:




  1. Добавить конфигурационный файл сервиса. Внимание: название файла должно соответствовать шаблону ‘имя_папки_сервиса.dll’, в которую мы положили сервис. Для моего случая это FooService.config, а не FooService.dll.config. Topshelf запускает сервис в отдельном домене приложения, и принудительно назначает ему этот конфигурационный файл.


  2. В конфигурационном файле необходимо указать ссылку на Bootstrapper сервиса:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section
      name="ShelfConfiguration"
      type="Topshelf.Shelving.ShelfConfiguration, TopShelf" />
  </configSections>
 
 <ShelfConfiguration
    Bootstrapper="Mazay.MazayBootstrapper, Mazay" />
</configuration>

После этого сервис заработал в штатном режиме.


Dashboard


Маленький бонус от создателей. Topshelf содержит небольшой проект Dashboard, который позволяет из браузера посмотреть, и управлять рабой сервисов. Для его подключения понадобиться nuget пакет Topshelf.DashBoard, и вписать строчку и вызвать метода расширения x.EnableDashboad() при конфигурации сервиса. Dashboard не поддаётся конфигурации, и найти его можно по адресу http://localhost:8483/TopShelf/Topshelf.Host


image


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

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

На пути к веб-разработке.

Жажда, жажда…

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

  Долгое время мне приходилось писать WinForms приложения, лопатить тонны кода хранимых процедур и заниматься разными побочными языками вроде x++. Каждый раз наступал момент, когда работа исчерпывала себя, и душа требовала свежего ветра. Сейчас пишу сервисы на C#, а жажда свежего ветра потихоньку начинает брать своё. В .NET мире осталась одна нехоженая мню дорога – Веб. Весьма востребован в наши дни. Это не попытка открыть Америку, а предисловие к тому факту, что долгое время я пытался ее обплыть стороной. Пришло время и мне стать Колумбом.

Возможно, самый удачный вариант – начать изучение протокола HTTP. Но логика прагматика подсказывает мне, что лучше остановится на одном из доступных фреймворков. Сделать выбор непросто. После официальной работы, и домашних забот остаётся не так уж много времени для работы над собой. Тратить нужно осторожно, на всё не хватит. Я вижу несколько интересных игроков на .net web-арене.

Кто следующий?

 ASP.NET MVCx – самый монструозный и самый популярный фреймворк, обязанный своей популярностью маркетингу (и умению копировать) Microsoft. Сотни вакансий  по сравнению с абсолютным  нулём для всех остальных конкурентов вместе взятых. Казалось бы идеальный выбор, но… Во-первых, Microsoft в очередной раз не изобрела ничего нового, а лишь ответственно подошла к копированию и модернизации Castle Monorail (на тот момент возможно лучший фреймворк для веб). Увы, вместе с достоинствами прихватили и недостатки: необходимость наследовать инфраструктурные классы контроллеров, и возвращаемые мутные ActionResult. Во-вторых, тяготы работы с “корпоративным” фреймворком – скачай то, доставь это, настрой сё и т.д. и т.п. Почему я не могу просто написать Install-Package, и насладиться красотой Hello World по asp.net-овски?  Увы, из-за популярности на рынке мне придётся им заняться, хотя совсем-совсем не хочется. Этот как рыбий жир: противно, но полезно.

 FubuMVC – собственно, не только веб-фреймворк, но и несколько сопутствующих проектов. Вкратце, своё отношение могу высказать фразой “я Пастернака не читал, но одобряю”. Фреймворк построен не тех принципах, которые мне очень близки: Convention Over Configuration, POCO и многое-многое другое, что позволяет освободиться от инфраструктурной тягомутины, и писать собственно приложение. Chad Myers хорошо написал, почему я (заочно) люблю FubuMVC. Не только Chad Myers, а вообще весь состав разработчиков внушает мне полное доверие. Будучи давним читателем CodeBetter, полностью разделяю их подходы, и философию программирования. Fubu точно займёт своё место в моём учебном плане.

 OpenRasta – темная лошадка в моём списке. Не подумайте, он неплохо документирован, и уже достаточно давно обживает белый свет. Но есть есть нюансы. Почти весь фреймворк написал Sebastian Lambla. Себ без сомнений круче Чака Норриса, но всё-таки не Брюс Всемогущий. Не успевает наводить марафет, что частенько вводит в расстройство даже опытных бойцов. Кроме того, у Себа свой особый взгляд на версионирование и управление зависимостями. Они не плохи, просто левая резьба: свои преимущества есть, но  пока не готов бросится вплавь против течения.

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

Легко в учении.

Учить документацию смысла нет – нужна практика. Вот целях обучения напишу скромное двух-трёхзвенное приложение, веб-интерфейс для которого попробую реализовать на заслуживающих внимания фреймворках. Бек-енд тоже не обойдётся без освоения нового – модные ныне CQRS и Event Sourcing уже давно тешат воображение. Пора оседлать их в реале. О своих успехах постараюсь рассказать на страницах блога.

понедельник, 7 мая 2012 г.

AssemblyVersion и git: маленькое дополнение.

  Я уже описывал вариант работы с git’ом для генерации файла SolutionVersion.cs. Этот файл добавляется в .gitignore, чтобы не коммитить его в контроль версий. А в проекты VIsual Studion добавлен BeforeBuild шаг генерации заглушки файла перед компиляцией, дабы работать  с проектом без дополнительных телодвижений. Основная идея – контроль версий не должен содержать сгенерированную AssemblyVersion и прочие атрибуты сборки, сохраняя удобство работы с кодом проекта.

  Сегодня у меня возникла еще одна  идея, как можно упростить процесс. Всё аналогично прошлой статье, за исключением пары других действий. Во-первых, сознательно кладём в контроль версий пустой файл SolutionVersion.cs. Во-вторых, в локальном репозитории выполняем команду:

git update-index –assume-unchanged SolutionVersion.cs

  Эта команда заставит git считать файл локально неизменённым, даже если в нём сделаны изменения. Случайно отправить в контроль версий непустой SolutionVersion.cs не получится. Эта команда влияет только на git-репозиторий, в котором выполнена. Если клонируем репозиторий заново – то и команду придётся выполнить еще раз.

  Какой из двух способов выбрать? Первый способ более надёжен, но требует больше манипуляций с файлами проектов, и вызывает большее количество обращений к диску при компиляции. Во втором случае проще начальная настройка, меньше обращений к диску, но есть риск всё-таки положить в контроль версий сгенерированный файл. Впрочем, с git’ом это не такая большая проблема, т.к. мы еще можем поработать над историей. Улыбка

  Лично я с текущего момент предпочитаю второй способ. Время компиляции (а оно вносит задержку перед запуском тестов) для меня важнее, а в случае проблем еще можно всё исправить.

четверг, 3 мая 2012 г.

Рефакторинг “выделить параметр” весёлым путём.

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

public Person(string firstName, string secondName, string lastName)
{
    _firstName = firstName;
    _secondName = secondName;
    _lastName = lastName;
}

  Выделить параметр не так сложно, он напрашивается сам по себе. Но реализация повеселила:

public PersonName(string firstName, string secondName, string lastName)
{
    _firstName = firstName;
    _secondName = secondName;
    _lastName = lastName;
}
public Person(PersonName personName)
{
    //WTF???
    _firstName = personName.FirstName;
    _secondName = personName.SecondName;
    _lastName = personName.LastName;
}

  О да! Таскать строки по одной так весело и интересно, что лучше делать это дважды! Сначала затолкав их в PersonName и затем скопировав в Person. Бессмысленная и беспощадная трата человеко-часов. Правильная и очевидная реализация:

        public Person(PersonName personName)
        {
            _personName = personName;
        }

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

вторник, 1 мая 2012 г.

NHibernate Future – оптимизация сетевых издержек.

  Куда уходит время.

  Думаю многие знакомы с проблемами распределённых вычислений. Межпроцессные вызовы выполняются на 3-4 порядка медленнее внутрипроцессных. Межмашинные вызовы длятся на 5-6 порядков медленнее внутрипроцессных. Сетевые  издержки накладываются на каждый межмашинный вызов, приводят к бесполезным потерям времени ожидающими потоками. Напрасный расход машинных ресурсов. Эта проблема была изначальным стимулом к созданию DTO-объектов – вместо многих вызовов, запрашивающих по чуть-чуть, запросить сразу все необходимые данные.

  Эта проблема знакома и создателям NHibernate. NH старается как можно реже обращаться к базе данных, откладывая запросы на последний момент,  выполняет их в пакетном режиме по возможности. Впрочем, в некоторых случаях мы всё равно вынуждены брать контроль в свои руки.  Один из таких случаев – управление временем выполнения запроса (моментом, когда запрос необходимо отправить в базу данных). По понятным причинам, создать DTO-объект для работы с базой данных невозможно. Но и не нужно – NHibernate уже предоставляет хорошее API для этих целей.

  Запроси завтра то, что можно запросить сегодня.

  Цель – уменьшить количество вызовов  к базе данных. Для этого в NHibernate есть специальное API – Futures. В каждом API (IQuery, ISQLQuery, ICriteria, IQueryOver) есть два специальных метода Future и FutureValue:

var count = session.CreateQuery("select count(*) from Cat c").FutureValue<long>();
var peter = session.CreateQuery("from Cat c where c.Name = 'Peter'").Future<Cat>();

  Оба метода на самом деле не выполняют запрос, а лишь добавляют его к списку запросов, которые будут выполнены  потом. “Потом” наступает в момент перечисления коллекции (Future) или получения единственное значение (FutureValue). Все запросы будут отправлены в базу данных пачкой за один вызов.


  Несмотря на явные преимущества, в ответ на предложение использовать Future,  я слышал в ответ нечто вроде “А зачем? Не так уж дорого сделать еще один запрос в базу”. Насколько “дёшево” сходить еще раз в базу я и решил проверить численно. На этот раз мне было лень выдумывать “примеры из жизни”, поэтому будет тренироваться на абстрактных кошках, и сравнивать время выполнения двух методов:

private void QueryDirectly(string timeRegion)
{
    using (var session = _factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        using (StopwatchManager.Start(timeRegion, elapsed => Console.WriteLine("{0} total time: {1}", timeRegion, elapsed.Total)))
        {
            var count = session.CreateQuery("select count(*) from Cat c").UniqueResult<long>();
            var peter = session.CreateQuery("from Cat c where c.Name = 'Peter'").List<Cat>();
            var bazil = session.CreateQuery("from Cat c where c.Name = 'Bazil'").UniqueResult<Cat>();
        }
        tx.Commit();
    }
}
private void QueryUsingFuture(string timeRegion)
{
    using (var session = _factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        using (StopwatchManager.Start(timeRegion, elapsed => Console.WriteLine("{0} total time: {1}", timeRegion,  elapsed.Total)))
        {
            var count = session.CreateQuery("select count(*) from Cat c").FutureValue<long>();
            var peter = session.CreateQuery("from Cat c where c.Name = 'Peter'").Future<Cat>();
            var bazil = session.CreateQuery("from Cat c where c.Name = 'Bazil'").FutureValue<Cat>();
            var value = count.Value;
        }
        tx.Commit();
    }
}

  Перед запуском на время, каждый метод прогоняется отдельно, чтобы исключить влияние JIT-компилятора. После этого оба метода выполняются в цикле по 1000 раз, и c помощью Stopwatch замерялось время выполнения только самих запросов. Итак, результаты:


image


  Как видно, время растёт линейно, с каждым новым запросом. Но при использовании Future увеличиваются только издержки на выполнение запроса базой данных, в то время как прямы запрос начинают существенно отставать. Стоит отметить, что эти результаты получены в ситуации, когда и сервер и клиент находились на одной машине, и сетевые задержки малы. В реальной ситуации выигрыш от использования Future может быть намного больше.


  Стоит отметить, что и в более ранних версиях существовал функционал для группировки запросов – IMultiCriteria и IMultiQuery. Но Future предоставляет более удобный и типобезопасный способ получения результатов.


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

воскресенье, 29 апреля 2012 г.

Не используйте Fluent Migrator для интеграционных тестов.

  Библиотеки  миграций баз данных вроде Fluent Migrator и Migrator.Net прочно заняли место среди must have инструментов .net-разработчика. Простота использования подкупает настолько, что появляется соблазн использовать миграции и для создания полноценной тестовой базы данных. Но я не рекомендую так делать по нескольким причинам.

Проблемы.

  Как вы знаете, чтобы быть успешными тесты должны быть быстры. Молниеносно быстры. А прогон миграций на базе данных требует время. Трансформировать миграции в скрипты БД, выполнить их за N-обращений к базе данных – всё это включает в себя затраты самой базы данных, и временные пени за обращение к удалённому сервису. Понятно, что с ростом числа миграций ситуация будет становится всё хуже и хуже.

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

Решение.

  Нет необходимости прогонять миграции в каждом тесте. Мы тестируем не миграции, а некий другой функционал, связанный с базой данных. Всё, что необходимо для тестов – это наличии актуальной схемы базы данных. Если бы мы были счастливчиками, и работали с ruby on rails – мы могли использовать функционал schema dumping встроенный в библиотеку миграций. Увы, для fluent-migrator’а проблему придётся решать собственноручно: написать некий schema dumper, или кодогенератор на основе модели предметной области. Впрочем, если вы работаете с NHibernate – то можно воспользоваться встроенным классом SchemaExport для создания пустой базы для тестов. Аналогичные возможности есть и у Entity Framework’а и некоторых других средств доступа к данным.

  Последний момент – сами миграции тестировать так же необходимо. Я бы писал тест на каждый спринт (если у вас scrum) или любой другой отрезок времени. В таком тесте прогоняются  миграции от начала до конца с тестовым профилем.Тестовый профиль добавляет в тесты некоторое количество показательных данных в разные моменты миграций, проверяя их состояние после миграции.

  На мой взгляд, такой подход наиболее оптимален.

NHibernate transactions – обязательны при всех операциях с базой данных.

  Соблазн не использовать транзакции всегда велик. Вроде бы и ресурсов они расходуют, и риск взаимоблокировок всегда велик. По этим причинам многие отказываются от транзакций. Но не стоит следовать этой практике. И сейчас я расскажу как минимум о трёх причинах, по которым транзакции следует использовать при любом обращении к базе данных через NHibernate.

Почему надо использовать транзакции.

  1. РСУБД транзакционны по своей природе. Даже если вы не создаёте транзакцию явно, СУБД сделает это сама. При этом  транзакцию создаётся на каждый оператор (statement), и риск взаимоблокировок сохраняется. Создание транзакции не бесплатный процесс, и требует ресурсов. Отсюда вытекает простая логика: меньше транзакций – меньше расходуется ресурсов сервера.

  2. Согласованность данных. Думаю, никому не надо объяснять – это основа основ работы с БД. Без использования транзакций в БД может попасть всякий мусор, влияние которого на поведение системы порой бывает трудно предсказать. Чтобы особенно болезненно если ссылочная целостность проверяется только на уровне приложения (например связи many-to-any в NHibernate). Всегда проще откатить транзакцию, нежели разбираться с битыми данными.

  3. Кеш второго уровня. NHibernate за кадром делает много работы, чтобы поддерживать кэш в актуальном состоянии. Отсутствие транзакций в коде заставляет NHibernate игнорировать кэш второго уровня, и всегда делать запрос в базу. Причина в неизвестности результата DML-операций: при обновлении базы без транзакции в случае ошибки база оказывается в недетерминированном состоянии. Поэтому NHibernate вынужден инвалидировать данные в кэше, и направлять запрос в базу. В этом легко убедиться. Посмотрим код:

[Test]
public void Query_cache_invalidated()
{
    using (var session = factory.OpenSession())
    //using (var tx = session.BeginTransaction())
    {
        session.SaveOrUpdate(new Cat {Name = "Bazil"});
        session.Flush();
        //  tx.Commit();
    }
    using (var session = factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var reloadedPeter = session.Get<Cat>(50L);
        tx.Commit();
    }
    Console.WriteLine("HitCount: {0}", factory.Statistics.SecondLevelCacheHitCount);
}   

И вот что мы увидим в логах:


image


  Но стоит только снять комментарии с кода транзакции, и картина сразу меняется в лучшую сторону:


image


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


  Полагаю, мне удалось убедить вас в необходимости использования транзакций. А теперь посмотрим на еще одну вещь, о которой стоит знать при использовании NHibernate с транзакциями.


Некоторые последствия.


  При использовании транзакций NHibernate старается обеспечивать согласованность данных. Давай рассмотри такой код:

[Test]
public void Sudden_insert_happened_on_read_operation2()
{
    using(var session = _sessionManager.OpenSession())
    using(var tx = session.BeginTransaction())
    {
        session.SaveOrUpdate(new Cat {Name = "Bazil"});
        tx.Commit();
    }
    using (var session = _sessionManager.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var peter = new Cat { Name = "Peter" };
        session.Persist(peter);
        var reloadedPeter = session.Get<Cat>(50L);
        reloadedPeter.Name = "Johnny";
        var totalCats = session
            .QueryOver<Cat>()
            .Where(x => x.Name != string.Empty)
            .RowCount();
    }
}

  В одной сессии создали сущность, и сохранили ее в базу, в другой добавили в сессию одну сущность, и изменили одно из свойств другой. Затем выполнили запрос. А теперь посмотрим, что же было в логах NHibernate?


image


    Я не делал ни Session.Flush(), ни tx.Commit(), но Nhibernate выполнил Insert и Update. Причина в том, что NHibernate понимает, что сделанные в сессии изменения могут повлиять на результат запроса. Поэтому предварительно приводит БД в согласованное с сессией состояние. Если убрать транзакцию, то предварительного обновления происходить не будет (всё равно БД будет в недетерминированном состоянии).


  Если еще не используете транзакции для работы с NHibernate – то самое время начать. Подмигивающая рожица

суббота, 21 апреля 2012 г.

Решаем проблему с SQLite x86 / x64.

  Уже давно использую SQLite совместно с NHibernate для быстрого тестирования приложений, и так же давно мучает проблема совместимости x86/x64. Открывая проект на старой машине приходилось нудно переустанавливать подходящую сборку SQLite. В простом варианте это можно принудительно выставить во всех проектах platform target = x86, но мне такой вариант принципиально не нравился. В общем, сегодня решил эту проблему в относительно приличном виде, используя nuget и немного магии msbuild. Итак, решение по шагам.

1. Используя nuget добавляем в проект ссылку на System.Data.SQLite.MSIL. Эта сборка понадобиться только на этапе компиляции. Лично я предпочитаю делать это через Package Manager Console:

install-package System.Data.SQLite.MSIL

2. Открыть в проекте файл packages.config, и дописать в него пакеты System.Data.SQLite.x64 и System.Data.SQLite.x64. Добавлять через nuget не стоит, т.к. при этом nuget заменит ссылку на msil-сборку ссылкой на платформо-специфичную сборку:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="System.Data.SQLite.MSIL" version="1.0.80.0" />
  <!--Добавить вручную-->
  <package id="System.Data.SQLite.x64" version="1.0.80.0" />
  <package id="System.Data.SQLite.x86" version="1.0.80.0" />
</packages>

3. Настроить восстановление сборок в проекте из контекстного меню файла проекта:


image


4.  Теперь пора немного пошаманить в файле проекта. Для начала из контекстного меню проекта выбираем меню Unload Project:


image


а затем открываем его на редактирование:


image


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

<PropertyGroup>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == 'AMD64'">..\packages\System.Data.SQLite.X64.1.0.80.0\lib\net40\</SQLitePath>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == 'X86'">..\packages\System.Data.SQLite.X86.1.0.80.0\lib\net40\</SQLitePath>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == '' AND $(PROCESSOR_ARCHITECTURE) == 'AMD64'">..\packages\System.Data.SQLite.X64.1.0.80.0\lib\net40\</SQLitePath>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == '' AND $(PROCESSOR_ARCHITECTURE) == 'X86'">..\packages\System.Data.SQLite.X86.1.0.80.0\lib\net40\</SQLitePath>
</PropertyGroup>
<Target Name="AfterBuild">
  <Exec Command="xcopy /s /y &quot;$(SQLitePath)*.*&quot; &quot;$(TargetDir)&quot;"/>
</Target>

6. Снова загрузить проект, и запустить build. После этого всё должно работать.


  В завершении хотелось бы сказать несколько замечаний. Все эти сложности нужны только если бы компилирует проект под AnyCPU, под конкретную платформу это всё не надо. Плохо, что путь к сборкам жёстко зашит. Его можно было бы выдернуть из ссылок, но на текущей момент мне не хотелось этим заниматься (программировать на xml всегда неудобно).


  Удачной разработки с SQLite. Улыбка