Показаны сообщения с ярлыком Programming. Показать все сообщения
Показаны сообщения с ярлыком Programming. Показать все сообщения

пятница, августа 28, 2009

WTF

Скажите, что должен чувствовать программист, когда вот для примерно такого кода:


float speed = Float.NaN;
if (speed == Float.NaN)
Dialog.alert("Not a number");
else
Dialog.alert("Shit happens...");



он видит на экране "Shit happens..."?
Ага, такое дерьмо приключилось вчера со мной, когда я ковырялся в реализации J2ME от одного вполне уважаемого вендора. Убив на проблему около часа, я совсем было отчаялся, решил что я уже слишком стар для таких приколов, и пора мне завязывать с программированием. Однако, неожиданно, замена (speed == Float.NaN) на (Float.isNaN(speed)) решила проблему. Дерьмо исчезло.
Но запах остался...
Продолжаю программировать.

среда, февраля 25, 2009

Модульные тесты для SilverLight

SilverUnit (другое название CThru) - движок для модульного тестирования SilverLight.
Интересно, что
- тесты могут исполняться в обычном NUnit или MS Test tools без специальной Silverlight-компиляции
- тесты исполняются в обычном CLR runtime, не в silverlight.

В общем полная изоляция. А все потому, что SilverUnit построен на базе Typemock Open-AOP API.

понедельник, января 05, 2009

Listma - .Net Workflow framework

Что вы делаете, когда вся логика разрабатываемого класса крутится вокруг его состояния? К примеру, разрабатываем мы web магазин. Есть у нас сущность Order (заказ), ее создает «покупатель», затем он ставится в очередь на обработку, затем «оператор» формирует заказ и передает его «курьеру» для доставки, «курьер» доставляет заказ и делает отметку о доставке. Покупатель может редактировать все поля заказа, пока не передаст его на исполнение. После этого покупатель не может редактировать заказ, но может отозвать его, но только если заказ еще не передан для доставки. Оператор не может редактировать заказ, но может оповестить покупателя о задержке в связи с отсутствием товара на складе. Курьер может только проставлять отметку о доставке и только на тех заказах, что переданы для доставки ему. Ну и т.д. (много деталей опущено).

Довольно типичная картина, не правда ли? Действия доступные пользователям зависят от их роли и текущего состояния сущности, причем все эти особенности и детали способны утомить еще при чтении требований, не говоря уж о реализации. И в тоже время они являются весьма важными с точки зрения заказчика. А при реализации они размазываются тонким ровным слоем по всей бизнес логике и по UI в придачу. Ситуацию усугубляет то что, все эти требования очень волатильны, то есть склонны к частым и непредсказуемым изменениям. И вот он – живой кошмар любого разработчика перед нами во всей красе.
Однако с подобными задачами довольно просто можно справиться на основе workflow подходов, и в частности, с помошью конечного автомата или Finite State Machine.
Основная идея состоит в том, чтобы описать диаграмму состояний сущности (в нашем случае заказа), и допустимых переходов, при этом связав их с ролями пользователей.
Обычно, говоря о методе конечных автоматов, подразумевают создание класса, реализующего конкретную диаграмму. Но в нашем случае интереснее использовать иной подход, который менее распространен. Нам интереснее создать класс, способный исполнять любую диаграмму состояний, по воздействию внешних событий. При этом список переходов, доступных в данном состоянии для данного пользователя, на уровне UI представляется в виде набора доступных действий. А выбор любого из этих действий, вызывает выполнение соответствующего перехода в диаграмме состояний, и выполнение связанной с ним бизнес логики.

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

Что должен уметь делать этот framework? Он должен:
- описывать диаграммы состояний объектов, включающие перечень возможных состояний объекта, и возможные переходы между состояниями
- определять бизнес логику, выполняющуюся при изменении состояния объектов
- определять доступность переходов на основе ролей пользователей
- определять шаблоны оповещения при изменении состояния объекта и правила адресации на основе ролей пользователей
- переводить объекты из одного состояния в другое на основе описанных правил
- определять права доступа к атрибутам объекта в зависимости от состояния и роли пользователя

И в тоже время он не должен:
- зависеть от способов хранения бизнес-сущностей
- предъявлять какие либо требования к реализации классов бизнес-сущностей
- зависеть от UI библиотек (ASP.NET, WinForms)
- зависеть от провайдеров role-based security
- требовать наличия собственной БД для хранения своих настроек и состояния

Ничего готового на платформе .Net не обнаружилось. Windows Workflow не подошел на эту роль по причине своей монструозности (посмотрите список чего «не должен» делать движок и вам все станет понятно). Поэтому появилась мысль сделать свой движок, обобщив в нем свой многолетний опыт в данной области.
И вот в первом приближении такой движок готов. Называется он Listma, что значит Linking State Machine, или Подключаемая машина состояний, что вполне отражает его суть.
Listma - это проект с открытым исходным кодом. Хостится он будет на Google Code.

Сайт проекта http://code.google.com/p/listma/
Последнюю версию можно взять здесь http://code.google.com/p/listma/downloads/list
Бактрэкер проекта здесь http://code.google.com/p/listma/issues/list
Исходники с примерами здесь (SVN) http://code.google.com/p/listma/source/browse
Блог проекта здесь http://listma-rus.blogspot.com/

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

воскресенье, декабря 07, 2008

Опять про тестирование приватных функций.

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

На вопрос "Как тестировать приватные функции класса?" есть только один ответ: - "Только через публичный контракт этого класса." Если в голову лезут еще какие либо "решения", значит смысл модульного тестирования, а тем более TDD, до вас еще не дошел.

Что есть модульный тест? Это пример того, как внешний код будет использовать ваш класс. Зачем может понадобиться модульный тест для приватных методов класса, неужели вы рассчитываете на то, что внешний код будет вызывать приватные методы вашего класса? Так почему не сделать эти методы публичными?
Вопрос о тестировании приватных членов - это вопрос новичков. Вот у меня есть куча приватных методов и мне надо покрыть их тестами, как же мне это сделать? Это делается всегда одним и только одним способом - тестированием публичного контракта класса. Если у вас есть приватный метод, который вы не можете протестировать через публичный контракт, значит у вас что то не так с дизайном класса.

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

Я вот недавно писал библиотеку, в которой доля публичных методов менее 20%, остальные private и protected. Модульные тесты покрывают 95% кода библиотеки, причем тестируются только публичные члены. Я легко достиг бы и 100% покрытия, дописав guard-тесты, поскольку непокрытый код, это по большей части, валидация входных параметров и строки, выкидывающие исключения, но мне просто лень. В этой библиотеке есть полностью приватные классы, которых не видно снаружи, и они покрыты тестами на 100%.

Скажу более, в паре приватных методов я нашел большие куски по 6 -10 строк кода, не покрытые тестами. При внимательном рассмотрении, оказалось что этот код просто лишний :). Он никогда не вызывался ни при каких тестовых сценариях, и я его просто выкинул. Это, кстати, хорошая иллюстрация того, что понимается под последней буквой D (design) в TDD.

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

Небольшое дополнение, по прошествии одного дня.
Очень редко бывают случаи когда, действительно надо тестировать приватные члены. Предположим, у нас есть класс хранящий множество объектов и выполняющий с ними какие-то действия. Внутри этого класса, объекты хранятся в массиве, или в линейном списке. И вот по каким либо причинам, например для достижения большей скорости работы, нам необходимо реорганизовать внутренний механизм хранения объектов, вместо списка нам нужно сбалансированное двоичное дерево. Заметьте, что при этом публичный контракт класса не меняется. Но нам надо проверить, что внутренний механизм хранения в классе работает соответствующим образом. Тут без тестирования приватных членов, вероятно, не обойтись. Повторю, что такие случаи возникают крайне редко.

понедельник, октября 20, 2008

Вышел ASP.NET MVC framework beta

