пятница, 25 декабря 2009 г.

ORM-style IdentityGenerator or impossible is nothing

То, что я сейчас собираюсь описать, иначе как грязный хак назвать нельзя. Но жизнь сложная штука, и иногда приходится изворачиваться как уж, чтобы добиться своего. А добиться я пытаюсь вот чего: чтобы NHibernate не ломал семантику Unit of Work при работе с классами, Id которых генерится Identity счетчиком в SQL Server.

Прежде всего, советую ознакомиться с историей вопроса:
о вреде Identity генераторов, еще о вреде и мнение в законе. Проблема Identyity генератора в том, что он не может получить Id, не вставив запись в таблицу, а без Id сущность нельза ассоциировать с NHibernate сессией, поэтому как только мы пытаемся связать объект с сессией, он тут же попадает в базу. Отсюда вырастают огромные проблемы в длинных бизнес транзакциях, когда все действия с БД атомарны и выполняются в отдельных БД транзакциях, а сама бизнес операция растянута по времени. Так вот откат бизнес транзакции будет невозможен в случае использования Identity генератора. А это как раз мой случай. Но, как я уже написал выше решение есть, но это грязный хак.

Итак. Прекратив ныть, про хак, перехожу к сути. Мне надо:

  • вставлять свои значения в поля Identity;
  • получать адекватные значения для Identity поля;
В SQL Server можно отключить автоинкрементацию Identity поля и вставить свое значение, например, пропущенное в последовательности (при удалении). А получить его можно, вставив в таблицу запись и удалив ее. Это возможно, потому что при откате транзакции значение счетчика Identity назад не откатывается, таким образом, изменив таблицу, мы перекидываем счетчик, а затем откатив транзакцию, убираем изменения.

Вооружившись этим знанием я нарисовал следующую схему:

Решение, как следует из картинки состоит из 3х частей:
  • Custom Id generator, который я назвал ORMIdentityGenerator;
  • PreInsert listener, для отключения автоинкрементного поля;
  • PostInsert listener, для его включения назад;
ORMIdentityGenerator

Следуя agile принципу KISS - keep it simple, stupid! я в
сего лишь конфигурирую его минимальным INSERT выражением, которое можно исполнить, не повредив базу и не вызвав нарушений целостности.

PreInsert


PostInsert


Вот и все. Теперь я могу спокойно редактировать объект в длинной сессии и откатывать изменения, когда потребуется. PS Простите ребята, гаджетом для вставки кода пока не обзавелся. Удачи!

пятница, 2 октября 2009 г.

Detached Template Method pattern

Идея не нова, ее я встретил впервые в Spring framework, в части интеграции с ORM framework’ами. Так для упрощения написания своих Repository или DAO (Как назвать дело вкуса. Я предпочитаю термин Repository, т.к. с точки зрения DDD он более точно описывает свое назначение.) Spring предлагает 2 опции:

  • передать своему DAO XXXTemplate и пользоваться предоставляемыми им методами, инкапсулирующими последовательность работы с конкретным ORM;
  • Унаследоваться от XXXDaoSupport, в котором XXXTemplate будет подставлен без вашего участия IoC контейнером;
Так что же это такое? Давайте обратимся к классикам [GoF]:

    «Template Method – паттерн поведения [классов]. Template Method определяет основу алгоритма и позволяет подклассам переопределить некоторые шаги алгоритма, не изменяя его структуру в целом.»

Почему Detached? Ну, вот так я придумал. Потому что это не предполагающая наследования реализация идеи Template Method в отдельном классе. Методы класса определяют последовательность действий, которые должны быть выполнены, скрывая детали реализации алгоритма и нижележащих ресурсов. Переопределение же некоторых шагов возможно при помощи callback’ов.

К примеру, под капотом у всех XXXTemplate в Spring происходит следующее (упрощенно):

  1. выделение ресурсов (создание connection и т.д.); (фиксированная часть алгоритма)
  2. начало транзакции; (фиксированная часть алгоритма)
  3. вызов callback executeInTransaction; (изменяемая часть алгоритма)
  4. возврат данных; (изменяемая часть алгоритма)
  5. подтверждение/откат транзакции; (фиксированная часть алгоритма)
  6. освобождение ресурсов; (фиксированная часть алгоритма)

