Мой дядя самых честных правил программ исходники за так ...

15 мар. 2009 г.

Цикл как самый выразительный способ перебора элементов

С выходом C# 3.0 увеличилось количество способов перебрать элементы коллекции. Но всё же самым выразительным способом перебора элементов остался цикл.

Например, реализуем перебор всех свойств объекта и распечатаем те из них, у которых есть аттрибут DisplayNameAttribute.

using System;

using System.ComponentModel;

using System.Linq;

using System.Reflection;

 

namespace Home.Andir.Examples

{

    class Foo

    {

        [Category("Основные свойства")]

        [DisplayName("Свойство 1")]

        public string Property1 { get; set; }

 

        [Category("Основные свойства")]

        [DisplayName("Свойство 2")]

        public string Property2 { get; set; }

    }

 

    class Program

    {

        static void Main(string[] args)

        {

            Action<PropertyInfo, DisplayNameAttribute> PrintToConsole =

                (p, attr) => Console.WriteLine("\t{0} : {1}", p.Name, attr.DisplayName);

 

            Console.WriteLine("Foo properties (with foreach):");

            foreach (var p in typeof(Foo).GetProperties())

            {

                foreach (var attr in p.GetCustomAttributes(false))

                    if (attr is DisplayNameAttribute)

                        PrintToConsole(p, attr as DisplayNameAttribute);

            }

 

            Console.WriteLine("Foo properties (with foreach+linq 1):");

            foreach (var p in typeof(Foo).GetProperties())

            {

                var attributes = from attr in p.GetCustomAttributes(false)

                          where attr is DisplayNameAttribute

                          select attr as DisplayNameAttribute;

 

                attributes

                   .ToList()

                   .ForEach(attr => PrintToConsole(p, attr));

            }

 

            Console.WriteLine("Foo properties (with linq 1):");

            typeof(Foo).GetProperties()

                .ToList()

                .ForEach(p =>

                {

                    var attributes = from attr in p.GetCustomAttributes(false)

                              where attr is DisplayNameAttribute

                              select attr as DisplayNameAttribute;

 

                    attributes

                       .ToList()

                       .ForEach(attr => PrintToConsole(p, attr));

                });

 

            Console.WriteLine("Foo properties (with linq 2):");

            typeof(Foo).GetProperties()

                .ToList()

                .ForEach(p =>

                {

                    p.GetCustomAttributes(false)

                        .Where(attr => attr is DisplayNameAttribute)

                        .Cast<DisplayNameAttribute>()

                        .ToList()

                        .ForEach(attr => PrintToConsole(p, attr));

                });

 

            Console.ReadKey();

        }

    }

}

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

Не злоупотребляйте функциональным стилем в пользу читаемости! :-)

12 мар. 2009 г.

Упрощённая валидация аргументов в .Net

Для валидации аргументов методов в .Net присутствует набор исключений, которые бросаются при нарушении контракта метода:

  • ArgumentException – обобщённое исключение предназначенное для наложения произвольных ограничений на аргументы метода,
  • ArgumentNullException – исключение, указывающее что значение аргумента оказалось равным null,
  • ArgumentOutOfRangeException – исключение, указывающее что значение аргумента выходит за пределы интервала разрешённых значений.

Одним из параметров конструктора этого типа исключений является имя аргумента, для которого проводится валидация:

void Foo(string fooArg)

{

    if (fooArg == null)

        throw new ArgumentNullException("fooArg");

 

    Console.WriteLine("Foo({0})", fooArg);

}

 

void Bar(int barArg)

{

    if (barArg < 0 || barArg > 10)

        throw new ArgumentOutOfRangeException("barArg");

 

    Console.WriteLine("Bar({0})", barArg);

}

Собственно, это всё довольно известно и часто используется.

Однако в таком коде присутствуют и свои, довольно очевидные, проблемы.

  • Отсутствует декларативность (явно используемые проверки с помощью if),
  • Использование строковых констант в качестве имён аргументов (мешает использованию автоматического рефакторинга Rename).

Попробуем избавится от этих проблем с помощью типизированного варианта получения имени аргумента. Для этого воспользуемся возможностями Linq Expressions. У меня получился следующий вариант:

Program.cs

using System;

 

namespace Home.Andir.Examples

{

    class Program

    {

        static void Main(string[] args)

        {

            var p = new Program();

 

            p.Foo(null);

            p.Bar(-1);

        }

 

        void Foo(string fooArg)

        {

            Argument.NotNull(() => fooArg);

 

            Console.WriteLine("Foo({0})", fooArg);

        }

 

        void Bar(int barArg)

        {

            Argument.InRange(() => barArg,

                new Range<int> { Begin = 0, End = 10 });

 

            Console.WriteLine("Bar({0})", barArg);

        }

    }

}

В коде явно прибавилось декларативности и исчезли проблемы со строковым именем проверяемого аргумента. А итоговая функциональность кода совсем не изменилась. Отлично.

Посмотрим на реализацию класса Argument.

Argument.cs

using System;

using System.Linq.Expressions;

 

namespace Home.Andir.Examples

{

    public static class Argument

    {

        private static string GetArgumentName<T>(Expression<Func<T>> argumentExpression)

        {

            var memberExpression = argumentExpression.Body as MemberExpression;

            if (memberExpression == null)

                throw new ArgumentException(

                    "Only MemberExpression allowed for expression body.",

                    "argumentExpression");

 

            return memberExpression.Member.Name;

        }

 

        private static T GetArgumentValue<T>(Expression<Func<T>> argumentExpression)

        {

            var compiledExpr = argumentExpression.Compile();

 

            return compiledExpr.Invoke();

        }

 

        public static void NotNull<T>(

            Expression<Func<T>> argumentExpression)

        {

            var argName = GetArgumentName(argumentExpression);

            var argValue = GetArgumentValue(argumentExpression);

 

            if (argValue == null)

                throw new ArgumentNullException(argName);

        }

 

        public static void InRange<T>(

            Expression<Func<T>> argumentExpression,

            Range<T> range)

            where T : IComparable<T>

        {

            var argName = GetArgumentName(argumentExpression);

            var argValue = GetArgumentValue(argumentExpression);

 

            if (argValue.CompareTo(range.Begin) < 0

                || argValue.CompareTo(range.End) > 0)

                throw new ArgumentOutOfRangeException(argName);

        }

    }

}

И небольшой класс для задания интервала Range.

Range.cs

using System;

 

namespace Home.Andir.Examples

{

    public class Range<T> where T : IComparable<T>

    {

        public T Begin { get; set; }

        public T End { get; set; }

    }

}

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

Такой вариант, конечно, не является идеальным, но позволяет используя только средства языка C# 3.0 упростить и улучшить механизм валидации аргументов.

А более красивый и практически идеальный вариант можно получить с помощью AOP. Для моего примера можно предположить такую реализацию:

void Foo([NotNull] string fooArg)

{

    Console.WriteLine("Foo({0})", fooArg);

}

 

void Bar([InRange(0, 10)] int barArg)

{

    Console.WriteLine("Bar({0})", barArg);

}

Здесь условия на значения аргументов накладываются с помощью атрибутов. Максимально декларативно и без потери читабельности.

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

Похожий вариант валидации аргументов возможно будет организовывать и в будущей версии языка C# 4.0 (прочитать об этом можно здесь).

7 мар. 2009 г.

JavaScript: Никогда не переопределяйте Object.prototype!!

Вот уже в который раз наблюдаю использование в коде сторонней библиотеки попытки сделать всё красиво за счёт добавления функций в Object.prototype.

    Object.prototype.Foo = function()

    {

        // Что-то делаем здесь.

    }

На первый взгляд может показаться, что нет ничего страшного в таком коде. Но, стоит только вспомнить один из самых распространённых вариантов использования объектов в JavaScript: “Object as hash-tables” и:

    var myHashTable = { "key1" : "value1", "key2" : "value2" };

 

    for (var key in myHashTable)

    {

        // используется функция логирования от расширения Firebug (http://getfirebug.org/).

        console.log("key = '" + key + "', value = '" + myHashTable[key] + "';");

    }

 

    // Вывод:

    // key = 'key1', value = 'value1';

    // key = 'key2', value = 'value2';

    // key = 'Foo', value = 'function () { }'; // WTF???