Scott Gu в своем блоге сообщает о выходе бета версии ASP.NET MVC framework. Там же есть ссылка для скачивания. Кроме того, Скот как всегда тщательно и подробно описывает все изменения которые появились в новой версии по сравнению с предыдущей.

Не новость конечно, но так, на память ссылка пусть будет.

вторник, сентября 23, 2008

Комментарии и код

В одном из предыдущих постов vinler задал вопрос о комментариях в коде. Я начал отвечать и понял, что тема интересная, и тянет на отдельный пост.
Итак вопрос:
Не мог бы ты высказаться по поводу вида комментариев в коде? Я так думаю, это не маловажная часть правил кодирования.
В частности очень интересует твое мнение, нужно ли вести прямо в коде историю изменений и сохранять имена авторов, если файл с момента создания находится под системой контроля версий.
Еще народ очень любит оставлять куски закомментированного кода. С поясненем, что так уже пробовали и это не сработало.


Есть распространенное мнение о том, что хорошему коду комментарии не нужны. И это действительно так. Другое дело, что хорошего кода не так уж много. Поэтому моя точка зрения: не можешь писать хороший код - пиши комментарии.
Не всегда удается написать хороший код. Иногда просто не хватает времени на проработку дизайна, кажется "потом переделаю", но "потом" обычно не наступает никогда. Иногда программисту просто не хватает опыта, чтобы разработать хороший дизайн. В этом случае очень полезно попытаться написать стандартные (для компилятора C#) комментарии к своим классам. После попытки сформулировать короткое описание класса часто наступает "просветление", после которого удается взглянуть на свой код по новому и подправить дизайн. Сам не раз ловил себя на таком.
Другая хорошая привычка, если уж ты чувствуешь в своем коде какой-то smell, но в силу каких-то причин ты не можешь им заняться прямо сейчас, напиши прямо в коде что-то вроде "//здесь воняет". Возможно потом это станет той последней каплей, которая перевесит чашу весов и ты почининишь этот код.
Одни из главных врагов читаемости кода, это невнятное именование и нарушение SRP (Single Responsibility Principle). Основные порождаемые ими проблемы, это невозможность понять по имени класса или метода что они делают, и невозможность отыскать нужный класс или метод. Есть такой парадокс: почти всегда мы считаем свой код хорошим и читаемым, а чужой плохим и невнятным. Происходит это от того, что о своем коде мы всегда знаем больше, чем о чужом. Поэтому никогда не повредит снабдить комментариями свои классы и ключевые методы.
Ну и на конец, если ты пишешь библиотеку для повторного использования, будь добр, прокоментируй публичные контракты всех своих классов. по моему это признак зрелости программиста и проявление уважения к своим коллегам.

Теперь по поводу ведения истории кода прямо в коде. Никогда не удалять старый код, а окружать его комментариями, вести многоэтажные заголовки файлов исходников с копирайтами и персональными списками изменений - все это полный маразм. Проверено, это не работает, и со временем просто превращает код в помойку. По моему это делают те, кто не умеет пользоваться системами контроля версий. Любая СКВ позволяет все это делать намного лучше и удобнее. Единственная полезная практика, сопровождать короткими коментами с номером бага или тикета свои изменения в старом коде (и то это ИМХО - мне так удобно).

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

понедельник, сентября 15, 2008

Правила хорошего кода

«Компетентный программист полностью осознает строго ограниченные возможности своего черепа, поэтому подходит к задачам программирования со всей возможной скромностью»
Dijkstra.

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

Есть масса рекомендаций по созданию хорошего кода. Большинство из них касается паттернов программирования. Другие относятся к дизайну классов, третьи – к оформлению кода. Но для качественного кода важны все они. Здесь я попытюсь собрать некоторые наиболее общие, не зависящие от конкретного языка (но лежащие в пределах объектной парадигмы). Паттерны оставим за бортом. Начнем с простейших правил оформления, и будем продвигаться к правилам дизайна.

Итак, правила хорошего кода:

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

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

  3. Пишите короткие методы (функции). Есть различные мнения по поводу оптимального размера процедур и функций. Я использую следующий критерий: если весь метод нельзя увидеть целиком на экране или странице – это слишком большой метод. С увеличением размера функции комбинаторная сложность управляющих структур (условий циклов и т.д.) и переменных возрастает нелинейно, и очень быстро начинает создавать проблемы не только в обеспечении надежности кода, но и в его понимании. Зачем вам лишние проблемы? Разделяйте большие функции на более мелкие. Первые кандидаты на выделение в отдельные функции, это сложные условия в предложениях if и тела циклов. Выделяйте в функции отдельные функциональные блоки. Если функция вычисляет и сохраняет, то выделите функцию, которая вычисляет, и функцию, которая сохраняет.

  4. Избегайте любого дублирования кода (правило трех О «Once and Only Once»). Существует масса приемов рефакторинга, которые помогают выполнять это правило. Используйте константы вместо литералов, выделяйте повторяющийся код в отдельные методы.

  5. Избегайте глобальных переменных (переменных с глобальной областью видимости). Глобальные константы - это хорошо, глобальные функции - это нормально, глобальные переменные – это зло. А как же быть с настройками приложения, спросите вы? Большинство платформ предлагают для этого специальные средства. Если же таких средств нет, используйте глобальные функции вместо глобальных переменных. Вместо Boolean MyGlobalFlag используйте функцию Boolean GetMyGlobalFlag(), которая будет возвращать копию текущего значения вашего глобального флага, а само его значение спрячьте за функциями доступа.

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


    public class Action
    {
    public void DoAction(Context context)
    {
    string message = string.Empty;
    If(IsInternal(context))
    message += PrepareInternalMessage(context);
    else
    message += PrepareExternalMessage(context);
    SendMessage(message);
    }
    private string PrepareInternalMessage(Context context){…}
    private string PrepareExternalMessage(Context context){…}
    private bool IsInternal(Context context) {…}
    // этот метод тоже должен быть private
    public void SendMessage(string message){…}
    }


    Класс Action предназначен для выполнения некоторого действия (DoAction), это собственно и есть его контракт. Метод SendMessage() представляет собой часть реализации этого действия. При этом формат сообщения поступающего в параметр SendMessage также определяется внутренней логикой класса Action. Все эти соображения говорят в пользу того, чтобы скрыть метод SendMessage. Наличие в публичном контракте подобных методов приводит к неправильному использованию класса.

  7. Не совмещайте в одном классе разные обязанности (принцип Single Responsibility Principle). Вот, пример базового класса для реализации действий:


    public class ActionBase
    {
    public abstract void DoAction(Context context);
    // полезная фича - запись в лог
    public void WriteLog(string message){…}
    }


    Здесь программист решил, что функция записи в лог потребуется во всех реализациях классов – действий и поэтому полезно будет ее вынести в базовый класс. Но основное назначение наследников ActionBase это выполнение действий (DoAction), а запись в лог это совсем другая обязанность. Ну и что с того? Вероятно, что запись в лог потребуется не только для классов-действий, но и для многих других классов. И нам придется либо создавать экземпляр класса-действия для записи в лог (т.е. использовать класс не по назначению), либо продублировать метод WriteLog в других классах. Оба варианта неприемлемы. Поэтому метод WriteLog следует вынести в отдельный класс, который будет заниматься исключительно записью в лог.

  8. Не разговаривай с незнакомцами (правило Деметры). Внутри метода следует обращаться только:
    • К параметрам, переданным в метод (к свойствам и методам этих параметров, если это объекты)

    • К членам объекта, которому принадлежит метод (this.xxx)

    • К объектам, созданным в данном методе.


    Цель - избежать связывания с непрямыми объектами.

  9. При построении иерархий наследования старайтесь не изменять и не расширять публичный контракт базового класса в наследниках. Данное правило напрямую вытекает из известного принципа подстановки Лискоу (LSP - Liskov Substitution Principle). Вот пример иерархии, который надо избегать:


    class Shape
    {
    public int X;
    public int Y;
    }
    class Rectangle : Shape
    {
    public int Width;
    public int Height;
    }
    class Circle : Shape
    {
    public int Radius;
    }


    Придерживаясь данного правила, вы защищаете уже написанный код от изменений в классах наследниках. Крайне неприятна ситуация, когда в различные места существующего кода приходится вносить многочисленные изменения при появлении нового класса наследника. Обычно такой код насыщен операторами if и case, выполняющими обращения к специфичным членам классов наследников.
    И все же. Наследники часто расширяют контракт базовых классов, достаточно взглянуть на классы .Net Framework. Почему? Такой подход часто оправдан при проектировании библиотек и фрэймворков. В этом случае базовый класс служит не столько для определения базового интерфейса, сколько для размещения логики взаимодействия с механизмами фрэймворка. Пример такой иерархии, компоненты .Net WinForms.

  10. Разделяйте команды и запросы (принцип QCS - Query Command Separation). Каждый метод должен представлять собой либо команду либо запрос. Метод-команда выполняет какие либо действия, и, возможно, изменяет состояние объекта. Метод-запрос возвращает данные объекта, но не меняет его состояние. Следование данному принципу вы всегда можете быть уверены, что состояние объекта остается неизменным, когда вы используете его данные, и всегда изменяется явно, при вызове методов-команд (к сожалению, возможности большинства языков не позволяет явно указать компилятору, что в методе не допустимо изменение состояния объекта).
    Правильно:


    class Counter
    {
    private int _value;
    public int GetCurrentValue(){ return _value}
    public void Increment(int delta) { _value += delta;}
    public void Decrement(int delta) { _value -= delta;}
    }


    Неправильно:


    class Counter
    {
    private int _value;
    public int GetCurrentValue(){ return _value}
    public int Increment(int delta) { _value += delta; return _value;}
    public int Decrement(int delta) { _value -= delta; return _value;}
    }


вторник, июля 15, 2008

Как работает ManualResetEvent

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

Итак, когда наш код выполняется в одном потоке, инструкции выполняются в определенном программой порядке. Но стоит нам запустить код в нескольких потоках у нас сразу возникает неопределенность в каком порядке выполняются инструкции нашего кода. Рассмотрим простейшим пример.


static void Main(string[] args)
{
Thread thread = new Thread(new ThreadStart(ParallelWork));
thread.Start();
Console.WriteLine("Main:thread work finished");
}

static void ParallelWork()
{
Console.WriteLine("Thread:begin work");
Thread.SpinWait(1000);// что то полезное делаем тут
Console.WriteLine("Thread:end work");
}


В методе Main() мы создаем и запускаем поток, в котором будет выполнен код метода ParallelWork(). Запустив несколько раз этот код мы можем получить на выходе:
Thread:begin work
Thread:end work
Main:thread work finished

или
Thread:begin work
Main:thread work finished
Thread:end work

или
Main:thread work finished
Thread:begin work
Thread:end work

Обычно ничего страшного в этом нет, ведь параллельное исполнение кода именно для этого и задумывалось. Но иногда у нас возникает необходимость синхронизировать исполнение отдельных участков кода в разных потоках.
Если взять наш куцый примерчик, то предположим, мы хотим, чтобы метод Main дождался завершения метода ParallelWork, и только потом завершился сам. Достичь этого можно разными способами. Например, можно объявить булеву переменную флаг, в метод Main вставить цикл проверки этого флага, а в конце метода ParallelWork выставлять флаг в true. Вот так:


static bool flag = false;
static void Main(string[] args)
{
Thread thread = new Thread(new ThreadStart(ParallelWork));
thread.Start();
while (!flag) ;
Console.WriteLine("Main:thread work finished");
}

static void ParallelWork()
{
Console.WriteLine("Thread:begin work");
Thread.SpinWait(1000);// что то полезное делаем тут
Console.WriteLine("Thread:end work");
flag = true;
}


На выходе получим искомое:

Thread:begin work
Thread:end work
Main:thread work finished


за счет того что в методе Main будет молотить холостой цикл while(!flag) ; пока метод ParallelWork() не выставит flag = true. Основной недостаток такого способа синхронизации состоит в том, что холостой цикл очень хорошо грузит процессор.

Ту же самую задачу можно решить при помощи ManualResetEvent.
Вот так:


static ManualResetEvent sync;
static void Main(string[] args)
{
sync = new ManualResetEvent(false);
Thread thread = new Thread(new ThreadStart(ParallelWork));
thread.Start();
sync.WaitOne();
Console.WriteLine("Main:thread work finished");
}

static void ParallelWork()
{
Console.WriteLine("Thread:begin work");
Thread.SpinWait(1000);// что то полезное делаем тут
Console.WriteLine("Thread:end work");
sync.Set();
}


Как это работает? Очень просто.
У ManualResetEvent есть внутреннее состояние: сигнальное и несигнальное. В сигнальное он переводится методом Set() в несигнальное - методом Reset(). Также начальное состояние ManualResetEvent можно задать и в конструкторе. В нашем случае мы создаем экземпляр ManualResetEvent в несигнельном состоянии.

Самый главный его метод WaitOne(). Когда в коде встречается вызов WaitOne() исполнение приостанавливается, если ManualResetEvent в несигнальном состоянии. А если в сигнальном – WaitOne исполняется без задержки. На этом и основано его использование. Мы заставляем код в главном потоке остановиться и ждать на вызове WaitOne(), пока где нибудь в другом потоке не вызовут Set(). Вызов Set() как-бы подает сигнал другому потоку (или нескольким), который висит и ждет на вызове WaitOne(), после чего этот поток может продолжить свое исполнение. Т.е. WaitOne() по своему эффекту похож на Thread.Sleep(), но с возможностью разбудить поток в нужный момент из другого потока.

Благодаря тому, что ManualResetEvent использует synchronization handle операционной системы, поток ожидающий на вызове WaitOne() действительно приостанавливается, ему не выделяются кванты процессорного времени и он не тормозит исполнение других потоков. И это основное отличие от синхронизации с флагом и холостым циклом. Висеть на WaitOne() может не один а несколько потоков, и все они выдут из ожидания при вызове одного Set().

Напоследок, хочу упомянуть, что рассматриваемую в примере задачу синхронизации можно решить и без ManualResetEvent, при помощи Thread.Join(), однако стоить заметить, что Thread.Join() использует внутри те же механизмы, что и ManualResetEvent


static void Main(string[] args)
{
sync = new ManualResetEvent(false);
Thread thread = new Thread(new ThreadStart(ParallelWork));
thread.Start();
thread.Join();
Console.WriteLine("Main:thread work finished");
}

static void ParallelWork()
{
Console.WriteLine("Thread:begin work");
Thread.SpinWait(1000);// что то полезное делаем тут
Console.WriteLine("Thread:end work");
}

четверг, июня 19, 2008

В помощь разработчикам под MS CRM

Если вам пришлось разрабатывать решения на основе MS CRM рекомендую блог Ronald Lemmen. Рональд является MVP как раз в области MS CRM. Помимо того, что в его блоге есть множество полезных ссылок на другие ресурсы, связанные с CRM, Рональд и сам пишет о многих тонкостях, которых вы не найдете ни в SDK ни в KB.
Мне, к примеру, он помог справиться с ошибкой при отладке Callout. Thanks a lot, Ronald!

пятница, июня 06, 2008

Surprise! Explicit Enum Cast in C#

О сколько нам открытий чудных
Готовит просвещенья дух.
(с) А. С. Пушкин

Как вы думаете, что выдаст на консоль приведенный ниже код?


public enum Parts
{
Engine = 1,
Wheels,
Brakes
}
static void Main(string[] args)
{
try
{
Parts lineItem = (Parts)4;
Console.WriteLine(lineItem);
}
catch
{
Console.WriteLine("Exception");
}
}

Не знаю, как вы, а я был убежден, что это будет "Exception". Однако на выходе получаем "4". Кстати, если Console.WriteLine(lineItem) заменить на Console.WriteLine(Enum.GetName(typeof(Parts),lineItem)) получим пустую строку. Вот так засада! Явным приведением можно загнать в enum любое значение underlying типа. Судя по обсуждению на RSDN это стало сюрпризом для многих.

Оказывается Anders Hejlsberg пишет в книге "The C# Programming Language":
"Each enum type has a corresponding integral type called the underlying type of the enum type. An enum type that does not explicitly declare an underlying type has an underlying type of int. An enum type’s storage format and range of possible values are determined by its underlying type. The set of values that an enum type can take on is not limited by its enum members. In particular, any value of the underlying type of an enum can be cast to the enum type and is a distinct valid value of that enum type."


Указания на такое поведение есть и в стандарте C#, дык только кто-ж это все читает...
Теперь, когда я думаю о том, сколько кода написано в твердой уверенности, что enum не может содержать никаких значений, кроме объявленных, мне становится как-то не по себе.
Надо взять за правило в switch-ах по enum-мам всегда ставить метку default.

понедельник, мая 12, 2008

Велосипедофобия

"Все мосты через преграды, переброшены без нас" (с) В.Высоцкий


Толкаясь на форумах начал замечать у разработчиков признаки надвигающейся эпидемии этакой "велосипедофобии". Спрашивает человек "Хочу реализовать логирование так-то и так...", а ему в ответ "Зачем изобретать велосипед!!! Есть log4net...", "Чукча не читатель. Чукча писатель. Опять велосипед изобретаешь...". Пишет другой человек статью о способах реализации Persistent Object, а ему в комментах: "Зачем это все надо! Кругом полно ORM-ов выбирай на вкус...". С ORM вообще тяжко стало. Похоже что проектировать свой ORM считается отменной ересью и признаком глубокой задвинутости.

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

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

И в заключение цитата из статьи Тода Хоффа (Todd Hoff) "Scaling Twitter: Making Twitter 10000 Percent Faster". Один из уроков, вынесенных в процессе масштабирования Twitter-а:

Build it yourself. Twitter spent a lot of time trying other people's solutions that just almost seemed to work, but not quite. It's better to build some things yourself so you at least have some control and you can build in the features you need.


"Делайте это сами. Twitter потратил кучу времени, испробуя казалось бы вполне работоспособные решения, сделанные другими, и все в пустую. Гораздо лучше сделать некоторые вещи самостоятельно, так что вы, как минимум, сможете иметь над ними полный контроль и встраивать новые фичи по мере необходимости". (с) Todd Hoff

P.S. На Тода Хофа наткнулся посредством вот этого блога Insight-it. Тематика, в основном, архитектура web приложений. Занимательно. Рекомендую.

понедельник, марта 31, 2008

Console Application - дурное дело, не хитрое?

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

  1. Измените сигнатуру метода Main. Вместо static void Main(string[] args) используйте static Int32 Main(string[] args). Значение Int32, возвращаемое методом будет использовано, как Exit code приложения. Это позволит вашему приложению просигнализировать вызывающему коду о характере возникших проблем. Обычно в качестве Exit code, сигнализирующего об успешном выполнении используют значение 0. Значения отличные от нуля сигнализируют о наличии ошибок.

  2. Поместите все тело метода Main() в блок try – catch. Необработанные исключения делают невозможным использование вашего консольного приложения в пакетах и сценариях. В блоках catch вы можете возвращать различные значения (отличные от нуля), которые расскажут вызывающему коду о характере произошедших ошибок. Для обработки ошибок вы можете использовать событие AppDomain.CurrentDomain.UnhandledException, но, на мой взгляд, try – catch в Main() и проще и нагляднее.

  3. Не используйте чтение с консоли (Console.Read() Console.ReadKey() Console.ReadLine()) потому, что это может оказаться неприятным сюрпризом для тех, кто использует ваше консольное приложение в пакете для автоматизации каких либо действий. Для ввода значений используйте либо аргументы командной строки, либо (если значений очень много) файлы.

  4. Предусмотрите выдачу help-a по параметру /? Это существенно облегчит жизнь тем, кто будет использовать утилиту, а от вас не потребует много труда на реализацию.

  5. Не скупитесь на Console.WriteLine() в коде своей утилиты. Пусть она подробно рассказывает о том, что делает, особенно при выполнении длительных операций. При обработке исключений используйте Console.WriteLine(exception.ToString()). Таким образом, вы выведите максимальное количество информации об ошибке, в т.ч. и stack trace. Для консольной утилиты это самое то, что надо.

Mashup Security

Интересная заметка о Mashup Security на Technology Review.
Смогут ли машапы стать безопасными? От этого зависит укоренится ли эта технология в enterprise секторе либо ее уделом останутся разнообразные Pageflakes, Netvibes и Yahoo Pipes, пока ее не смоет новой волной какого нибудь Web 3.0.
Проблемма в основном в том, как обезопасить корпоративные источники данных. Злодейский код из чужого домена, подмешанный в машап может с легкостью получить корпоративные данные и слить их куда надо, воспользовавшись царящим на сегодняшний день в машапах бардаком с доступом к учетным записям.
Microsoft, как обычно предлагает ввести новые тэги (читай изменить стандарты) и добиться того чтобы все браузеры их понимали (знакомо, не правда ли).
IBM предлагает другое решение - Smash, который не требует изменений в браузерах и будет разруливать неразбериху с credentials в машапах. Они даже предлагают исходники своей разработки в OpenAjax Alliance. Но энтузиазма на лицах ajax-оводов пока не наблюдается: "the next step for the mashup industry is to make sure that we develop a universal picture of security."
Видимо Smash на универсальное решение все же не тянет.

среда, марта 05, 2008

LINQ to Everything

Я немного перефразировал заголовок оригинального поста Link to Everything: A List of LINQ Providers в блоге Charlie Calvert's Community Blog.
Там приведен действительно впечатляющий список провайдеров LINQ начиная с Active Directory и Excel и заканчивая Amazon и Flickr. А как вам, к примеру, LINQ to JavaScript?
Да, пожалуй LINQ, это на сегодня самая "гормкая" и популярная инновация от Microsoft в области програмных языков.

вторник, марта 04, 2008

ASP.NET MVC Framework

Должен вам признаться в том, что я жуть как не люблю ASP.NET WebForms. Я не люблю их за то, что они хотят казаться WinForm-ами, а это у них плохо получается. Я не люблю их за постбэки (postback) и их аццкие порождения – вьюстэйты. Это не религиозные чувства, просто все это очень сложно и с ними не удобно работать. Viewstate тяжело контролировать, а в купе с серверными событиями он порождает очень неэффективные решения, которые по полной грузят и канал и сервер. Механизм postback-ов провоцирует размещать код, отвечающий за сценарий взаимодействия с пользователем (workflow) в самих aspx формах. Постепенно они обрастают кодом, который естественно не охвачен модульными тестами и служит постоянным источником проблем.

Но, есть хорошая новость. В готовящемся к выпуску пакете Microsoft ASP.NET 3.5 Extensions есть замечательная штука под названием MVC Framework.

В своем блоге Scott Gu опубликовал несколько подробных постов с описанием MVC Framework, ссылки на которые собраны под заголовком «ASP.NET MVC Framework Road-Map»

Что же такого хорошего предлагают нам под маркой MVC Framework?

Во-первых, это честный Model View Controller фрэймворк, который к тому же очень красиво спроектирован. Он позволяет делать хорошо структурированные решения, и предлагает для этого очень удобный инструментарий. Web запросы обрабатываются не ASP.Net страницами, а классами контроллерами. Контроллеры содержат всю бизнес логику, а на долю web страниц оставлена лишь отрисовка UI.

Во-вторых, MVC Framework легко расширяем. В качестве модели можно использовать практически все что угодно: ADO.Net DataReader-ы и DataSet-ы, любые ORM фрэймворки, XML, и конечно LINQ to SQL. В качестве View можно использовать обычные ASP.NET формы и контролы, или написать свои собственные, хотя MVC предлагает свой готовый набор компонент и утилит для генерации представления.

В–третьих, MVC Framework спроектирован максимально удобно для модульного тестирования. Основные контракты реализованы через интерфейсы, и легко заменяются mock-объектами (у меня сложилось впечатление, что необходимые mock-и уже есть в составе фрэймворка). Контроллеры и URL-мапперы можно тестировать вне ASP.NET процесса и приложения. Представления практически не содержат кода, кроме биндинга и рендеринга. Все это облегчает тестирование MVC приложения.

В-четвертых, MVC Framework содержит мощный и гибкий механизм URL-маппинга. Так, например, можно сменить формат URL с вот такого «/products/edit.aspx?id=4», на вот такой «/products/edit/4», и при этом не придется изменять ни одной строчки кода, в контроллерах и web станицах. Достаточно только поменять URL-маппинг.

При всем при этом, MVC Framework не отменяет и не конфликтует (я надеюсь…) с существующими фичами ASP.NET, такими как аутентификация windows/forms, membership/roles, механизм провайдеров, кэширование данных, master pages и т.д.

В общем, MVC Framework производит очень приятное впечатление. С нетерпением буду ждать его выхода. Вот только не понятно мне, почему он выходит в составе ASP.NET Extensions, по моему ему самое место в ASP.NET Core :)