Все эти шаги вам пришлось бы реализовывать самим в каждом месте, где предполагался бы код доступа к данным. Кроме того XXXTemplate скрывают детали реализации и отвязывают от лишних зависимостей. Например, HibernateTemplate конвертирует hibernate-специфичные exception’ы в Spring DataAccessException.

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

У меня есть базовый класс Resource, реализующий disposable pattern. Многие из его наследников – объекты системы, хранящие данные о состоянии системы и идентифицирующие конкретные экземпляры по Id.

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

Как видите, IIdentifiable наследует интерфейс IEquatable, т.е. объекты должны сами определять является ли переданным им экземпляр идентичным. И, наконец, тождественность объектов должна определяться не только на основании Id, но и на основании других полей данных. Чувствуете к чему это идет? Мне придется реализовывать логику сравнения по Id и по значимым полям в каждом классе, многократно дублируя код! Я такую личную неприязнь к дублированию чувствую, что аж кушать не могу.

Поэтому давайте разберемся а что же я должен сделать? Я должен реализовать алгоритм установления тождественности, который содержит фиксированную часть (сравнение ссылок

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

Потому в IIdentifiable я ввел метод DataEquals – это изменяемая часть алгоритма и, как мне кажется наименьшее из зол (но все-таки зло, потому что он публичный и засоряет интерфейс (один из способов уменьшения этого зла - explicit implementation)), а в каждом реализуемом методе Equals интерфейса IEquatable я делегирую выполнение алгоритма моей реализации Detached Template Metod – EqualsTemplate.

Я знаю, знаю, что в .Net есть IEqualityComparer, но мне мое решение нравится больше по 3м причинам:

  1. мне не придется «заставлять» пользователей моих классов использовать IEqualityComparer;
  2. мне кажется, что реализовать один метод проще, чем написать новый класс;
  3. оно мое;
Посмотрите как просто реализуется теперь этот алгоритм:



Удачи!

воскресенье, 27 сентября 2009 г.

Select object rather then string in JSF

Сначала хотел написать, что пост кардинально отличается от предыдущих, потому что про Java и связанные технологии, а потом подумал и понял, что основное-то предназначение блога - делиться опытом применения паттернов и методологий, а не описывать "фичи" языка, который всего лишь инструмент в данном случае. А потому начну так: Этот пост не сильно отличается от предыдущих, но он про Java и связанные технологии.

Все еще здесь? Тогда продолжим! (отлично, спер клише, молодец)


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

Проект – небольшой сайт с frontend на JSF 1.2, facelets, RichFaces и инфраструктурой с использованием Spring и Hibernate ORM.

Так вот заметка моя касается представления списков выбора в JSF, которая сделана, на мой взгляд, весьма коряво. Вкратце – сущности, из которых ведется выбор, вы должны представить в виде строк, завернув в класс SelectItem. Назад вместо объекта вы, естественно получите строку, а вовсе не объект, т.к. последний не путешествовал от сервера к клиенту и обратно. Так вот мое решение призвано обойти данную шероховатость JSF.
Кстати сказать, я не один такой умный и решений этой задачи есть как минимум несколько: http://jsf-comp.sourceforge.net/components/easysi/index.html, http://balusc.blogspot.com/2007/09/objects-in-hselectonemenu.html, - но… Первое не работает с RichFaces, а второе я нашел уже после того как написал свое.

На решение натолкнула меня тоска. Ага, тоска по счастливым временам когда GUI я «творил» на WPF. Я тогда сильно упростил себе жизнь, написав SelectorCollectionPresenter. И я подумал
–почему бы не использовать похожий подход в Java? Сказано – сделано. Я написал класс CollectionPresenter =).

Основная идея – это объект, который хранит карту ассоциаций строковых представлений с объектами и текущую выбранную строку, по которой, собственно, из карты выбирается объект.


Для Generic коллекций в Java я использовал реализацию apache Generic common-collections

Теперь, имея возможность получить ссылку на объект, можно написать facelet,



который получает на вход список SelectItem’ов, которые создал Presenter, и возвращает выбранную строку, по которой Presenter вернет соответствующий объект.
В принципе этот процесс можно отдать на откуп facelet’у целиком:



И вот как выглядит использование первой версии facelet’а в разметке страницы:


Поверх presenter’а я навернул еще generic контроллер, который выбирает список объектов из
Repository и подставляет их presenter’у.


Важно заметить, что работать будет с контроллером, с любым временем жизни (стратегией времени жизни в Spring контейнере).

Объекты, для выбора в моем случае – строки справочника, представляемые наследниками класса DictionaryEntry, что в общем случае необязательно, просто тогда надо будет написать соответствующий контроллер.


Вот и все решение, в общих чертах!

Удачи!

четверг, 3 сентября 2009 г.

Fluent Builder

Fluent Interface и Builder - идеальное сочетание для декларативного создания объектов.
На самом деле примеры такого выгодного симбиоза давно у нас под носом, например: StringBuilder.

Я использовал такой симбиоз для декларативного создания SIP аккаунтов и звонков.

Решение состоит из внутренних Builder-классов, которые предоставляют декларативный интерфейс для создания объектов и скрывают детали типа суффиксов транспорта, схемы и пр., о которых не надо задумываться.








Оба этих класса опираются на вспомогательный Builder – SIPUriBuilder. Этот класс настолько интуитивен, что я удивлен, что я не написал его первым делом.



Результат на лицо:


Было:

string remote = "sip:";

remote += txbURI.Text == "00"

? "127.0.0.1"

: (txbURI.Text + "@" + SIPUserAgent.SIPUserAgent.Instance.AccountManager.DefaultAccount.RegistrarUri.Split(new[]{':'})[1]);

remote += SIPUserAgent.SIPUserAgent.Instance.SIPTransport is TCPTransport

? ":5061;transport=TCP"

: "";

SIPUserAgent.SIPUserAgent.Instance.CallManager.MakeCall(remote);


Стало:

Call c = Call.ConstructCall().SetAccount(acc).SetExtension(txbURI.Text).SetDomain(txbURID.Text).Call();


Было:

var acc = new Account(false);

using (acc.CreateInitializationSession())

{

acc.Credential = new NamePasswordCredential

{

Password = txbPass.Text,

Realm = txbRegistrar.Text,

UserName = txbLogin.Text

};

acc.AccountId = "sip:" + txbLogin.Text + "@" + txbRegistrar.Text;

acc.RegistrarUri = "sip:" + txbRegistrar.Text;

if (ua.SIPTransport is TCPTransport)

{

acc.AccountId += "5061;transport=TCP";

acc.RegistrarUri += "5061;transport=TCP";

}

}

ua.AccountManager.RegisterAccount(acc, true);


Стало:

var acc = Account.ConstructAccount().SetLogin(txbLogin.Text).SetPassword(txbPass.Text)
.SetRegistrarDomain(txbRegistrar.Text).Register();


Комментарии я думаю излишни. Просто сравните число строк.

Удачи!

среда, 15 апреля 2009 г.

Designing managed API over "flat" native one or .Net SIP library

Попробую описать процесс проекции плоского API, написанного в процедурном стиле, в многомерное ООП пространство, на примере работы, которой было занято почти все мое свободное время за последний месяц.

Возможно многим эта заметка покажется интересной не с точки зрения содержания, сколько с точки зрения результата работы, которым я готов поделиться со всеми, а именно - .Net обертка над замечательной SIP библиотекой pjsip, но для меня она ценна именно как описание процесса. Можно сказать мемуар.

Конечно обертка эта не покрывает все то богатство возможностей управления процессом, которое предоставляют различные модули pjsip, но предоставляет высокоуровневый .Net интерфейс к pjsua (user agent) API, необходимый для быстрого создания SIP клиентов.

  1. P/Invoke или как не утонуть в тоннах кода;
  2. Designing API;
  3. Testing API;
1. P/Invoke.

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

  • реализующая последние RFC в наиболее полном объеме;
  • с поддержкой различных кодеков, в том числе широкополосных;
  • портируемая на разные платформы (на будущее);
  • с возможностью передачи видео-потока (на будущее);
  • open source с разумной лицензией, которая позволила бы использовать ее в коммерческих приложениях. (UPD3: GPL, под которой распространяется эта библиотека не такая уж и разумная, а, как сказал мой друг: "детище неадекватного проповедника Столмена". Следует заметить, что исходное выражение подверглось жесткой цензуре =). )