Упс. В импровизированной хэш-табличке, внезапно, появился новый забавный элемент.

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

Чтобы обнаружить этот баг на ранней стадии, имеет смысл добавить в свои unit-тесты следующий тест:

    testObjectAsHashTableAreEmptyByDefault = function()

    {

        var obj = {};

 

        var keysCount = 0;

        for (var key in obj) { keysCount++; }

 

        assertEquals(keysCount, 0);

    }

Разработчикам же, библиотек для JavaScript нелишне будет запомнить, что Object.prototype – это тайна за семью печатями (sealed).

4 мар. 2009 г.

System.Web.Routing: Route, Defaults, Constraints, Data Tokens

Продолжаем обзор возможностей System.Web.Routing. Эта заметка будет очень небольшая в связи с тем, что функциональность, в принципе, вполне самоочевидная и простая.

Route

Как известно, основной реализацией RouteBase в библиотеке является класс Route, который реализует вариант маршрутизации с разделением адреса запроса на именованные сегменты. Суть этого всего на самом деле очень простая, но при этом внутри спрятана достаточно мощная семантика.

Примечание: Сегментами URL называются части, которые отделяются знаком “/”, соответственно сегмент не может содержать этого знака. В .Net все сегменты Url можно получить с помощью метода Uri.Segments.

Итак, рассмотрим произвольный до степени достоверности Url в типичном ASP.Net приложении подверженном операции реврайтинга (url rewriting), пусть это будет виртуальный блог неизвестного автора:

http://example.org/blog/2009/03/04/this-is-very-smart-subject-for-best-blog-post.aspx 

Разобьем этот адрес на самоочевидные логические сегменты, например так:

http://example.org/{virtual-path}/{year}/{month}/{day}/{subject}.aspx

и заметим, что собственно, всё что нам надо – это получить значения этих логических сегментов и передать гипотетическому хэндлеру ASP.Net, который в ответ сможет отрендерить HTML-код c текстом сообщения из блога под именем {blog} за указанное число {day}.{month}.{year} с указанной темой {subject}. 

В общем-то, после такого разбиения Url уже фактически готов к тому, чтобы стать некоторым маршрутом зарегистрированным в глобальной таблице маршрутов приложения:

using System;

using System.Web.Routing;

 

namespace Home.Andir.Examples

{

    public class Global : System.Web.HttpApplication

    {

        protected void Application_Start(object sender, EventArgs e)

        {

            RegisterRoutes(RouteTable.Routes);

 

            RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);

        }

 

        private void RegisterRoutes(RouteCollection routes)

        {

            routes.Add(

                new Route(

                    "{virtual-path}/{year}/{month}/{day}/{subject}.aspx",

                    new StopRoutingHandler()));

        }

    }

}

Ну вот, теперь в приложении есть ровно один зарегистрированный маршрут. Можно его протестировать.

Примечание: Для удобного тестирования маршрутов я использую RouteDebug.dll про которую я писал в недавней заметке. Плюс я дополнительно русифицировал результирующую страничку.

Скриншот: Тестирование виртуального маршрута для блога

На скриншоте хорошо видно, что маршрут успешно зарегистрирован, но введёному запросу “~/test” не удовлетворяет.

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

Теперь введём Url, который удовлетворяет нашему маршруту и пронаблюдаем как заполняется словарь RouteData при этом:

Скриншот: Тестирование подходящего виртуального маршрута для блога

Всё прошло хорошо.

Параметры

Как можно разглядеть на скриншотах и в параметрах класса Route, в каждом маршруте опционально могут присутствовать некие Defaults, Constraints и Data Tokens. Обсудим их подробнее.

Defaults – значения подстановочных переменных по умолчанию, позволяют задать опциональные сегменты Url, которые должны находится в конце текущего Url. Например, для такого маршрута:

routes.Add(

    new Route(

        "{street}/{building}/{flat}",

        new RouteValueDictionary { { "flat", "1" } },

        new StopRoutingHandler()));

Будут соответствовать такие запросы:

~/svetlanskaya/15/5/ – { { “street”, “svetlanskaya”}, { “building”, “15” }, { “flat”, “5” } }
 ~/aleutskaya/46/     - { { “street”, “aleutskaya”}, { “building”, “46” }, { “flat”, “1” } }

и при этом во втором случае, так как не задан сегмент для номера квартиры (flat), то будет использоваться значение по умолчанию.

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

Рассмотрим ограничения на первоначальном примере. Пусть нам необходимо ограничить значения для года, месяца и дня так, чтобы в них могли попадать более-менее реальные значения (только цифры, год больше 2000, месяц меньше 12, день меньше 31).

Модифицируем маршрут так:

routes.Add(

    new Route(

        "{virtual-path}/{year}/{month}/{day}/{subject}.aspx",

        null,

        new RouteValueDictionary {

            { "year", @"2\d{3}" },

            { "month", @"(0\d|11|12)" },

            { "day", @"(0\d|1\d|2\d|30|31)" }

        },

        new StopRoutingHandler()));

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

Проверка для маршрута показывает:

~/blog/2009/03/04/this-is-subject-for-blog-post.aspx - соответствует,
 ~/blog/2009/33/44/this-is-subject-for-blog-post.aspx - не соответствует,
 ~/blog/1999/03/04/this-is-subject-for-blog-post.aspx - не соответствует.

Data Tokens – набор дополнительных параметров для маршрута. Позволяет передать хендлеру, зарегистрированному для данного маршрута ещё какие-нибудь произвольные данные.

Интересное и показательное использование Data Tokens придумать у меня не получилось, поэтому оставляю пока здесь место для читательской фантазии ;-).

Catch-all сегменты

А ещё маршруты можно указывать так:

routes.Add(

    new Route(

        "route/{*all}",

        new StopRoutingHandler()));

Это случай так называемых catch-all сегментов. Такие сегменты могут появляться только последними в регистрируемом маршруте, но зато они при этом соответствуют произвольному окончанию данного маршрута.

~/route/test/ - соответствует, и при этом {"all", "test/"}
 ~/route/segment1/segment2/?id=1 - соответствует и при этом {"all", "segment1/segment2/"} 

Единственное, что может остановить сatch-all сегмент – это ограничения (Constraints), заданные на значение параметра.

Например:

routes.Add(

    new Route(

        "{*allaspxpages}",

        null,

        new RouteValueDictionary { { "allaspxpages", @".+\.aspx" } },

        new StopRoutingHandler()));

Такой маршрут будет срабатывать только для запросов к несуществующим aspx-страницам.

Для информации

Q: Что будет, если маршрут будет соответствовать некоторому существующему на диске файлу?

A: Текущий движок UrlRoutingModule запросы соответствующие физическому пути к существующему файлу по умолчанию не обрабатывает! В ответ на запрос будет возвращён соответствующий файл. Чтобы изменить это поведение, нужно смотреть в сторону RouteTable.RouteExistingFiles.

Q: А если это будет виртуальный путь, но закреплённый за каким-нибудь IHttpHandler ?

A: Приоритет окажется в руках у UrlRoutingModule, и если окажется зарегистрированным маршрут соотвествующий запросу к зарегистрированному IHttpHandler, то запрос до него не дойдёт. Поэтому надо явно прописывать игнорирование маршрутов до вашего хендлера с помощью метода IgnoreRoute.

3 мар. 2009 г.

Gallio – платформа для автоматизации юнит-тестов

Набрёл на проект посвященный созданию универсальной платформы для юнит-тестирования.

Основная идея состоит в том, что по сути многие фреймворки для юнит-тестирования (NUnit, xUnit, csUnit, MbUnit, MS Test и т.п.) очень похожи между собой, и можно попробовать выделить общую часть, касающуюся запуска и анализа результатов юнит-тестов, в отдельный легко расширяемый набор сервисов и инструментов.

Текущий статус проекта неясно описан, но в данный момент уже есть несколько релизов и даже пара инструментов, среди которых: красивый GUI-tool для запуска тестов Gallio.Icarus:

Скриншот: Запущенные тесты в Gallio.Icarus 

Интересно, что название проекта Gallio – это в действительности Gallileo написанное с двумя пропущенными буквами.