пятница, февраля 08, 2008

Entity Framework - Identity, Computed и ключевые столбцы

Таблицы БД могут содержать столбцы, которые изменяются в момент сохранения изменений на стороне БД. В SQL Server это столбцы IDENTITY и вычисляемые (computed) столбцы. Как с такими таблицами работает Entity Framework?

Рассмотрим фрагмент Storage Schema (это Xml описание схемы БД в составе EDM)



<EntityType Name="Invoice">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
<Property Name="Customer" Type="nvarchar" Nullable="false" MaxLength="50" />
<Property Name="Cost" Type="money" Nullable="false" />
<Property Name="Discount" Type="money" Nullable="false" />
<Property Name="Total" Type="money" StoreGeneratedPattern="Computed" />
</EntityType>



Это описание таблицы Invoice. Столбец “Id” объявлен identity, а столбец “Total” - это вычисляемый столбец ([Cost]-[Discount]). Мы видим, что визард при генерации EDM добавил в описания этих столбцов атрибут StoreGeneratedPattern. Именно этот атрибут сигнализирует EF о том, что значение данного столбца вычисляется в БД.
StoreGeneratedPattern имеет три значения: “None”, “Identity”, “Computed”. Значение “None” принято по умолчанию. “Computed” обозначает, что столбец содержит вычисляемое значение, и при вставке и обновлении EF вместо того, чтобы передавать значения для этого столбца в БД, получает его значение после обновления. “Identity” соответственно обозначает столбец Identity. По смыслу он практически идентичен значению “Computed”, но заставляет EF генерировать специальный SQL код для получения значения этого столбца после вставки:


select [Id] from [dbo].[Invoice]
where @@ROWCOUNT > 0 and [Id] = scope_identity()


Свойства классов, замапленные на вычисляемые столбцы хот и имеют аксессор set, никогда не сохраняют свои значения в БД. Что бы мы ни присваивали свойству Invoice.Total после сохранения изменений, там будет записано значение, прочитанное из БД.

В этом отношении свойства классов, которые входят в состав Entity Key - ключа сущности, отличаются еще более странным поведением. Логично было бы предположить что Invoice.Id будет вести себя подобно Invoice.Total. Поскольку это свойство замаплено на identity столбец, то какие бы значения мы ему не присваивали. После сохранения мы получим значение, установленное БД. Это так лишь отчасти:


01 using (TestEntities context = new TestEntities())
02 {
03 Invoice inv = new Invoice();
04 inv.Id = 1;
05 inv.Total = 10;
06 context.AddToInvoice(inv);
07 context.SaveChanges(true);
08 Console.WriteLine(inv.Id);
09 inv.Id = 1000;
10 context.SaveChanges(true);
11 Console.WriteLine(inv.Id);
12 }


В приведенном коде мы сперва создаем экземпляр Invoice, а затем присваиваем значения его свойствам Id и Total. Затем мы сохраняем изменения в БД. После сохранения, в строке 8 мы видим в свойстве Id значение назначенное БД. Однако, когда в строке 9 мы пытаемся изменить значение Id то получаем исключение:

"The property 'Id' is read-only because it is part of the entity's key."

Вот так. В строке 4 присваивание Id проходит нормально, а здесь сваливается с исключением. Изменять значения ключевых свойств (свойств, которые входят в состав ключа) можно только, если состояние объекта “Detached” или “Added”. Такое поведение появилось только в beta 3. В предыдущем CTP изменения значений ключевых свойств тупо сохранялись в БД или игнорировались для identity столбцов.

вторник, февраля 05, 2008

Entity Framework - производительность

Для всех, кого интересует производительность Entity Framework, рекомендую почитать пост Brian Dawson в ADO.NET team blog - "Exploring the Performance of the ADO.NET Entity Framework - Part 1".

Там нет сравнительных характеристик, но зато подробно расписано из чего складывается время выполнения запроса, что кэшируется и как, и каким образом это время можно сократить. Очень интересные вещи, понимание которых, помогает писать правильный код. Например, Object context construction занимает 1.38% времени при самом худшем раскладе. А загруженные в MetadataWorkplace метаданные EDM сохраняются в глобальном кэшэ (на уровне приложения). Это, например, означает, что нет никакой надобности кэшировать контекст в ASP.Net сессии, а лучше создавать свой экземпляр для каждого ASP.NET запроса.

среда, января 30, 2008

Entity Framework - mapping (часть 2)

Продолжение. Начало здесь

Один класс - несколько EntitySet.



Еще один интересный вариант маппинга один класс – несколько таблиц, когда все таблицы имеют одинаковую структуру. В терминах EF это называется multiple entity sets per type (MEST). Рассмотрим пример БД:



БД содержит две таблицы одинаковой структуры для хранения каких-то запросов. Поскольку запросов много, для хранения текущих запросов используется таблица Request, а устаревшие и обработанные запросы переносятся в таблицу RequestHistory.

Если создать EDM визардом, мы получим две идентичные сущности Request и RequestHistory, и два соответствующих им EntitySet. Согласитесь это не очень удобно. Вот если бы EntitySet Request и RequestHistory остались, но оба использовали один и тот-же тип сущности. И это можно сделать. Однако нам придется опять расстаться с дизайнером, поскольку он не поддерживает такой вариант маппинга.

Итак, закрываем дизайнер и открываем edmx файл в XML редакторе. Изменяем описание EntitySet RequestHistory, чтобы он использовал класс Entity вместо EntityHistory:


<EntityContainer Name="MultiSet">
<EntitySet Name="Request" EntityType="MultiSetModel.Request" />
<EntitySet Name="RequestHistory" EntityType="MultiSetModel.Request" />
</EntityContainer>


Затем удаляем описание сущности RequestHistory (<EntityType Name="RequestHistory">).

Наконец правим маппинг EntitySet RequestHistory, заменяем тип RequestHistory на Request (измененное выделено):