pjsip удовлетворяет всем этим условиям, и многим другим, неперечисленным здесь. Замечательная вобщем библиотека.

Итак, прежде всего нам надо скачать исходные коды и собрать dll. Скачать pjsip можно из SVN хранилища, как стабильную версию, так и текущий закоммиченый код.

Далее, забегая вперед, сразу же скажу, что при генерации interop-кода возникла проблема с callback функциями, т.к. C# поддерживает только stdcall callback функции (UPD: врешь ты все =) ), т.е. делегаты будут вызываться как stdcall, тогда как pjsip декларирует свои callback'и как cdecl, это проблема! Путей решения у нее несколько.
Если бы у нас не было доступа к исходному коду pjsip - у нас была бы проблема, которую пришлось бы решать нетривиальным образом, изменяя IL код, сгенерированный для interop кода. Этот хак описан здесь.
UPD: Да, и пока я искал ссылку, нашлось более простое решение, поставляемое самой платформой .Net -
UnmanagedFunctionPointer атрибут, с помощью которого можно пометить делегат и обозначить calling convention.
Последнее же решение, к которому прибегнул я, - пометил все нужные мне callback функции в pjsua __stdcall, благо их немного.

Итак, dll, экспортирующую функции высокоуровневого pjsua API я собрал, но даже для этого интерфейса (довольно скромного по сравнению с более низкоуровневыми) я бы писал interop код вручную недели две. Столько времени у меня нет, да это еще к тому же ужасно скучно, поэтому я нашел средство автоматической генерации interop кода - PInvoke Assistant. Достаточно лишь указать этой замечательной утилите всю необходимую информацию, и вуаля - 90% кода сгенерились автоматически. Надо сказать, что 10% недостающего кода эта_замечательная_утилита все-таки не осилила, но винить ее в этом я не намерен, т.к. код был действительно очень сложный для автоматического разбора. Вобщем 10%, написанные вручную, все-таки лучше, чем 100%.

UPD2: нашел еще одну утилиту для генерации interop кода: SWIG
. Выглядит она намного мощнее той, которую я использовал, достаточно посмотреть на список поддерживаемых языков.

На этой стадии у меня уже был готов рабочий .Net интерфейс к pjsip, но вот только одна беда - в процедурном стиле, что впрочем не помешало мне написать тестовое консольное приложение, которое регистрировалось на SIP сервере.

2. Designing API.
Дизайн ООП обертки во многом продиктован структурой pjsua API (больше половины работы уже сделано за нас). pjsua логически и очень интуитивно разделяется на следующие группы:
Также логически и интуитивно я выделил следующие сущности:
  • SIPUserAgent - центральная фигура и главная точка доступа к API, отвечающая за создание/конфигурирование клиента;
  • Account Manager - интерфейс для управления аккаунтами (аналог - телефонный номер);
  • Call Manager - интерфейс для управления звонками (прием/создание новых звонков, управление состоянием);
  • Media Manager - интерфейс для управления медиа-параметрами системы (кодеки, звуковые устройства, конференция);
  • Conference Bridge - жалкое отражение мощного медиа-микшера pjmedia;
Итак вот он взгляд на систему с высоты:

Поскольку большинство объектов являются обертками над нативными структурами, выделяющимися в неуправляемой памяти, то нам нужен механизм управления освобождением последних. Такой механизм в .Net изобретен давно и называется он Disposable pattern. Очень кстати полезно помнить про этот паттерн, так как он .Net specific, а про такие MS любит спрашивать на собеседовании. Фундамент для Disposable pattern - базовый класс Resource, с одной лишь разницей, что он не реализует IDisposable, т.к. я не хочу отдавать контроль над временем жизни всех объектов, потому что это может нарушить работу системы. Например: нельзя дать пользователю этого API освободить ресурсы VoIPTransport'а во время звонка, или даже пока в системе есть зарегистрированные аккаунты. Следить же за всеми зависимостями просто невозможно, и вообще это есть нарушение всех принципов хорошего дизайна. Поэтому в Resource вместо публичной реализации IDisposable есть internal метод InternalDispose. В случае, когда время жизни объекта может управляться извне, то класс наследник Resource явно реализует интерфейс IDisposable.

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

