Привет, Хабр! Меня зовут Андрей, я ведущий разработчик в ООО "НЛМК-Информационные технологии", руковожу небольшой командой по разработке на 1С-Битрикс. Наша команда специализируется на разработке и развитии функционала корпоративного портала группы компаний НЛМК.
На практике мы сталкиваемся с самыми разными задачами: от вывода ленты новостей компании и простейших форм обратной связи до функционала оценки качества работы сотрудников и планировщиков рабочего времени некоторых отделов.
Однако, несмотря на все разнообразие решаемых нами вопросов, зачастую разрабатываемые нами сервисы можно собрать из одних и тех же функциональных блоков, своеобразных "кирпичиков".
Конечно, реализуемая бизнес-логика всегда разная, но в основе часто используются одни и те же элементы. Умение правильно их применить, настроить и "подружить" друг с другом существенно снижает затраты времени и труда, как на разработку, так и на поддержку функционала портала.
В рамках этой статьи мы хотим поделиться опытом использования бизнес-процессов Битрикс: рассказать про их преимущества, недостатки и особенности внедрения.
Практика показывает, что достаточно большая часть сервисов, реализуемых на корпоративном портале, обладают одними и теми же повторяющимися чертами, несмотря на то, что задачи в этих сервисах решаются самые разнообразные.
В основе таких сервисов всегда лежит заявка в том или ином виде.
Это может быть заявка на:
перевод документов с иностранного языка,
проведение оценки качества работы сотрудника,
фиксацию опасных действий/условий на предприятии,
участие в волонтёрской деятельности,
и многое-многое другое.
Конечно, в данном случае речь идет про заявку с технической точки зрения, пусть даже если в терминологии бизнеса она имеет какое-то иное название.
В каждом случае реализуется реестр таких заявок, форма их подачи и, что нас особенно
интересует, жизненный цикл этих заявок.
Из чего состоят заявки?
На этапе проектирования сервиса важно правильно определить его модель данных, задать для заявки набор полей/свойств, которые как-либо будут эту заявку характеризовать.
Поля заявки можно разделить на две основные категории:
поля с полезной бизнес-информацией,
технические поля для обработки заявки.
Поля первой категории хранят полезную информацию, которая должна быть обработана в рамках жизненного цикла заявки.
Например, это привязка к документу на иностранном языке для заявки на письменный перевод или ссылка на заполненную анкету на участие в волонтёрской деятельности. Или просто текстовое поле, хранящее вопрос к техподдержке портала.
Без этой информации заявка не будет иметь смысла, так как не будет нести никакую полезную нагрузку (ради обработки которой и разрабатывается непосредственно сам сервис).
Поля второй категории нужны для корректной обработки заявки сервисом.
Здесь в большинстве случаев достаточно использования следующего универсального набора:
уникальный идентификатор,
дата создания,
дата последнего изменения,
статус.
Я рекомендую хранить в отдельных полях даты создания/последнего изменения заявки во всех случаях, даже когда весь жизненный цикл заявки где-либо отдельно логируется.
Наличие этих дат позволяет в будущем реализовать возможность автоматического построения ряда отчётов по заявкам, а также упрощает разбор кейсов нештатной работы сервиса (опять же, даже в случаях, когда реализовано полноценное логирование).
Обязательно использование поля статуса, хранящее текущее состояние заявки. Пример стандартного списка допустимых значений этого поля: "черновик", "опубликована", "в работе", "на согласовании", "завершена".
В некоторых случаях есть смысл добавить к списку полей выше поле для "технического" наименования заявки, чтобы хранить в нём какую-либо полезную для разработчика информацию (такая информация может ускорить разбор нештатных кейсов).
Впрочем, в большинстве случаев, использование "технического" названия избыточно.
Как живут заявки?
От момента создания до завершения (помещения в архив) заявка проходит определенный путь. При этом выполняется последовательность некоторых действий, содержание которых может зависеть от набора факторов, как внутренних (свойства самой заявки), так и внешних (параметры системы на момент выполнения заявки).
Какой бы сложной и разветвленной не была логика жизненного цикла, в ней почти всегда будут встречаться следующие типовые действия:
изменение статуса заявки,
отправление заявки на согласование,
согласование/отклонение заявки,
уведомление ответственных лиц.
Заметим, что в случае разработки/поддержки действительно крупного корпоративного портала количество сервисов заявочного типа (т. е. сервисов, в центре которых будет список заявок) будет достаточно большим и возникнет потребность решать одни и те же задачи для разных сервисов.
Как приготовить сервис заявочного типа?
Обычно вся логика жизненного цикла заявок реализуется в рамках какого-то набора классов, разработанных специально для определенного сервиса.
В случае, если портал небольшой, это не приносит каких-либо неудобств. Сложности возникают позже, когда проект в ходе развития "обрастает" всё новыми и новыми сервисами.
В какой-то момент команда разработчиков сталкивается с ситуацией, когда снова приходится реализовать те же функциональные блоки, которые ранее на этом проекте были реализованы многократно в рамках аналогичных сервисов (движение заявки по жизненному циклу, согласования, уведомления и пр.).
Проблема в том, что просто так переиспользовать старый код зачастую нельзя, т. к. скорее всего ранее он был написан специально для другого функционала и содержит в себе множество зависимостей. Не будем также забывать, что в условиях коммерческой разработки трудозатраты и сроки на разработку нового сервиса строго ограничены и не позволяют производить рефакторинг ранее написанного кода (к тому же работающего). Обычно эти ограничения носят такой характер, что не позволяют даже с нуля написать какой-либо универсальный набор классов, на базе которого можно было бы успешно реализовывать новые сервисы заявочного типа.
Как показывает практика, если придерживаться подхода с классами, самое реальное, что можно в данной ситуации сделать – написать новый сервис тем же путём, что и раньше, взяв за основу (скопировав) уже готовые классы аналогичного сервиса. Проблема в том, что общее состояние проекта ухудшается при этом ещё сильнее, и кодовая база разрастается повторяющимися блоками.
Конечно, здесь можно возразить: почему сразу не реализовать какой-то универсальный набор классов, на основе которого в будущем можно успешно разрабатывать новые сервисы?
Но сразу возникает и другой вопрос: как часто вам приходится стоять у истоков разработки по настоящему крупных корпоративных порталов и, более того, реально влиять на принимаемые в ходе разработки решения?
И ещё проблема: даже если изначально был выбран этот путь, какой шанс, что получится разработать по настоящему универсальный набор классов, который можно будет использовать на протяжении всего жизненного цикла продукта?
Это далеко не такая простая задача, как это может показаться на первый взгляд. И не факт, что при старте разработки корпоративного портала найдутся ресурсы для решения задачи этим путём.
Помним также, что в будущем очень непросто будет найти ресурсы для рефакторинга сервиса – с точки зрения бизнеса сервис функционирует корректно – зачем вкладывать средства ещё и в какой-то непонятный рефакторинг?
Даже если не брать во внимание и дополнительные проблемы, которые почти наверняка будут этому рефакторингу сопутствовать: зона регресса рефакторинга и исторические
данные, потеря которых для бизнеса недопустима практически во всех случаях.
Подход с бизнес процессами
К счастью, есть подход, позволяющий решить все описанные выше вопросы: использовать бизнес-процессы из коробки 1С-Битрикс. Конечно, этот подход не идеален, не лишён некоторых минусов, однако он позволяет во многих случаях сильно упростить жизнь разработчика и ускорить время разработки функционала.
Ниже мы расскажем, про то, что такое бизнес-процессы от 1С-Битрикс, какими отличительными особенностями они обладают и сравним их с описанным ранее стандартным подходом на основе классов.
Также дадим несколько советов разработчикам по внедрению бизнес-процессов на практике.
Итак, бизнес-процессы от 1С-Битрикс позволяют автоматизировать повторяющиеся задачи и оптимизировать работу разработчиков.
Разработчики имеют возможность из коробки:
Создавать и настраивать бизнес-процессы для различных документов и проектов, таких как заявки на отпуск, согласование счетов, обработку заказов и т. д.
Определять процедуры, которые необходимо выполнить, порядок их выполнения,.
Управлять задачами в системе, контролировать выполнение назначенных задач.
Рассылать уведомления и отчеты о ходе выполнения бизнес-процессов ответственным сотрудникам.
В 1С-Битрикс есть два типа бизнес-процессов:
Последовательный бизнес-процесс – действия выполняются одно за другим от точки входа до точки выхода.
Бизнес-процесс со статусами — бизнес-процесс, не имеющий начала и конца, в процессе работы которого происходит переход из одного состояния (статуса) в другое с разделением прав доступа.
Последовательный бизнес-процесс обычно выбирается, если жизненный цикл заявки представляет собой простую последовательность действий.
В противном случае используется бизнес-процесс со статусами когда подразумевается, что заявка может находиться в различных статусах (состояниях), переходы между которыми осуществляются по определенным правилам.
Впрочем, сейчас не хотелось бы далее заниматься цитированием официальной документации, отмечу лишь, что на практике в большинстве случаев выбирается БП (бизнес-процесс) со статусами.
Детали реализации БП подробно описаны в учебных курсах от 1С-Битрикс, в данной статье расскажем в первую очередь о нашем опыте использования БП и поделимся некоторыми советами.
Здесь очень важно обратить внимание на то, что БП - это не просто набор готовых классов, которые можно использовать в своём коде. Битрикс предоставляет визуальный редактор БП, с помощью которого можно очень быстро, буквально за один рабочий день одному разработчику "нарисовать" весь жизненный цикл заявки!
Примечание: действия, выполняемые в рамках бизнес-процесса не ограничены набором "из коробки". Присутствует возможность подключения специального действия для выполнения кастомного блока кода.
Хозяйке на заметку:
Не стоит назначать ответственным за этап какого-либо одного сотрудника. В случае недоступности этого сотрудника (из-за отпуска или болезни) БП может надолго задержаться, ожидая согласования.
В случае увольнения этого сотрудника может вообще возникнуть проблема дальнейшего продвижения БП, что может привести к нарушениям процессов внутри компании.
Болезненной эта ситуация может стать в случае, когда на проекте отслеживаются действия сотрудников и служба ИБ запрещает авторизацию под чужой учётной записью.
Правильный подход - ставить ответственными за этап сразу несколько сотрудников.
Лучший вариант - когда для сервиса, в рамках которого был реализован БП, на этапе формирования ролевой политики выделяется отдельная роль администратора сервиса. И администратор сервиса наделяется правом согласования/отклонения этапов БП взамен ответственных лиц в случае их незапланированного отсутствия.
Визуальный конструктор бизнес-процессов позволяет быстро вносить изменения в логику процессов, корректировать набор согласующих для конкретного этапа жизненного цикла заявки, добавлять рассылку уведомлений и т. д.
Но у этой скорости есть и обратная сторона.
На любом серьёзном проекте кроме продуктивной среды есть несколько площадок разработки и тестирования. Это, как минимум, одна тестовая площадка (иногда добавляется ещё и препрод), а также набор песочниц разработчиков (даже если эти песочницы развёрнуты разработчиками локально).
С введением в рабочий процесс хотя бы одной дополнительной площадки сразу возникает дополнительная потребность переноса разработок по задачам с одной площадки на все остальные (естественно, в рамках жизненного цикла задач, принятого в конкретной компании).
С переносом изменений в коде проблем нет - здесь поможет система контроля версий, тот же git.
Перенести изменения в БД также несложно - требуется всего лишь написать соответствующую миграцию.
Но как перенести бизнес-процесс?
Бизнес-процессы хранятся в БД, поэтому первая мысль - также написать миграцию, которая создавала бы соответствующий БП на новом ландшафте. Проблема в том, что структура БП достаточно сложная и написание таких миграций занимало бы очень большое количество сил и времени разработчиков. Также мы понимаем, что такая миграция ещё должна пройти код-ревью и тестирование! При таком подходе трудозатраты возрастают настолько, что использование БП сразу же становится нецелесообразным.
Конечно, есть самый примитивный вариант - описать в задаче алгоритм модификации БП, по которому этот БП будет вручную модифицироваться при переносе ветки.
На практике, по понятным причинам, такой подход лучше не использовать, так как он обладает рядом критичных недостатков:
Каждый ручной перенос требует отдельных затрат времени и сил разработчика.
При каждом переносе логики есть риск допустить ошибку и сломать бизнес-процесс (особенно это критично при переносе задачи на продуктив).
Каждый перенос логики будет требовать даунтайма песочницы, что может быть критично (особенно для продуктивной среды).
После проведения деплоя задачи с изменением логики БП потребуется актуализировать песочницу каждого разработчика. Если сразу не актуализировать все площадки, в будущем это почти наверняка не будет сделано и бизнес процесс на песочнице конкретного разработчика будет неактуален. Это может привести к серьёзным проблемам, если в будущем этот разработчик будет заниматься задачами, так или иначе затрагивающими целевой бизнес-процесс. Неактуальная логика, заложенная в бизнес-процесс может повлиять на процесс разработки и привести к перерасходу часов по задаче, т. к. разработчику придётся дважды выполнять одну и ту же задачу: сначала для неактуального БП, а после для актуального (когда выяснится, что логика БП на площадке разработчика устарела).
Очевидно, что требуется другое решение, которое автоматически обновляло бы БП при деплое изменений.
Очевидно, что требуется написание миграции, но как именно её написать?
Хорошие новости: для этой задачи есть своё решение!
Требуется всего лишь экспортировать БП в файл средствами битрикса и в коде миграции импортировать БП из файла.
Хозяйке на заметку:
В методе отката миграции стоит описать обратный процесс - импорт предыдущей версии БП. Это позволит быстро вернуться к ранее реализованной логике, если оказалось, что новая логика БП ошибочна.
Таким образом для одной миграции на модификацию логики БП привязывается два файла БП - файл с предыдущей и файл с актуальной версиями логики БП.
Этот подход не лишён недостатков, но по соотношению своих плюсов и минусов однозначно является лидером (личное мнение автора статьи, перед применением рекомендуется консультация лечащего врача).
Минусы подхода:
Миграция требует для себя дополнительно двух файлов: новой и старой версии БП (для установки и отката миграции).
Невозможно провести код-ревью такой миграции - для проверки обновлённого алгоритма БП требуется установка миграции.
Плюсы подхода:
Перенос логики БП в рамках миграции (со всеми преимуществами использования миграций, особенно, когда настроена автоматическая установка миграций при деплое задач).
Отсутствие даунтайма при обновлении логики БП.
Скорость реализации.
Сам код миграции пишется лишь единожды, в дальнейшем его можно переиспользовать во всех аналогичных миграциях.
Возможность быстрого отката миграции, в случае, если замечено какое-либо нештатное поведение изменяемого функционала.
Хозяйке на заметку:
Удобно реализовать отдельный вспомогательный метод для импорта БП и использовать его в миграциях.
В методах установки и отката миграций будет использоваться один и тот же метод, но на вход ему будет передан или новый, или старый файл с логикой БП.
Ниже привожу пример написания кода такой миграции.
<?php
namespace Sprint\Migration;
use Bitrix\Main\Loader;
use Bitrix\Main\LoaderException;
use CBPWorkflowTemplateLoader;
use Exception;
/**
* Класс миграции
* В качестве примера используется миграция, совместимая с модулем sprint.migration https://marketplace.1c-bitrix.ru/solutions/sprint.migration/
*/
class DemoMigrationTasks20231010150224 extends Version
{
/** @var string */
protected $description = "Описание тестовой миграции";
/** @var string */
protected $moduleVersion = "3.12.12";
/** @var HelperManager */
private HelperManager $helper;
/** @var int ID целевого инфоблока */
protected int $targetIblockId;
/** @var string Символьный код типа целевого инфоблока */
const IB_TYPE = 'target_iblock_type';
/** @var string Символьный код целевого инфоблока */
const IB_CODE = 'target_iblock_code';
/** @var string Путь к директории, хранящей файлы миграций */
const BP_BASE_PATH = '/local/php_interface/migrations_files/';
/**
* Массив, описывающий новую версию бизнес-процесса
*/
const BP_NEW_OPTIONS = [
'file' => 'BP-v9.bpt',
'iblock_code' => self::IB_CODE,
'name' => 'Наименование бизнес-процесса',
'code' => 'BP_CODE',
'auto' => 1
];
/**
* Массив, описывающий старую версию бизнес-процесса (требуется для отката миграции)
*/
const BP_OLD_OPTIONS = [
'file' => 'BP-v8.bpt',
'iblock_code' => self::IB_CODE,
'name' => 'Наименование бизнес-процесса',
'code' => 'BP_CODE',
'auto' => 1
];
/**
* DemoMigrationTasks20231010150224 constructor.
*
* @throws LoaderException
* @throws Exception
*/
public function __construct()
{
$this->helper = $this->getHelperManager();
$this->targetIblockId = $this->helper->Iblock()->getIblockId(static::IB_CODE, static::IB_TYPE);
// Сразу подключаем все необходимые модули
if (! Loader::includeModule("bizproc") || ! Loader::includeModule("iblock")) {
throw new Exception('cant load required modules');
}
}
/**
* Метод up
* устанавливает миграцию.
*
* @return void
* @throws Exception
*/
public function up()
{
$this->importBizproc(static::BP_NEW_OPTIONS);
}
/**
* Метод down
* выполняет откат миграции.
*
* @return void
* @throws Exception
*/
public function down()
{
$this->importBizproc(static::BP_OLD_OPTIONS);
}
/**
* Метод getBpFilePath
* возвращает полный путь к файлам импорта бизнес-процессов.
*
* @param string $filename
* @return string
*/
public function getBpFilePath(string $filename): string
{
return $_SERVER["DOCUMENT_ROOT"] . static::BP_BASE_PATH . $filename;
}
/**
* Метод importBizproc
* Подготавливает бизнес-процесс к импорту.
*
* @param array $options - Параметры бизнес-процесса.
* @return void
* @throws Exception
*/
private function importBizproc(array $options)
{
$bpLoader = CBPWorkflowTemplateLoader::GetLoader();
$iblockId = $this->helper->Iblock()->getIblockId($options['iblock_code']);
$bpFields = [
'DOCUMENT_TYPE' => [
'iblock',
'CIBlockDocument',
'iblock_'
],
'AUTO_EXECUTE' => $options['auto'],
'NAME' => $options['name'],
"DESCRIPTION" => $options['code']
];
$filePath = $this->getBpFilePath($options['file']);
$this->importBP($filePath, $iblockId, $options['code'], $bpFields, $bpLoader);
}
/**
* Метод importBP
* импортирует бизнес-процесс.
*
* @param $path
* @param $iblockId
* @param $bpCode
* @param $bpFields
* @param $bpLoader
* @return void
* @throws Exception
*/
private function importBP($path, $iblockId, $bpCode, $bpFields, $bpLoader)
{
$bpId = $bpLoader->GetTemplatesList(
['ID' => 'DESC'],
['DESCRIPTION' => $bpCode],
false,
false,
['ID']
)->fetch()['ID'];
$bpFields['DOCUMENT_TYPE'][2] .= $iblockId;
$f = fopen($path, 'rb');
$bpData = fread($f, filesize($path));
fclose($f);
CBPWorkflowTemplateLoader::ImportTemplate(
$bpId,
$bpFields['DOCUMENT_TYPE'],
$bpFields['AUTO_EXECUTE'],
$bpFields['NAME'],
$bpFields['DESCRIPTION'],
$bpData
);
}
}
Надеюсь, приведенный листинг будет полезным для читателя.
Бизнес-процессы или классы?
Итак, что выбрать для реализации сервиса заявочного типа? Подытожим всё вышесказанное.
Плюсы подхода с бизнес-процессами:
скорость реализации бизнес-процесса,
скорость редактирования бизнес-процесса,
возможность быстрого отката бизнес-процесса к предыдущей версии, если что-то пошло не так,
наглядность бизнес-процесса при его изучении из визуального редактора от 1С-Битрикс (поймёт даже заказчик!),
готовый набор компонентов для работы с бизнес-процессами "из коробки",
гибкость механизма бизнес-процессов, возможность выполнения в их рамках кастомного кода.
Плюсы подхода с классами:
удобство корректировки логики - для корректировки логики достаточно только внести корректировку в соответствующий класс (в случае использования бизнес-процессов для корректировки логики требуется экспортировать и импортировать файл БП с песочницы на песочницу),
удобство проведения код-ревью - все изменения, внесённые в логику разработчиком, видны в коде.
Напоследок хочется сказать, что выбор конкретного инструмента зависит не только от его характеристик, но и от множества других факторов: компетенции и предпочтения команды, особенностей проекта, ограничений по трудозатратам и срокам и т. д.
Оба подхода имеют право на жизнь и иногда даже в рамках разных сервисов одного проекта.