Каждый уважающий себя техлид \ архитектор ПО \ руководитель разработки, должен написать в своей жизни хотя бы одну CRM
народная мудрость
Всем привет! Меня зовут Михаил я техлид в компании ДомКлик. Сегодня я хочу поговорить про автоматизацию бизнес-процессов. У нас есть объекты, граф состояний \ набор статусов и в каждый момент времени объект находится в одном из возможных состояний. Это позволяет описать workflow или конечный автомат для рассматриваемого процесса и строить сервис автоматизации на этой абстракции.
В основе многих сервисов, которые мы используем в повседневной жизни, лежат процессы которые можно описать с помощью этих абстракций - это покупки в интернете, еда, такси, CRM, ERP, ...
Рассмотрим для примера, процесс оформления и доставки некоторого заказа.
Описание объекта
class Order: status responsible price paid
I
WF_STATUSES = (NEW, ORDERED, RESERVED, CANCELLED, RETURNED, PAID, SHIPPED, DELIVERED, COMPLETED,)
Borland Developer Studio, ODBC, все как положено, на дворе 2006 год.. именно тогда мне довелось поработать над первой в своей жизни CRM. Человеческая психика так устроена, что все плохое вытесняет и замещает, поэтому, знакомясь с очередной реализацией workflow или создавая проект с нуля, я старался найти ту самую серебряную пулю - общий подход, который будет наиболее удобен в использовании, интуитивно понятен и эффективен. За время своей работы у меня скопилась хорошая подборка решений из серии, как не надо делать, но удалось выработать и кое-что полезное.
Как не надо делать
def set_ordered(self, request): ... object = self.get_object() object.status = ORDERED object.save() ...
Наиболее неудачное решение, это размазывание, по коду программы, всей логики движения объекта по workflow. Мы изменяем состояние объекта в API-handlers, сигналах, триггерах, методах класса, везде где только можно. При таком подходе нет общего понимания процесса, вносить изменения крайне сложно.
class Order: ... def set_ordered(self): pass def set_reserved(self): pass
Достаточно удобно, реализовать workflow через класс, где для перехода в каждый статус будет своя функция. Мне этот вариант не нравится тем, что, часто есть общий подход к смене статуса - какие-то общие действия, которые должны быть в каждой функции, например:
валидация состояния объекта
назначение ответственного
логирование смены статуса
Кроме того, поддержка и развитие такого workflow в нашем изменчивом мире, так же создаст проблемы. Например включение новых шагов в процесс, потребует заново оценить все места в проекте, где происходит смена состояния объекта. Кроме того, логика workflow утекает между пальцев и нам снова не понятно, какой статус идет за каким. Чтобы разобраться, потребуется глубокое понимание проекта.
К чему мы пришли
Итак, у нас есть объект - заказ в интернет магазине и статусная модель, описывающая процесс покупки товара. Вначале мы определили варианты для манипуляций с объектом, мы можем
двигать объект по позитивному сценарию DIR_NEXT,
двигать в альтернативные ветки DIR_FAIL, DIR_WAIT, DIR_RETURN,
отменять обработку объекта DIR_CANCEL,
завершить обработку объекта DIR_COMPLETE.
DIRECTIONS = (DIR_NEXT, DIR_FAIL, DIR_RETURN, DIR_WAIT, DIR_CANCEL, DIR_COMPLETE,)
Далее, чтобы получить наглядное представление о процессе, его нужно описать. Мы выбрали JSON-схему, это хорошая отправная точка для построения визуального представления процесса. Кроме того схема процесса всегда есть в коде под рукой, чтобы вспомнить что за чем идет.
Workflow процесса
ORDER_WORKFLOW = { NEW: { DIR_NEXT: ORDERED, 'responsible': AUTHOR, }, ORDERED: { DIR_NEXT: RESERVED, DIR_RETURN: RETURNED, DIR_CANCEL: CANCELLED, 'responsible': MANAGER, }, RESERVED: { DIR_NEXT: PAID, DIR_CANCEL: CANCELLED, }, PAID: { DIR_NEXT: SHIPPED, 'notify_manager': True, 'responsible': STOREKEEPER, }, SHIPPED: { DIR_NEXT: DELIVERED, 'notify_client': True, 'responsible': DRIVER, }, DELIVERED: { DIR_NEXT: COMPLETED, DIR_CANCEL: CANCELLED, }, COMPLETED: { 'notify_manager': True, 'finished': True, }, RETURNED: { DIR_NEXT: ORDERED, DIR_CANCEL: CANCELLED, }, CANCELLED: { 'notify_manager': True, 'finished': True, }, }