SIPUserAgent (UA) - центральная точка доступа к API. UA - инициализируемый объект и прежде чем вы получите доступ к различным частям системы, его необходимо... правильно! проинициализировать. По окончании сессии UA проверит все введенные данные на валидность и, если все в порядке, создаст необходимые структуры pjsua. UA поддерживает конфигурирование в config файле, но по умолчанию не использует его, хотя и имеет для этого явный интерфейс - метод ReadConfiguration(), который может быть вызван только во время сессии инициализации. Имеется и зеркальный метод WriteConfiguration(), который следует вызвать до того, как UA будет уничтожен. Кстати, UA это пример объекта, временем жизни которого можно управлять пользователям API, поэтому он реализует интерфейс IDisposable и как дополнение, для большей наглядности метод Destroy(), который по смыслу ничем не отличается от Dispose().
SIPUserAgent, AccountManager, CallManager, MediaManager - singleton'ы и они существуют всегда в единичном экземпляре. Это означает, что в приложении вы не сможете создать больше одного SIPUserAgent'а, да оно и не требуется, т.к. в системе есть понятие аккаунтов.

Transport интерфейс pjsua инкапсулирован в классе VoIPTransport.
Это пожалуй самая маленькая часть системы, предоставляющая интерфейс для создания и конфигурирования конкретного типа SIP транспорта (UDP, TCP или TLS). В UA поумолчанию создается UDPTransport для SIP, который может быть заменен либо при загрузке конфигурации, либо явно одним из наследников VoIPTransport во время сессии инициаллизации. К сожалению в текущем релизе pjsip поддерживает только UDP транспорт для RTP, но в дальнейших планах стоит TCP транспорт для RTP, что авторы сопровождают пометкой "анекдот", им виднее. Поэтому на всякий случай VoIPTransport, а не SIPTransport.

Многие объекты системы имеют различные состояния, которые прекрасно описываются state machine. Состояния эти передаются через callback'и из pjsip и обрабатываются соответствующими менеджерами, так за обработку callback'ов сессии регистрации аккаунта отвечает AccountManager, который затем сообщает state machine, что ее состояние изменилось. Поначалу state machines были частью сущности, состояние которой они описывали, но при более детальном взгляде оказалось, что объект (например звонок [Call] имеет Invite-сессию и медиа-сессию) может иметь несколько никак не связанных состояний, которые лучше даже назвать сессиями. Поэтому для моделирования этих сессий я написал малюсенький набор классов, состоящий из StateMachine - базового класса для сессии; и AbstractState - базового класса для состояния. Классы состояний управляют логикой обработки событий (этакий workflow), а классы сессий просто хранят атрибуты состояния. Я даже рассматривал возможность использования WF движка для моделирования сессий, но понял, что это был бы overkill для задачи такого масштаба.

Accounts


Тут все просто - пользователь создает аккаунт, инициализирует, передает AccountManager'у, который позаботится о его дальнейшей судьбе. В принципе уже здесь можно выкинуть ссылку на аккаунт, т.к. он автоматически попадает в коллекцию аккаунтов AccountManager'ов, из которой будет удален только когда будет вызван метод AccountManagerUnregisterAccount(account).
AccountManager при старте системы создает внутренний локальный аккаунт, так что система сразу готова к приему и отправке звонков.
У аккаунта есть сессия регистрации, управляющая его состоянием. В принципе число возможных состояний аккаунта равно числу кодов статуса SIP протокола (около 100). Поэтому, чтобы не плодить кучу классов, описывающих состояния, в которые аккаунт неизвестно может ли перейти вообще, я описал лишь те состояния, в которых при тестировании аккаунт побывал однозначно (registering, timedout, registered), а для всех остальных написал общий класс UnknownStatusState, который, получая код статуса, пишет в Trace строку с описанием этого статуса.
Об изменениях в состоянии и Account и AccountManager информируют пользователей генерируя соответствующие события.
В классе Account великое множество свойств, из которых можно почерпнуть информацию о текущем состоянии аккаунта.

Calls