<EntitySetMapping Name="RequestHistory">
<EntityTypeMapping TypeName="IsTypeOf(MultiSetModel.Request)">
<MappingFragment StoreEntitySet="RequestHistory">
<ScalarProperty Name="Id" ColumnName="Id" />
<ScalarProperty Name="IncomDate" ColumnName="IncomDate" />
<ScalarProperty Name="Text" ColumnName="Text" />
<ScalarProperty Name="AcceptDate" ColumnName="AcceptDate" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>



Вот собственно и все. Теперь класс Request используется при запросах к обеим коллекциям (и дизайнер не может открыть нашу модель).

Маппинг на хранимые процедуры



Многие спрашивают, поддерживает ли EF при маппинге классов хранимые процедуры? Для любителей маппинга на хранимые процедуры у меня две новости, хорошая и плохая. Хорошая состоит в том, что маппинг сущностей на хранимые процедуры в EF есть. Плохая – полностью отказаться от использования запросов в маппинге не возможно. Использовать хранимые процедуры в маппинге сущностей можно для операций вставки, обновления и удаления. В EF beta3 эти возможности уже поддерживаются дизайнером EDM. Однако для выборки все равно нужен маппинг на таблицы или view.

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

Создадим таблицу Category с двумя полями Id (первичный ключ) и Title:



CREATE TABLE Category(
Id int IDENTITY(1,1) NOT NULL,
Title nvarchar(50) NOT NULL,
CONSTRAINT PK_Category PRIMARY KEY CLUSTERED (Id ASC)



Обратите внимание, что ключевое поле Id объявлено как IDENTITY, это повлияет на некоторые детали маппинга.

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



CREATE PROCEDURE [dbo].[CreateCategory]
@Title nvarchar(50)AS
BEGIN
INSERT INTO [Category] ([Title])
VALUES(@Title)

SELECT [Id] from [Category] where @@ROWCOUNT > 0 and [Id] = scope_identity()
END

CREATE PROCEDURE [dbo].[UpdateCategory]
@Id int,
@Title nvarchar(50)AS
BEGIN
UPDATE [Category]
SET [Title] = @Title
WHERE [Id] = @Id
END

CREATE PROCEDURE [dbo].[DeleteCategory]
@Id int as
BEGIN
DELETE [Category]
WHERE [Id] = @Id
END



Из-за того, что столбец Id у нас объявлен как Identity, в процедуру CreateCategory передается только один параметр @Title, иначе надо было бы передавать еще и @Id. По этой же причине в процедуре присутствует select, который возвращает значение Id только что добавленной строки. И наконец, по этой же причине, в процедуре UpdateCategory параметр @Id используется исключительно для поиска нужной записи.

Теперь можно создать EDM по нашей базе, при этом в визарде надо поставить галочки для импорта хранимых процедур. Я не буду останавливаться на том, как хранимые процедуры описываются в EDM, там все довольно очевидно просто загляните в edmx файл и в документацию по EF.

Теперь займемся маппингом. В левом углу панели «Mapping details» есть две кнопки, которые переключают режим работы панели: «Map Entity to Tables / Views» и «Map Entity to Functions». До сих пор мы пользовались только первым режимом. Теперь нам надо переключиться на второй:



В пустой панели предлагается выбрать три функции для Insert, Update и Delete. Выбираем функции в выпадающих списках. Затем мапим параметры. Для функции CreateCategory необходимо выполнить Result Column Bindings. Для этого на месте надписи <Add Result Binding> вводим «Id» и мапим его на свойство Id. Таким образом, EF сможет получать значение Id вставленной записи.

Для параметров функции Update мы видим флажки «Use Original Value». При помощи этого флажка мы говорим, что EF должен взять оригинальное, а не измененное значение поля Id и передать его в функцию UpdateCategory. Почему так, я думаю, вы сами догадались.

Остается добавить, что «Result Column Bindings» есть и для функции Update. В нашем случае он не используется. Однако можно представить себе ситуацию, когда хранимая процедура не только сохраняет переданные в параметрах значения в БД, но руководствуясь какой-то своей логикой, еще и изменяет их. В этом случае новые значения можно вернуть через «Result Column Binding». Это, конечно, кошмарный сценарий, но в жизни бывает всякое.

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



Избавиться от него не получится. EF для выборок будет использовать только его. Единственное, что можно сделать, это заменить таблицу на view.

Ассоциация «многие ко многим»



Все мы знаем, что объектная ассоциация многие ко многим в реляционных структурах реализуется посредством промежуточной таблицы и двух ассоциаций «один ко многим». Вот я и решил посмотреть, как с этой задачей справится EF. Для этого я сделал вот такую БД с клиническим классическим примером «студенты – курсы»:



После работы визарда получилась вот такая EDM модель:



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

Заключение



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

Среди обнаруженных недостатков можно выделить следующие:

  • при маппинге столбцов необходимо устанавливать соответствие свойству и нельзя вместо свойства использовать константу

  • нет способа запретить создание навигационных свойств на одном из концов ассоциации.

  • нет возможности маппировать класс только на хранимые процедуры без использования таблиц и view.

  • не все возможности маппинга поддерживаются дизайнером EDM.

вторник, января 29, 2008

Entity Framework - mapping (часть 1)

В прошлых статьях я рассказывал про EDM и теперь приступаю к наиболее интересной ее части Mapping Specification. С того момента, как я взялся за описание этой темы, количество материала непрерывно увеличивалось, и сегодня я понял, что в один пост это все не влезет. Возможности маппинга в EF обширны. Я буду рассматривать их от простых к более сложным на конкретных примерах. Сегодня мы рассмотрим мапинг класса на несколько таблиц, маппинг ассоциаций и варианты маппинга при наследовании классов.

Один класс - одна таблица


Не рассматриваю данный вариант, во-первых, в виду его очевидности, во-вторых, он является подмножеством варианта «один класс – несколько таблиц», который мы рассмотрим более подробно.

Один класс - несколько таблиц


В маппинг EDM сущности (здесь и далее сущность и класс - это синонимы) изначально заложена такая возможность. Рассмотрим БД, в которой две таблицы БД Contact и Person:

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

Согласитесь, получилось не совсем то, что надо, вернее, совсем не то. Нам не нужна ссылка на Contact из Person. Нам надо, чтобы все поля Contact были в Person. Поэтому, удаляем Contact из модели, а в Person добавляем Scalar Properties: FirstName, LastName, Email, Phone. Теперь переходим к маппингу, кликнув в контекстном меню дизайнера команду “Mapping details”, и выделив Person на диаграмме:

То же самое, но в Xml выглядит вот так:


<EntitySetMapping Name="Person">
<EntityTypeMapping TypeName="IsTypeOf(test1Model.Person)">
<MappingFragment StoreEntitySet="Person">
<ScalarProperty Name="PersonId" ColumnName="PersonId" />
<ScalarProperty Name="BirthDate" ColumnName="BirthDate" />
<ScalarProperty Name="Address" ColumnName="Address" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>


“Maps to Person” в дизайнере соответствует Mapping Fragment в Xml описании EDM. Мапппинг любой сущности (EntityType) может состоять из нескольких фрагментов. Каждый фрагмент – это одна таблица в БД. В данный момент у нас выполнен маппинг только свойств, соответствующих таблице Person. Чтобы выполнить маппинг свойств добавленных в ручную жмем в дизайнере <add> и выбираем таблицу Contact. Дизайнер добавляет новый фрагмент и автоматически устанавливает соответствие между колонками таблицы и свойствами сущности, ориентируясь при этом на имена и типы данных:

Дизайнер все сделал замечательно, но у нас осталась без соответствия колонка Contact.ContactId. Устанавливаем ему в соответствие свойство PersonId, которое является ключом сущности Person. Все, теперь у нас данные сущности Person будут храниться в таблицах Person и Contact. Если вы что-то сделали не так, и вам надо удалить фрагмент маппинг свойства или фрагмент маппинга (таблицу) не ломайте кнопку Del на клавиатуре, не поможет. Для удаления элемента в дизайнере маппинга нужно в его выпадающем списке выбрать опцию <delete>. В первый раз я потратил на это пять минут :)

