понедельник, января 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, о способах его использования, планах развития и прочих вопросах я буду писать именно в блоге проекта.

15 комментариев:

Mike Chaliy комментирует...

Класно, на блог подписался, вечером погляжу. Напишите в темке про воркфлоу.

Анонимный комментирует...

+1

gandjustas комментирует...

Идея очень понравилась, а реализация...
Короче какашками покидаюсь.
1)Слегка не .NETовский стиль быблиотеки. Немаленькие XML конфиги, не интегрированные со стандартным конфигом. Полное отсутствие средств конфигурации в коде. Это сильно снижает заявленную "легковесность".
Больше похоже на Java.

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

3)очень хотел увидеть хороший фреймворк для создания statechart, но увидел только фреймфорк для изменения одного свойства объекта. И совсем непонятно что делать если есть два свойства, которые надо менять.

4)Жутко не радуют параметры string entityType при наличии generic-параметров.

Sergey Rozovik комментирует...

to gandjustas
Да, документации нет пока, но я постараюсь это исправить.
1. Можно работать полностью без конфигов. Как, конфигурировать в коде напишу в ближайшее время.
2. На описанием работаю
3. Для transition можно задавать Handler и в нем выполнять любую бизнес логику.
4. string EntityType нужен для того, чтобы одному классу привязать несколько workflow. По умолчанию там будет Type.FullName

В-общем, я понял, дело за хорошим описанием.

gandjustas комментирует...

Еще немного "покурил" исходники. Нашло озарение: зачем вообще привязка statechart-а к какой либо сущности? Такая привязка дает достаточно большое ограничение на использование, а гибкости очень мало. В текущей реализации привязка идет к строковому полю или свойству.

Если просто выставить у event на изменение текущего состояния, то код станет гораздо проще, гибче, и использовать его станет не намного сложнее.

Мысль по авторизации. Вместо IRoleProvider использовать IPrincipal. Объект с таким интерфейсом можно получить из HttpContext.User и из Thread.CurrentPrincipal.

Sergey Rozovik комментирует...

> В текущей реализации привязка идет к строковому полю или свойству.
свойство или поле типа string, int или enum.

Если же реализовать свой IWorkflowFactory то можно делать людой маппинг, в т.ч. и на несколько свойств.

> Если просто выставить у event на изменение текущего состояния,

Интересно, надо подумать.

> Мысль по авторизации. Вместо IRoleProvider использовать IPrincipal.

Первоначально так и было. Потом появился generic интерфейс IRoleProvider. Дело в том, что очень часто нужны роли в контексте конкретного объекта, напр. роль владельца документа.

gandjustas комментирует...

> Если же реализовать свой IWorkflowFactory то можно делать людой маппинг, в т.ч. и на несколько свойств.

Ну а если вообще не нужно привязывать к какому-либо объекту?
В IWorkflowAdapter такая привязка уже есть, отказаться от нее нельзя.


>> Мысль по авторизации. Вместо IRoleProvider использовать IPrincipal.

> Первоначально так и было. Потом появился generic интерфейс IRoleProvider. Дело в том, что очень часто нужны роли в контексте конкретного объекта, напр. роль владельца документа.

Так никто не мешает реализовать свой IPrincipal, его интерфейс делает тоже самое что IRoleProvider.
Кроме того, для кастомизации процесса авторизации можно выставить event.

Sergey Rozovik комментирует...

> Ну а если вообще не нужно привязывать к какому-либо объекту?

Диаграма состояний описывает возможные состояния и переходы. Объект хранит состояние. Без объекта где хранить состояние? Понадобится свое хранилище... и задравствуй WW #2 :)

> Так никто не мешает реализовать свой IPrincipal, его интерфейс делает тоже самое что IRoleProvider.
Кроме того, для кастомизации процесса авторизации можно выставить event.


Внутри Listma есть IRoleProvider который использует текущий IPrincipal. Он используется по умолчанию. Поэтому вариант со своим прнципалом работает :)

Вариант с event был отброшен, потому что часто возникает необходимость использовать отдельный алгоритм определения ролей для каждого типа бизнес-объекта

gandjustas комментирует...

> Диаграма состояний описывает возможные состояния и переходы. Объект хранит состояние. Без объекта где хранить состояние? Понадобится свое хранилище... и задравствуй WW #2 :)

Каждое состояние задается ID. Этого достаточно чтобы хранить текущее состояние.

>Внутри Listma есть IRoleProvider который использует текущий IPrincipal. Он используется по умолчанию. Поэтому вариант со своим прнципалом работает :)

В веб-приложениях чаще тербуется использовать HttpContext.User, в не Thread.CurrentPrincipal, зачем в таком случае создавать еще один наследник IRoleProvider?
Можно создать GenericRoleProvider, который принимает на входе IPrincipal, а для него вызывает IsInRole. Но в таком случае проще самому workflow передавать IPrincipal, а не IRoleProvider.
Всю кастомизацию можно возложить на реализацию IPrincipal.

>Вариант с event был отброшен, потому что часто возникает необходимость использовать отдельный алгоритм определения ролей для каждого типа бизнес-объекта
Так вариант с event-ом самый гибкий, поволяет вообще любою логику подключить к конкретному инстансу workflow без модификации кода библиотеки и создания новых классов.

Я имел ввиду передавать IPrincipal и сделать event Authorize, в котором обрабатывать авторизацию на каждый переход.

Nikita Govorov комментирует...

При прочтении описания напоминает Simple State Machine(
http://www.codeplex.com/SimpleStateMachine) + метаданные для ui и security.

Sergey Rozovik комментирует...

to Nikita Govorov
Да, действительно похоже. Подход аналогичный, но в Simple State Machine, похоже совсем нет поддержки ролей. А в UI с ролями практически всегда заморочки.

Nikita Govorov комментирует...

Как я понимаю, у Вас так же имеются данные о том какие элементы управления(т.е. их идентификаторы) (не)доступны((не)видим и (не)активен) определенным ролям в определенных состояниях и каким ролям разрешены переходы из одного состояния в другое.
Такой функционал достаточно просто встраивается в Simple State Machine.

Sergey Rozovik комментирует...

> Такой функционал достаточно просто встраивается в Simple State Machine
Я бы не сказал это это "достаточно просто" там сделать.

aloneguid комментирует...

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

Sergey Rozovik комментирует...

to aloneguid
> Почему не взять готовое, тот же WWF? :)

Так и возьмите! Когда намучаетесь, приходите, еще поговорим :)

Дело в том, что Listma решает те задачи, для которых WWF, как бы это сказать, тяжеловат слегка.
Во вторых - WF (он уже не WWF) откровенно неудачная конструкция. Если хотя бы, при всей своей тяжеловесности и неуклюжести WF поддерживал индустриальные стандарты в области workflow, так и этого нет.

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