Здесь, конечно, дела обстоят посложнее, но не для пользователя системы, разумеется. С его точки зрения все также просто: чтобы позвонить куда-то нужно лишь вызвать метод MakeCall() CallManager'а, указав последнему куда позвонить и с какого аккаунта, чтобы принять звонок - надо подписаться на событие IncomingCall и вызвать метод Answer() переданного звонка, и все. Звонок также будет помещен в коллекцию звонков, и дальнейшей его судьбой будет управлять CallManager, ну и конечно же пользователь может явно закончить звонок, вызвав Call.Hangup(). Под капотом же все намного сложнее.

Начнем с того, что у звонка две сессии: Invite сессия, управляемая SIP протоколом, и медиа сессия, управляемая RTP протоколом.

С Invite сессией все более менее понятно - все через нее проходят каждый раз как набирают номер на мобильном и ждут соединения, а вот медиа-сессия более интересная тема. Дело в том, что звонок может иметь более одного слушателя, равно как и вещателя, т.е. быть частью конференции. Но здесь возникает небольшое расхождение с плоской иерархией классов состояний, описывающих эту сессию, т.к. звонок может быть активным и не участвовать в конференции, или участвовать, но не может быть неактивным и участвовать в конференции, т.к. это абсурд. Для описания этой ситуации я применил Decorator - ConferenceMediaStateDecorator, оборачивающий ActiveMediaState.

Между аккаунтом и звонками есть зависимость один-ко-многим, т.е. несколько звонков могут использовать один и тот же аккаунт. Следовательнго система должна каким-то образом следить за этими зависимостями. Можно создать множество и хранить в нем ассоциации, а можно воспользоваться механизмом подсчета ссылок, как это было сделано в COM. Именно этой техникой в сочетании с описанным приемом smart-pointer для .Net я и воспользовался для реализации это й задачи в loosely-coupled манере.


Каждый звонок при создании получает ссылку на аккаунт, с которым он ассоциирован. Он не хранит ее, а блокирует аккаунт (его невозможно будет удалить) вызовом метода Lock(), который возвращает IDisposable (smart-pointer), который при создании увеличивает число ссылок на аккаунт, а при явном уничтожении (Dispose) уменьшает. Этот smart-pointer освобождается при очистке ресурсов звонка.

Media

Медиа менеджер позволяет установить устройства воспроизведения и захвата звука, посмотреть коллекцию кодеков и посмотреть состояние конференц-моста.
Конференц-мост - это часть системы, выполняющая роль микшера медиа потоков. Звонки, подключающиеся к конференции, соединяются со всеми узлами двунаправленного графа, уже участвующих в конференции звонков. В системе может быть только одна активная конференция.
Подключение к конференции может происходить как автоматически - если установлен флаг UA AutoConference (тогда ActiveMediaState будет автоматически оборачиваться ConferenceStateDecorator'ом, который и подключает звонок к конференц мосту), либо явно при помощи метода Call.ConnectToConference() (в этом случае сначала будет произведена проверка активна ли медиа сессия и, если да, то ActiveMedaiState будет обернут ConferenceStateDecorator'ом с тем же результатом). За подключение звонков между собой отвечает ConferenceBridge.

3. Testing API.
Сейчас скажу крамолу - очень важно тестировать код! Поэтому в процессе разработки постоянно писалось тестовое консольное приложение. Оно позволило вычистить API, найти частично ошибки. Далее я решил залезть в чужую шкуру - стать пользователем своей же библиотеки и написать GUI приложение. И в процессе этой работы выловил еще несколько проблем. Например при уничтожении UA возникали racing conditions, связанные с отсутствием синхронизации между процессом уничтожения всех звонков и очисткой аккаунтов, или ошибки связанные с публикацией событий не в UI потоке, т.к. pjsip обрабатывает различные транзакции в различных потоках, то для упрощения клиентского кода, все менеджеры публикуют события через SyncronizationContext, буде таковой существует. Их как правило устанавливают GUI framework'и WinForms или WPF.

Conclusion
Возможно вы заметили, что я покрыл не все части pjsua API и не все возможности уже покрытых групп реализованы - это потому что моя обертка все еще в разработке!
Надеюсь чтение про мои страдания было интересным и поучительным.

PS: Код будет выложен позже.

Всем удачи!

понедельник, 16 марта 2009 г.

