Правила построения иерархической бизнес-логики (бизнес-процессов)
Для построения бизнес-логики используется иерархическая модель функциональных элементов. То есть базовым элементом бизнес-логики является функция - компонент, которая реализована с помощью объекта класса Command с методом Exec, это типичный шаблон проектирования Command
Имеется четыре базовых класса Command, Operation, Process, Application
- Command представляет собой логику, которая выполняется последовательно и непрерывно на стороне сервера
- Operation представляет собой частный случай Command и является интерактивной командой, которая взаимодействует
с клиентом (под клиентом понимается любое клиентское приложение) - Process представляет собой частный случай операции и является долгоиграющей операцией, которая время от времени
сохраняет свое текущее состояние на сервере независимо от времени жизни клиентского приложения. Process
имеет набор состояний и к каждому состоянию привязывается определенный набор операций, доступных в этом
состоянии, в том числе в качестве операции процесса может выступать другой процесс, в результате чего он
становится подпроцессом. - Application представляет собой частный случай процесса и является синглетоном в разрезе клиентов. То есть
каждый клиент (клиентское приложение) имеет экземпляр приложения, причем единственный. При этом Application
является корнем приложения. Его точкой входа и выхода.
Таким образом бизнес-логика строится в виде дерева с корнем Application, набором состояний приложения и
вложенными операциями, привязанными к этим состояниям. Дерево имеет неограниченное количество уровней.
Рассмотрим в качестве примера площадку для проведения аукционов, реализованную в виде WEB-приложения
Попасть на площадку может любой пользователь, в том числе не зарегистрированный на площадке. Тем не менее,
такой пользователь имеет доступ к некоторому функционалу:
- Посмотреть описание площадки
- Прочитать новости площадки
- Посмотреть список торгов
- Залогиниться (если есть аккаунт)
- Зарегистрироваться (если нет аккаунта)
Все эти операции доступны для приложения в состоянии New, которое означает, то процесс запущен, но еще не имеет
сохраненного экземпляра в базе данных (или еще не инициализирован экземпляр).
Оформим иерархию
- New
- Description- News- AuctionList- Login- Registration
Жирным цветом обозначены процессы, красным цветом состояния, остальное операции.
Application - экземпляр класса Application (процесс - синглетон)
Login - операция, позволяющая пользователю авторизоваться на площадке
Тогда он должен ввести логин и пароль на главной странице и вызвать операцию Login
http://www.ets24.ru/index.php?op=Login
Операция проверит в базе наличие аккаунта и соответствие пароля, инициализирует процесс (Application)
и вернет его состояние: Registered.
- New
- Description- News- AuctionList- Login- Registration
- Registered
- Description- News- AuctionList- Logout- Cabinet- OrganizeAuction
они не зависят от состояния приложения. Такая возможность у процессов тоже есть. То есть в процессе можно
прописывать операции, зависимые от контекста (привязанные к состоянию), и не зависимые от контекста, которые
можно выполнить в любом состоянии процесса, главное, чтобы доступ к самому процессу был.
С учетом вышеизложенной информации можно перестроить дерево бизнес-логики
- Description- News- AuctionList
- New
- Login- Registration
- Registered
- Logout- Cabinet- OrganizeAuction
- Registration
- New
- CreateRequest
- Template
- ViewRequest
- EditRequest- PrintRequest- DeleteRequest- SignRequest- SendRequest
- Sent
- ViewRequest- PrintRequest- RecallRequest
- Description
- News
- AuctionList
- New
- Login
- Registration- New- CreateRequest- Template- ViewRequest- EditRequest- PrintRequest- DeleteRequest- SignRequest- SendRequest- Sent- ViewRequest- PrintRequest- RecallRequest
- Registered
- Logout- Cabinet- OrganizeAuction
- Синхронный запуск - означает, что процесс, внутри которого синхронно запущен дочерний процесс, становится
недоступным пока свою работу не завершит дочерний процесс - Асинхронный запуск - означает, что родительский процесс не ждет завершения дочернего и дает доступ к запуску
других подпроцессов. - Инициирующий запуск - означает запуск зависимого подпроцесса, предназначенного для другого субъекта, например,
запуск процесса проверки заявки на регистрацию, предназначенного для оператора. Инициирующий запуск может быть
как синхронным, так и асинхронным. - Множественный запуск - означает, что можно запустить несколько вложенных процессов одного и того же типа, иначе
повторный запуск подпроцесса будет приводить к доступу уже имеющегося подпроцессу.
процесс. Никто другой не может получить доступ к процессу.
Система разграничения прав доступа при таком построении бизне-логики приобретает новый смысл
Application находится в состоянии Registered
op=WorkSpace напрямую ни к чему не приведёт (кроме сообщения об отсутствии доступа), т.к. внутри Application
такой операции нет, и она появляется, только когда инициализируется операция Cabinet
И соответственно вызвать команды AddPanel и RemovePanel, которые зарегистрированы в операции WorkSpace также не
получится, пока не зайдешь в WorkSpace
его тип и идентификатор, например
op=Registration&OID=8253
При этом на стороне сервера происходит проверка принадлежности процесса вызывающему собъекту (по полю Subject в
процессе).
и является синглетоном), его идентификатор и имя вложенного процесса, например
op=Registration/SendRequest&OID=8253
При этом запуск подпроцесса SendRequest будет возможен только в том случае, если процесс Registration с
идентификатором OID=8253 принадлежит пользователю и находится в состоянии, в котором доступен подпроцесс
В итоге имеем одновременно способ описания бизнес-логики и средство разграничения прав доступа.
где ключ - название параметра, а value - значение параметра.
закрывает транзакцию или откатывает в зависимости от результата выполненной логики
public $OnExec;
public $Error;
function __construct($parent=null)
{
parent::__construct($parent);
}
public function Exec($params)
{
try
{
$this->Error = null;
$this->DBManager->BeginTran();
if($this->OnExec)
$this->OnExec->Exec($params);
$this->DBManager->CommitTran();
}catch (\Exception $e)
{
$this->DBManager->RollbackTran();$this->Error = $e->getMessage();
}
}
class SendMail extends Command
public $Receiver;public $Theme;public $Header;public $Body;function __construct($parent=null){
parent::__construct($parent);
}public function Exec($params) {
$mailOID = $this->DBManager->GetOID('Mail.System');$mail = $this->DBManager->CreateObject($mailOID);$mail->Subject($this->Theme);$mail->To($this->Receiver);$mail->body = $this->Body;$mail->Send();
}
Операция регламентирует бизнес-логику более жестко. Основным также является метод Exec
Но в наследниках он не перекрывается. Метод Exec выглядит так:
{
try{
// Запоминаем параметры вызова$this->Params = $params;// Инициализируем вложенные операции$this->Init();$op = null;if(isset($this->Params['op']))
$op = $this->Params['op'];
if($op){
$root = $this->GetRootPath($op);$last = $this->GetLastPath($op);if($root){
$params['op'] = $last;$this->Active = $this->GetChild($root);if(!isset($this->Active))
throw new \Exception('Operation '.$root.' not found');
$this->Active->Exec($params);
}
}else
$this->ExecDefault(); // Выполняем логику по-умолчанию
$this->Run();
}catch(\Exception $e){
$this->HandleError($e->getMessage());
}
Сначала идёт вызов виртуального метода Init, который регистрирует вложенные операции и команды, которые
как раз и будут доступны внутри данной операции, пример реализации метода Init
$this->AddChild('WorkSpace', new WorkSpace($this));$this->AddChild('PrivateData', new PrivateData($this));$this->AddChild('MyObjects', new MyObjects($this));
Этот метод используется в личном кабинете и определяет какие страницы в личном кабинете доступны
Поскольку вложенных операций может быть достаточно много, а каждую операцию нужно скомпилировать,
и при этом в каждом сеансе будет выбрана лишь одна из них, то логичнее регистрировать не сами
операции, а проксиобъекты - команды, которые уже будут создавать операции, например
protected function Init()
$this->AddChild('WorkSpace', new ProxyOperation($this, 'WorkSpace'));$this->AddChild('PrivateData', new PrivateData($this));$this->AddChild('MyObjects', new MyObjects($this));
Это уже оптимизация быстродействия
Затем в методе Exec операции определяется подоперация, если она указана. Смотрим параметр op.
К слову сказать, в $params сидит $_GET. В каждую операцию в параметре op приходит строка, в которой
уже вырезана рутовая часть, относящаяся к текущей операции, т.е. в операции Application строка op
выглядит так op=Cabinet/WorkSpace, а в операции Cabinet строка op выглядит так: op=WorkSpace
Поэтому следующий код как раз и занимается тем, что вычленяет дальнейший путь, ищет вложенную
среди зарегистриргованных вышеуказанным методом Init и если находит, то передаёт ей управление,
обрезав корневую часть строки. Если не находит, выдает ошибку. Если уже была достигнута последняя
точка пути, то выполняется логика, определенная по-умолчанию. То есть вызывается метод ExecDefault
Затем выполняется метод Run, в котором прописывается логика которую необходимо выполнить независимо
от вариаций выбранного пути.
И если всё же произошла ошибка, то выполняем метод HandleError, передав ему текст ошибки.
Process еще более жёстко регламентирует логику, настолько жестко, что создавать наследники не нужно
Доступные операции и состояния прописываются в базе данных, например
exec RegisterOperation 'Registration', 'Регистрация'
exec RegisterOperation 'Login', 'Вход'
exec RegisterOperation 'Logout', 'Выход'
exec RegisterOperation 'EstateAdd', 'Добавить объект'
exec RegisterOperation 'EstateEdit', 'Изменить параметры объекта'
exec RegisterOperation 'ExpositionExpose', 'Опубликовать объект'
exec RegisterOperation 'ChangeProfile', 'Изменить профиль'
exec RegisterOperation 'Cabinet', 'Личный кабинет'
go
-- Процессы
exec RegisterProcess 'Application', 'Приложение'
exec RegisterProcess 'EstateProcess', 'Недвижимость'
exec RegisterProcess 'Exposition', 'Публикация'
go
-- Операции процессов
exec RegisterProcessOperation 'Application', 'New', 'Login', 'Login'
go
exec RegisterProcessOperation 'Application', 'New', 'Registration', 'Registration'
exec RegisterProcessOperation 'Application', 'Registered', 'Cabinet', 'Cabinet', 1
exec RegisterProcessOperation 'Application', 'Registered', 'Estate', 'EstateProcess'
exec RegisterProcessOperation 'Application', 'Registered', 'Logout', 'Logout'
exec RegisterProcessOperation 'Application', 'Registered', 'ChangeProfile', 'ChangeProfile'
go
exec RegisterProcessOperation 'Exposition', 'New', 'Expose', 'ExpositionExpose', 1
go
exec RegisterProcessOperation 'EstateProcess', 'New', 'Add', 'EstateAdd', 1
exec RegisterProcessOperation 'EstateProcess', 'Template', 'Expose', 'Exposition', 1
exec RegisterProcessOperation 'EstateProcess', 'Template', 'Edit', 'EstateEdit'
go
Сначала регистрируются операции, затем процессы, затем операции привязываются к состояниям процессов.
Таким метод Init процесса обращается к базе данных, читает настройки и регистрирует доступными операции,
причем не все, а только те, что привязаны к текущему состоянию. Состояние определяется по переданному
OID процесса. Если OID не передан, то значит это новый процесс и состояние = New
Как уже сообщалось, мы не перекрываем класс Process в слое бизнес-логики, исключение составляет только
класс Application. Он инициализируется без OID, по связи с текущим пользователем и отрисовку страницы
выполняет самостоятельно, в отличие от процесса, который сформированную страницу возвращает вызвавшей его
функции. Applicationу возвращать страницу уже некуда, поэтому он ее эхает.
Как необходимо разрабатывать новые прикладные классы бизнес-логики. Посколько Application и Process являются
самодостаточными, то остаётся разработка операций и команд. Про разработку команд выше уже сообщалось. Нужно
просто перекрывать метод Exec, поэтому рассмотрим подробно разработку операций. Сделаем это на примере админки.
Создадим операцию Administration. Прикрепим ее к Application в состоянии Registered. Доступ к ней должен получать
только администратор системы. Для этого есть два способа:
1. В методе Init вызывать хранимую процедуру, которая будет проверять является ли пользователь администратором
и если нет, то создавать исключение.
(IsAdministrator). Тогда все, кто не админ, не будут получать доступ к операции
'Administration', -- системное имя
'Администрирование' -- пользовательское имя
'Application', -- процесс, к которому привязываем'Registered', -- состояние, к которому привязываем'Admin', -- алиас, под которым операция будет сидет в процессе'Administration' -- класс операции (системное имя)
protected function Init()
{
// хранимка проверяет принадлежность текущего пользователя к роли$sp = new \System\StoredProc('User_CheckRole');$sp->AddParam('@Role', DB_STRING, 'Admin');$sp->AddParam('@Result, DB_BOOL, false, output);$sp->Execute();if(!$sp->GetParam('@Result')->Value)
throw new Exception('Доступ к администрированию разрешен только администратору');
// Регистрируем дочернюю операцию$this->AddChild('Object', new Object($this));
Дочерняя операция Object предоставляет доступ к любому действию любого объекта, её нужно реализовать
Но это будет дальше, а сейчас продолжим реализовывать операцию Administration
Что должна делать данная операция по-умолчанию? Наверное, выводить список всех классов.
Перекроем метод ExecDefault.
protected function ExecDefault()
{
$this->Active = new Object($this);$params['op'] = 'List';$params['class'] = 'Class';$this->Active->Exec($params);
То есть по-умолчанию будет вызываться операция Object, которая будет вызывать действие объекта List
А класс объекта - Class, то есть выводим список объектов типа Class.
Теперь нужно перекрыть метод Page, который будет выводить html на экран
public function Page()
{
return $this->Active->Page();
Предполагаем, что страницу всегда будет формировать вложенная операция.
Посколько иерархия бизнес-логики это один из слоев MVC, то вторые два это слой бизнес-сущностей и интерфейса.
Как устроен слой Бизнес-сущностей вы уже знаете. Это иерархия классов бизнес-сущностей, и в каждой
зарегистрирован набор действий Edit, Create, View, Delete, Print и т.д.
Это значит, что метод Init операции Object, должен создать экземпляр класса бизнес-сущности и получить у него
список действий. Базовое действие Action является наследником операции. А метод Exec должен выболнить это
действие.
Таким образом команда вида
op=Admin/Object/Edit&OID=1234
Создасть операцию Admisitration (если есть доступ), затем создаст опреацию Object, которая создаст сущность,
найденную по параметру OID и выполнит его действие Edit.
Для создания нового объекта или вызова действия класса, нужно указывать не OID, а Class, например
op=Admin/Object/Create&class=Notice
или
op=Admin/Object/List&class=Auction