Итак, для создания описанного типа маппинга «один класс - несколько таблиц», при котором данные экземпляра сущности «размазаны» по нескольким таблицам, необходимым условием является наличие первичных ключей во всех участвующих таблицах, и наличие отношения «один к одному» между ними (причем наличие в БД явно прописанных foreign keys не обязательно). Если теперь выполнить объектный запрос к EntitySet Person, то SQL трассировщик покажет нам следующий SQL запрос сформированный EF:


SELECT
[Extent1].[PersonId] AS [PersonId],
[Extent1].[BirthDate] AS [BirthDate],
[Extent1].[Address] AS [Address],
[Extent2].[FirstName] AS [FirstName],
[Extent2].[LastName] AS [LastName],
[Extent2].[Email] AS [Email],
[Extent2].[Phone] AS [Phone]
FROM [dbo].[Person] AS [Extent1]
INNER JOIN [dbo].[Contact] AS [Extent2] ON [Extent1].[PersonId] = [Extent2].[ContactId]


Обратите внимание на INNER JOIN. Он означает, что в выборку могут попасть не все записи таблицы Contact. Если вы помните, то в БД таблица Person содержит foreign key constraint на таблицу Contact. Это означает, что любой записи Person соответствует запись Contact. Однако обратное утверждение не верно, и в таблице Contact могут быть записи, для которых нет соответствия в таблице Person. Объектным запросом к EntitySet Person мы эти записи выбрать не сможем. Имейте это в виду.

Наследование, или одна таблица - несколько классов (TPH)


TPH – это Table per Hierarchy. Таким термином в EF окрестили вариант маппинга, когда данные нескольких сущностей хранятся в одной таблице БД, а сущности (или классы) представляют собой иерархию, связанную отношением наследования. Звучит довольно сложно, но выглядит все очень просто. Рассмотрим пример, в котором снова используем многострадальную таблицу Person.


Теперь мы собираемся хранить в таблице Person данные о студентах и инструкторах. Id является первичным ключом таблицы, колонки FirstName, LastName, Email - общие для студентов и инструкторов. Для студентов также храним дату зачисления - EnrollmentDate, а для инструкторов дату приема HireDate, размер зарплаты Salary и ссылку на подразделение в котором числится инструктор - DepartmentId. Я специально не стал делать foreign key constraint на поле DepartmentId, чтобы показать, как EF обходится без них при создании ассоциаций сущностей. И, наконец, в колонке PersonKind мы будем хранить 0 для студентов и 1 для инструкторов. Этой колонке предстоит сыграть большую роль в построении TPH маппинга.

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



Для этого сначала воспользуемся визардом. Затем уберем в сущности Person лишние поля и переименуем ее в PersonBase. Это будет базовый класс (сущность) для нашей иерархии. Далее мы создаем две сущности унаследованные от PersonBase: Student и Instructor. Для этого в диалоге “New Entity” мы выбираем PersonBase в списке “Base Type”. Добавляем свойства в новых сущностях, в Student - EnrollmentDate, а в Instructor - HireDate и Salary. Теперь самое интересное - маппинг. Для сущности Instructor добавляем маппинг на таблицу Person:


Нужные столбцы замаплены автоматом. Как мы помним, данные об инструкторах у нас содержат строки таблицы, в которых PersonKind = 1. Этот столбец в терминах EF называется столбцом дискриминатором (discriminator column), а для маппинга мы задаем условие (condition). Условие накладывается именно на столбец дискриминатор. Для сущности Instructor мы устанавливаем условие PersonKind = 1, а для Student соответственно PersonKind = 0. Если мы попробуем сейчас скомпилировать всою модель, то получим ошибку примерно такого содержания:

Error 3023: Problem in Mapping Fragment(s) starting at line(s) (122, 130, 136, 137): Column Person.PersonKind has no default value and is not nullable. A default value is required to store some of the EDM states.

EF говорит, что ему не понятно, что же делать с PersonKind для сущности PersonBase. Оно не имеет маппинга и значения по умолчанию, а в БД описано как NOT NULLABLE. В предыдущих CTP для разрешения этой проблемы приходилось вводить условие в маппинге базового класса. Но в нашей стрктуре не придусмотрена возможность хранения каких-то персон, кроме студентов и инструкторов. Студентам предназначено значение PersonKind=0 а инструкторам PersonKind=1. В beta3 эта проблема решена очень красиво. В свойствах сущности PersonBase мы можем указать специальное свойство «Inheritance Modifier» - abstract. Оно указывет на то, что класс PersonBase будет объявлен как абстрактный, а это именно то, что нам нужно.

Заметьте, что столбец PersonKind нигде не замаплен в виде свойства. Тем не менее, его значения будут заданы автоматически при сохранении экземпляров Student и Instructor. В этом смысле Condition можно рассматривать как разновидность маппинга.

Теперь рассмотрим, какие возможности существуют для задания условий. Прежде всего, мы можем задавать условия по нескольким столбцам. Для сравнения может быть использован оператор «=» или оператор «is null» или оператор «is not null». Если мы используем оператор «=», то для сравнения используется константа. Никаких «больше» или «меньше», никаких диапазонов выражений и т.д. Эти ограничения следует учитывать при дизайне модели. В дизайнере маппинга в EF beta3 присутствует баг связанный, с заданием условий. При задании условия PersonKind = 1 значение «1» записывается в edmx файл в каком то закодированном значении. Если при компиляции модели вы получаете сообщение об ошибке, подобное этому:

Error 2016: The value specified for the condition is not compatible with the type of the Member.

то вам надо открыть edmx файл в XML редакторе и исправить значение атрибута Value элемента Condition(<Condition ColumnName="PersonKind" Value="_x0031_" >)
Вместо “_x0031_” должно быть “1”. После этого сохраняем файл и вновь открываем его в дизайнере.

Теперь несколько слов о реализации отношений наследования. Сущности наследники не имеют своих EntitySet и принадлежат EntitySet базовой сущности. Это означает, что в классе ObjectContext нашей модели не будет свойств коллекций, соответствующих Student и Instructor. Как же тогда нам делать выборки судентов и инструкторов? Для этих целей существует метод ObjectQuery.OfType(). Используется он следующим образом (выбираем всех инструкторов):


using (TPH context = new TPH())
{
ObjectQuery<Instructor> q = context.Persons.OfType<Instructor>();
foreach (Instructor i in q)
{
if (!i.DepartmentReference.IsLoaded)
i.DepartmentReference.Load();
Console.WriteLine("Instructor: {0} {1} dep. {2}", i.FirstName, i.Lastname, i.Department.Title);
}
}



Ассоциации



В нашей модели есть еще и ассоциация между Instructor и Department, которую мы создали руками, поскольку соответствующего constraint в БД не было. Рассмотрим создание ассоциации более подробно. Для создания ассоциации используем команду контекстного меню дизанера «Add association».



В появившемся диалоге указываем сущности (Entity) между, которыми устанавливается ассоциация, мощность отношения (Multiplicity), и навигационные сойства (Navigation Property), которые будут добавлены в сущности. Тут главное, не запутаться в концах ассоциации :). В этом плане очень помогает информационное поле в нижней части окна, в котором ассоциация описывается словами. Внимательно читайте эти описания и сопоставляйте их с тем, чего вы хотите добиться. Изменяйте параметры концов ассоциации, пока не добъетесь нужного результата. В нашем случае необходимо, чтобы Instrictor имел ссылку на 0 или 1 экземпляр Department, а Department ссылался на * (несколько) экземпляров Instructor посредством специального свойства - коллекции. Заметьте, что в зависимости от мощности отношения на конце ассоциации, соответствующее навигационное свойство будет представлять собой ссылку либо коллекцию. Для большей семантической ясности я рекомендую использовать множественное число для имен свойств – коллекций (Department.Instructors), хотя дизайнер по умолчанию дает имена в единственном числе всем navigation properties.