Initializable pattern

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

  1. Ввести метод Initialize в каждом объекте, который нужно проинициаллизировать;
  2. Более "правильный" и следующий дизайну .Net framework - реализовать в классе ISupportInitialize;
  3. Пойти еще дальше и автоматизировать запуск и закрытие сессии инициализации;
Что значит автоматизировать запуск и закрытие сессии инициализации? Скажем, возложить эту обязанность на framerwork. Как это сделать? Об этом и заметка! Вспомните мой пост про удобную обертку над Monitor'ом, с помощью которой можно узнать есть ли в очереди на доступ к ресурсу заблокированные потоки? Так вот для пущего удобства и сохранения стиля выполнения блокировок с поддержкой языковых конструкций (скобочек =) ) там использовался .Net-специфичный паттерн Disposable. Почему бы не воспользоваться тем же приемом для обрамления сессии инициализации? Сказано, сделано. Прошу любить и жаловать - .Net-specific Initializable pattern. Состоит это решение из 2х частей:
  • собственно объект, который необходимо проинициаллизировать (реализует ISupportInitialize);
  • обертка, с помощью которой и происходит неявное управление сессией инициаллизации;
Давайте взглянем на эту обертку поближе:

Это маленькая обертка, которая при создании получает ссылку на инициаллизируемый объект и запускает сессию, вызовом метода ISupportInitialize.BeginInit(). И при детерминированном освобождении закрывает сессию вызовом метода ISupportInitialize.EndInit(). Я намеренно не стал реализовывать в этом классе Disposable паттерн целиком с тем, чтобы сессия закрывалась только явно, либо вызовом Dispose() обертки, либо EndInit() самого объекта.

Вот как может выглядеть API:



и использование его в коде:



Easy-peasy
!
А вот как выглядел бы код без Initializer:

мое чувство прекрасного плачет =).

Все это конечно не отменяет проверок на наличие сессии в самом объекте, т.к. он должен разрешать редактировать себя только на протяжение последней, а в остальном мне кажется несколько упрощает код, избавляя его от 2х строчек =), зато визуально ограничивает рамки сессии, что на мой взгляд весьма удобно и лаконично.

UPD: поступила вполне обоснованная критика - трудно читаемый код, тяжело разобраться зачем создавать аккаунт и сразу же "диспозить" его. На это могу лишь ответить, что API в данном случае (и вообще, если применяется этот паттерн) должно подсказывать ЧТО оно возвращает, другими словами метод называется не AddAccount, a InitializeAndAddAccount. А еще лучше если бы он назывался CreateAccountInitializer =).

UPD#2:
Придумал как избавиться от неразберихи и нарушения принципов, сохранив красоту идеи.

Не надо делать никаких "conventions" в API. Надо дать возможность желающим писать так как они хотят и привыкли, а извращенцам вроде меня дать возможность "юзать" такие "извраты":


и пример:



как видите API ничуть не пострадало и осталось читабельным и ясным, и ничьи принципы не нарушены.


PS: И еще одно дополнение. Вся эта возня вокруг Disposable паттерна - это ничто иное как smart pointer для .Net! Те из вас, кто писал на С++ поймут о чем я.

Удачи!

понедельник, 2 февраля 2009 г.

Much of muchness. Other readings.

This is a short note on other possible solutions of forementioned scenarios that these series of articles were devoted to.

Actually there is only one writer that had discovered the same theme. His name is Kent Boogaart. You might know him for his SCSF.WPF Contrib project, that I for one actively use in my projects.

Discovering MAF-enabled solution he breakes it to following pieces:

The most interesting parts here are Event Hub and Service Provider implementations.
Kent's techinque is fully based on MAF advantages (it means he doesn't use any other .Net libraries as I did with WCF in my EventBroker implementation) finishing with the fully independent addins and hosts, that is not true in my case, where host and addin are coupled by service contracts and service provider libraries.

P.S. other articles in this series
P.P.S. Actually there is nothing preventing you from generating proxies to subscription and publication services in addin's and host's projects separately, so there is only one dependency left - my ServiceProvider library. Not a big deal =)

Good luck!

вторник, 27 января 2009 г.

Much of muchness. Part 6. XAppDomain EventBroker.

Алилуйя! Мы добрались до заключительной (и самой интересной) части.
Здесь я расскажу, свой подход к реализации кросс-доменного EventAgregator'а.