Собственно реализацию workflow делаем через класс. При инициализации связываем объект с instance Workflow и все манипуляции со сменой состояния \ статуса объекта делаем через этот класс. Интерфейс работы с workflow имеет следующий вид:
у нас есть метод get_state для получения состояния объекта, которое включает в себя доступный набор переходов и необходимую информацию для отображения объекта,
есть метод step, который обеспечивает смену состояния объекта с учетом доступных переходов.
Реализация workflow
class Workflow: def __init__(self, order, workflow): self.order = order self.workflow = workflow def get_state(self): """ получить состояние заявки в workflow - возможные перех��ды - finished true | false - какая-то дополнительная информация, описывающая состояние заявки в рамках процесса """ order = self.order stage = self.workflow[task.status] state = { 'status': order.status, # actual status 'finished': stage.get('finished', False), 'directions': tuple(), } for direction in DIRECTIONS: dir_status = stage.get(direction) if dir_status: state['directions'] += (direction, dir_status), return state def _step_assert(self, task, direction, user): assert task.status in self.workflow, 'wrong workflow status' assert direction in DIRECTIONS, 'wrong direction' def get_direction(self, stage, direction): return stage.get(direction) def step(self, direction=DIR_NEXT, **kwargs): """ перемещение заявки в следующий возможный статус в рамках workflow :param direction: :return: moved - true | false, int_code, text_reason """ order = self.order user = get_current_user() self._step_assert(order, direction, user) stage = self.workflow[order.status] if stage.get('finished'): return False, 2, 'Обработка заявки завершена' next_status = self.get_direction(stage, direction) if next_status: next_stage = self.workflow[next_status] notify_manager = next_stage.get('notify_manager') notify_client = next_stage.get('notify_client') if notify_manager: self.notify_manager(order) if notify_client: self.notify_client(order) order.set_status(next_status) if 'responsible' in next_stage: order.responsible = self.set_responsible( order, next_stage['responsible'] ) order.save() return True, 0, 'Переход произведен' return False, 1, 'Переход не был произведен' @staticmethod def notify_manager(order): raise NotImplemented @staticmethod def notify_client(order): raise NotImplemented @staticmethod def set_responsible(order, role): raise NotImplemented class OrderWorkflow(Workflow): """ Order Workflow """ def __init__(self, order): super().__init__(order, ORDER_WORKFLOW) @staticmethod def set_responsible(order, role): return order.set_responsible(role=role)
Данная реализация собирает всю логику процесса, внутри класса описывающего workflow. В нашем случае, у нас было несколько процессов, которые имеют разные статусные модели, но при этом мы смогли использовать общий движок для изменения состояний сущностей разного типа. Я считаю что, для этой реализации расширение статусной модели и внесение новой логики может проходить относительно безболезненно.

Добавление нового состояния сведется к обновлению собственно JSON-схемы. Если мы хотим добавить новую логику при смене состояний, это удобно сделать через навешивание новых флагов \ признаков в описание каждого состояния в схеме.
RESERVED: { DIR_NEXT: PAID, DIR_FAIL: CANCELED, 'log_status': True, },
Затем нужно прописать логику, те действия, которые мы хотим выполнить, при наличии этого флага в очередном состоянии объекта.
@staticmethod def log_status(order): pass
Заключение
Я описал наш подход к реализации workflow, который обеспечивает, по моему мнению
наглядное описание процесса в коде
удобство расширения логики обработки переходов по состояниям
удобство изменения схемы процесса и расширения описания процесса
Предложенный подход имеет свои сильные и слабые стороны. Возможно, он не подойдет для реализации любого процесса, но послужит хорошей отправной точкой для ваших проектов по автоматизации процессов. Спасибо что дочитали! Всем добра!