Итак, ассоциация создана, но это еще не все. Для ассоциаций, как и для сущностей в EF необходимо указывать маппинг. Поскольку в ассоциации участвуют две таблицы, в нашем случае это Department и Person, то какую из них выбирать для марппинга? Ответ очевиден, ту, в которой размещено поле внешнего ключа. В нашем случае это таблица Person и поле DepartmentId. А сам маппинг ассоциации выглядит так:



С ассоциациями EF связанна одна неприятная особенность. Как вы заметили, навигационные свойства создаются для обеих сущностей, участвующих в ассоциации. Причем способа отменить создание навигационного свойства на одном из концов ассоциации не существует (во всяком случае я его не обнаружил). Однако многие ассоциации имеют однонаправленный характер. Типичный пример, справочник валют. В большой системе десятки сущностей могут иметь ссылки на справочник валют, некоторые и не одну. Причем самомой сущности «справочник валют» вовсе не интересно, кто там на нее ссылается. В EF ваш справочник валют ощетинится десятками свойств коллекций, вовсе ему не нужных.

Наследование, или таблица для каждого класса (TPT)



Рассмотрим вариант реализации наследования, в котором каждому классу соответствует отдельная таблица в БД. В терминах EF данный вариант называется Table per Type (TPT).

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

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

Обратимся опять к модели, которую создал визард по схеме БД.


Для реализации задуманного нам необходимо удалить ассоциацию FK_Person_Contact и вместо нее установить между двумя сущностями отношение наследования. Для этого надо выполнить команду контекстного меню «Add Inheritance…» и в появившемся диалоге указать родительскую и дочернюю сущность. После того как отношение наследования установлено, мы обнаруживаем, что маппинг дочерней сущности (Person) исчез и его необходимо выполнить заново. Это не баг, потому что мы уже знаем, что иерархии наследования в EF могут маппироваться различными способами.


Обратите внимание, что сущность Person не содержит первичного ключа, как и все унаследованные сущности. Свойство PersonId мы также удалили за ненадобностью (у нас есть ключ в родительском классе). Маппинг Person тоже предельно прост:


Добавляем таблицу Person. Никаких условий в нашем случае использовать не надо. Обратите внимание, осиротевший столбец PersonId маппим на свойство родительского класса ContactId.

Интересно взглянуть на SQL запрос, который генерируется при выборке по EntitySet Contact, т.е. по базовому классу:



SELECT
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN '0X' ELSE '0X0X' END AS [C1],
[Extent1].[ContactId] AS [ContactId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Email] AS [Email],
[Extent1].[Phone] AS [Phone],
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN CAST(NULL AS datetime) ELSE [Project1].[BirthDate] END
AS [C2],
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN CAST(NULL AS nvarchar(200)) ELSE [Project1].[Address]
END AS [C3]
FROM [dbo].[Contact] AS [Extent1]
LEFT OUTER JOIN (SELECT
[Extent2].[PersonId] AS [PersonId],
[Extent2].[BirthDate] AS [BirthDate],
[Extent2].[Address] AS [Address],
cast(1 as bit) AS [C1]
FROM [dbo].[Person] AS [Extent2] ) AS [Project1] ON [Extent1].[ContactId] = [Project1].[PersonId]


Тут мы видим нечто удивительное. В запросе по базовому классу тянутся данные принадлежащие классу наследнику, а также хитрые ‘0X0X’, которые можно интерпретировать, как дискриминатор типа. Это заставляет нас предположить, что экземпляры, выбранные по запросу к базовому классу могут быть приведены к классам наследникам. Проверка подтверждает это предположение:


using (TPT context = new TPT())
{
ObjectQuery<Contact> q = context.Contact;
foreach (Contact c in q)
{
if(c is Person)
Console.WriteLine("person {0} {1}", c.LastName, ((Person)c).Address);
else
Console.WriteLine("contact only {0}", c.LastName);
}
}


Выражение (c is Person) возвращает true для тех контактов, у которых есть данные в таблице Person. Причем приведение к классу наследнику не порождает подчиненных запросов, подтягивающих дополнительные данные, при инстанциации экземпляров сразу используется нужный тип и он заполняется всеми данными. По полиморфизму зачет.

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

На сегодня все.
В следующей части я расскажу о том, что такое MEST и чего не может дизайнер EDM.

четверг, января 24, 2008

В чем отличие Thread.Sleep() и Thread.SpinWait()

В чем отличие Thread.Sleep() и Thread.SpinWait()? Оба метода позволяют приостановить исполнение текущего потока. MSDN немногословна:
Thread.Sleep
Blocks the current thread for the specified number of milliseconds.

Thread.SpinWait
Causes a thread to wait the number of times defined by the iterations parameter.

После таких описаний, ИМХО, становится еще непонятной.
Между тем различие этих методов весьма велико. Thread.Sleep() не только блокирует текущий поток, но и сообщает планировщику потоков Windows (scheduler) о том, что текущий поток освобождает причитающийся ему квант процессорного времени (time slice), поэтому scheduler может передать этот квант следующему потоку в очереди. Если вы передали в Thread.Sleep() достаточно большое значение, то ваш поток на протяжении всего этого промежутка практически не будет потреблять процессорное время. При обходе очереди потоков, ожидающих своего кванта процессорного времени, планировщик будет просто пропускать ваш поток. Это делает метод Thread.Sleep() весьма полезным, например, для борьбы с явлением известным под названием "инверсия приоритетов", когда потоки с низким приоритетом, но "жадные" до ресурсов, вытесняют потоки с более высоким приоритетом.
Еще один интересный момент. Знаете ли вы, что обозначает вызов Thread.Sleep(0)? Поток вообще не заснет, или передаст управление другому потоку? MSDN говорит:
A value of zero causes the thread to relinquish the remainder of its time slice to any other thread of equal priority that is ready to run. If there are no other threads of equal priority ready to run, the function returns immediately, and the thread continues execution.

Т.е. остаток кванта времени, причитающегося потоку будет отдан другому потоку ожидающему исполнения, с тем-же приоритетом что и у текущего, а если такого потока нет, то текущий поток продолжит работу. Т.е. бороться с инверсией приоритетов при помощи Sleep(0) вообще бессмысленно. Джо Даффи призывает вовсе избегать использования Sleep(0), и даже завести правило в FxCop на этот счет.

Метод Thread.SpinWait() ведет себя совсем по другому. Главное отличие от Thread.Sleep() состоит в том, что здесь не происходит переключение контекста потока. Т.е. текущий поток не передает управление планировщику Windows. Вместо этого, внутри метода SpinWait() запускается некий холостой цикл, на что прозрачно указывает название метода. Число итераций этого передается в параметре метода. Отсюда можно сделать вывод, что время ожидания SpinWait() будет очень незначительным, даже по сравнению с вызовом Sleep(1). Ведь, согласно утверждению Джо Даффи, только переключение контекста потоков занимает 4000+ процессорных тактов.
Для чего стоит применять метод SpinWait()? В основном, в случаях когда потоку необходимо "немножко подождать", не передавая при этом управления другим потокам (понятно, что за время исполнения SpinWait планировщик может прервать текущий поток, но SpinWait этого "не заметит"). Обычно SpinWait() применяют в "тонких" техниках неблокирующих алгоритмов, когда ценой потери нескольких сот тактов мы можем избежать переключения контекста при явной блокировке на мониторе или вызове Sleep(), а также связанных с ними проблем обновления кэша и т.д. Т.е. SpinWait применяется довольно редко. Добавлю, что SpinWait дает хорошие результаты на много процессорных конфигурациях, и еще, его поведение отличается стабильностью и не зависит от аппаратной конфигурации (один процессор, HyperThread, много ядер).