Необходимость создания этого монстра диктует плагинная архитектура - различные изолированные модули, ничего не знающие об остальных плагинах, должны уметь оповещать окружающих о каком-то важном событии, причем получатели сообщения, обязательно будут жить в другом, изолированном домене.
Фундаментом моей платформы является CAB - composite application block, содержащим реализацию EventAggregator'а. Как сделать так, чтобы он не был ограничен одним доменом? Никак. Дело в том, что в CAB композиты публикуют события и подписываются на них при помощи ObjectBuilder pipeline, в котором специальная стратегия EventAggregator'а аккумулирует всю эту информацию. В нашем же случае все объекты плагинов будут создаваться MAF pipeline'ом, и доступа к ним у нас не будет. Так как же быть? Использовать WCF - Windows Communication Foundation.

В WCF из коробки нет реализации механизма Publish-Subscribe, но есть все средства для его реализации. Подходов может быть несколько:

За основу решения я взял последний framework от Juval Lowy и расширил его, сделав его contract&topic based, с тем, чтобы интегрировать его с CAB EventAggregator (topic based). Но обо всем по порядку.

Publish-Subscribe framework (С) Juval Lowy IDesign.

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

Прежде всего небольшим изменениям подвергся контракт сервиса подписки. Теперь ему передается не имя метода, которое надо вызвать у подписчика, а некий объект, выражающий интерес в подписке, который содержит имя метода (contract based) и имя topic (topic based), по которым EventAggregator сможет определить заинтересованность того или иного подписчика в неком событии.





Интерфейс сервиса подписки кросс-доменного EventAggregator'а для моей платформы выглядит следующим образом:

основное его назначение - сообщать WCF инфраструктуре о том, что IEventNotifyCallback - это callback contract для нашего кросс-доменного EventAggregator'а. DomainDTO - это суперкласс для моих DTO.

Сам сервис реализован в классе XAppDomainEventBrokerSubscription.

Он выполняет функции сервиса подписки и связывает инфраструктуру CAB с нашим механизмом. Последнее осуществляется аналогичным CAB'овскому образом - поиск публикаторов событий (по соответствующим аттрибутам) и аккумуляция этих публикаторов в CAB EventAggregator с одной лишь разницой, что вместо подписчика подставляется суррогатный объект, который при получении события опубликует его в нашей кросс-доменной системе (фактически просто пробросит дальше). Интегрируется этот сервис в CAB при помощи специальной стратегии ObjectBuilder'а XAppDomainEventBrokerStrategy.


Публикацией ведает XAppDomainEventBrokerPublication - наследник слегка измененного PublicationService сервиса из framework'а Juval Lowy. Первый реализует callback контракт как контракт сервиса, а последний на основании вызываемого метода и topic находит и вызывает соответствующий метод у соответствующего подписчика.


Но возникает резонный вопрос - а где, собственно, этому сервису передается topic? И куда его вообще воткнуть, если это contract-based framework? Ответ прост. Засунуть в заголовок
сообщения и занимаются этим клиенты сервиса. Их только надо унаследовать от PublisherServiceClient - часть моей инфраструктуры WCF (может быть опишу попозже).


Обратите внимание - у базового клиента сервиса подписки есть свойство EventTopic, с помощью которого мы можем передать это значение в WCF pipeline через расширение канала EventTopicChannelExtension, с одним единственным свойством - EventTopic.
Помните суррогатные объекты, добавлявшиеся в EventBroker CAB? Так вот они публикуют события через EventPublisherClient, передавая ему значение EventTopic из аттрибута.
Как EventTopicChannelExtension попал в канал? Все тот же базовый клиент при создании фабрики каналов добавляет EventTopicFlowBehavior, выполняющий всю работу.




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



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

Вот и все! Теперь все, что нужно сделать по шагам:
1. Создать прокси к сервису подписки и подписаться на интересуемое событие, указав при желании метод контракта, который следует вызвать.
2. Создать прокси к сервису публикации и вызвать, мимикрирующий под контракт подписчика сервис публикации, передав при желании topic.

Не очень надеюсь вы не запутались, потому что сам запутывался не раз.

P.S. ссылки на предыдущие посты в этой серии
Удачи!