Практическое использование UML-диаграмм: реализация классов для работы с документами на Python. Пример СЭД на минималках с блэк-джеком задачами согласования и девушками пользователями.
Простые конструкторы классов на Python
Чтобы реализовать постановку задачи, описанную в прошлой статье, создадим классы, определенные на UML-диаграмме.
Класс – это конструктор для создания объектов. Помимо определения статической структуры объекта, т.е. набора его полей (атрибутов) с типом данных для каждого из них, в конструкторе класса также описываются его методы – функции для работы с объектом этого класса. Например, согласно UML-диаграмме класса, документ можно опубликовать, а в маршрут можно добавить задачу. Для этого в конструкторах классов document и route предусмотрены соответствующие методы.
Чтобы вы могли повторить это упражнения, не устанавливая среду разработки, далее я приведу код на Python для запуска его в интерактивной среде Google Colab. Для удобства разделим код на ячейки. Сперва импортируем необходимые модули: datetime для работы с датой и временем, Enum для работы с перечислениями, а также List и Optional из пакета typing, который предоставляет инструменты для указания типов данных в аннотациях.
Для работы с перечислениями в Python используется модуль Enum. Для перечислений не нужен метод инициализации, поскольку Enum – базовый класс, на основе которого создаются перечисления, автоматически управляет созданием своих элементов. Когда создается класс-перечисление, Python автоматически генерирует нужные методы для создания и управления элементами перечисления. Создадим классы-перечисления, в которых определены ограниченные списки значений, такие как роли пользователей, состояния жизненного цикла документов, задач и маршрутов, а также допустимые форматы файлов. В терминах домено-ориентированного проектирования (DDD, Domain-Driven Design) эти классы будут объектами-значениями (Value Object), которые не имеют собственной идентичности.
Код классов-перечислений:
Далее создадим сами классы сущностей и агрегатов в терминах DDD. Поскольку для сущностей и агрегатов явно создаются объекты, нужно использовать метод __init__ в объявлении класса. Метод __init__ в Python называется конструктором класса: он используется для инициализации объектов класса и вызывается автоматически при создании нового экземпляра. Первый аргумент метода __init__ всегда должен быть self, который представляет собой экземпляр создаваемого класса. Это позволяет методу обращаться к атрибутам и методам объекта. Дополнительные аргументы могут быть добавлены для передачи данных при создании объекта. В методе __init__ обычно задаются начальные значения атрибутов объекта.
Для наглядности визуализации данных об объектах каждого класса добавим в конструкторы метод info. Он будет выводить информацию о значениях атрибутов объекта в виде словаря формата JSON (ключ-значение). Это особенно пригодится, чтобы посмотреть информацию об объектах-агрегатах: маршруте согласования документа, в который пользователь-инициатор добавил несколько задач. Например, следующий код показывает объявление класса user, который вызывается при создании нового пользователя.
Код класса user:
Код метода инициализации принимает четыре аргумента:
- login — строка, представляющая логин пользователя.
- email — строка, представляющая адрес электронной почты пользователя.
- phone — строка, представляющая номер телефона пользователя.
- role — объект типа данных role, представляющий роль пользователя как объект класса role, ранее описанного как перечисление.
Метод info() возвращает информацию о пользователе в виде словаря. Ключи словаря — это строки, представляющие названия атрибутов, а значения — соответствующие атрибуты объекта класса user.
Аналогичный по набору методов код для создания объекта файл документа выглядит так:
Класс, описывающий документ, будет сложнее, поскольку он содержит методы управления документом, а также атрибуты документа:
- number – строка с номером документа;
- created_timestamp – время и дата создания документа, устанавливаемая при инициализации объекта;
- published_timestamp – время и дата публикации документа, которая не задана (None), когда документ ещё не опубликован;
- status – статус, состояние жизненного цикла документа, который имеет значения из перечисления document_status, описанного ранее, например, new, published, on_review, approved, rejected;
- author – автор документа, т.е. объект класса user, создавший документ;
- doc_file — файл, связанный с документом, объект класса doc_file, который может быть не задан (None), если к документу не присоединен файл.
Код класса document:
Класс document содержит следующие методы:
- __init__() — конструктор, который инициализирует объект документа с номером, автором, временной меткой публикации и файлом. При создании он заполняет временную метку создания документа и устанавливает его начальный статус new;
- add_file() – функция добавления или замены файла, связанного с документом;
- delete_file() — функция удаления файла, связанного с документом, устанавливает значение атрибута doc_file в None;
- publish_document () — функция публикации документа, устанавливает статус документа в значение published и временную метку публикации текущим датой и временем;
- review_document() — функция перевода документа в статус согласования (on_review);
- approve_document() — функция согласования документа, устанавливает ему статус approved;
- reject_document() – функция отказа в согласовании документа, устанавливает ему статус rejected;
- info() – функция получения информации о документе в виде словаря: номер, временные метки создания и публикации, информацию об авторе, статус и информацию о файле (если файл существует). Для форматирования временных меток в атрибутах created_timestamp и published_timestamp вызывается метод isoformat(), если они установлены.
Разобравшись с относительно простыми классами, далее рассмотрим более сложные классы задач и маршрутов, которые включают логику изменения жизненного цикла.
Взаимозависимые состояния и обратные вызовы
Как уже было отмечено выше, с классами task и route немного сложнее. Опишем класс задачи со следующими атрибутами:
- name – строка с названием;
- created_timestamp – время и дата создания задачи, устанавливается в момент создания объекта;
- status — текущий статус задачи, по умолчанию равен значению, заданном в перечислении, task_status.new;
- started_timestamp – время и дата начала выполнения задачи, может быть None;
- finished_timestamp – время и дата завершения задачи, может быть None;
- executor — исполнитель задачи, объект класса пользователь;
- _status_change_callback**: Приватный атрибут для хранения функции обратного вызова, которая будет вызываться при изменении статуса задачи.
Код класса task:
В этом коде определены следующие методы класса:
- __init__() – конструктор, инициализирует объект задачи с заданными параметрами;
- set_status_change_callback() — функция обратного вызова для уведомления об изменении статуса;
- _notify_status_change() — внутренний метод, вызывает функцию обратного вызова при изменении статуса задачи;
- start_task() – функции старта задачи, устанавливает статус задачи как task_status.started и фиксирует временную метку ее старта;
- receive_task() – функция назначения задачи на исполнителя, устанавливает статус задачи как task_status.assigned;
- decline_task() – функция отклонения задачи исполнителем, устанавливает ее статус как task_status.declined_by_executor;
- cancel_task() – функция отмены задачи инициатором, устанавливает ее статус как task_status.canceled_by_initiator и фиксирует временную метку завершения текущим временем;
- perform_task() – функция выполнения задачи, задает ее статус как task_status.performing;
- freeze_task() – функция заморозки задачи, устанавливает ее статус как task_status.frozen;
- complete_task() – функция согласования задачи, устанавливает ее статус как task_status.completed_by_executor и вызывает метод update_task_status;
- reject_task() – функция несогласования задачи, устанавливает ее статус как task_status.rejected_by_executor и вызывает метод update_task_status;
- accept_task() – функция принятия задачи инициатором, устанавливает ее статус как task_status.accepted_by_initiator и вызывает метод update_task_status;
- finish_task() – функция завершения задачи, устанавливает ее статус как task_status.finished и фиксирует временную метку завершения;
- update_task_status() – функция, которая проверяет статус задачи и вызывает метод finish_task, если задача была принята инициатором;
- info() – функция получения информации о задаче в виде словаря: имя, временные метки, информацию об исполнителе и текущий статус. Для форматирования временных меток в соответствующих атрибутах вызывается метод isoformat(), если они установлены.
Поскольку состояние маршрута меняется в зависимости от входящих в него задач, их необходимо связать. В рассмотренном примере это делается с помощью функций обратного вызова (callback): когда статус задачи изменяется, маршрут обновляет свой статус соответственно. Сallback передается как аргумент другой функции и вызывается в определённый момент, например, когда происходит какое-то событие. Например, функция обратного вызова используется для уведомления маршрута о том, что статус задачи изменился.
Когда задача добавляется в маршрут с помощью метода add_task, для этой задачи устанавливается функция обратного вызова с помощью метода set_status_change_callback:
Сам метод set_status_change_callback() описан в классе task. Он сохраняет переданную функцию обратного вызова в атрибуте _status_change_callback задачи:
Каждый раз, когда статус задачи изменяется, вызывается внутренний метод задачи _notify_status_change(), который проверяет наличие функции обратного вызова и вызывает её, передавая текущую задачу в качестве аргумента. Также здесь вызывается функция обновления статуса задачи:
Методы, изменяющие статус задачи, такие как start_task(), receive_task(), decline_task() и пр., вызывают метод _notify_status_change() после изменения статуса. Например, метод, который переводит задачу в состояние начата (started) и устанавливает ей время старта:
При вызове метода _notify_status_change() переданная функция обратного вызова, которая является методом update_route_status() у класса route, вызывается и обновляет статус маршрута в зависимости от текущего состояния всех задач:
Таким образом, класс маршрута (route), который управляет процессом маршрутизации документа и связанными с ним задачами, будет иметь следующие атрибуты:
- created_timestamp – дата и время создания маршрута;
- status — статус маршрута, который может принимать значения из перечисления route_status;
- started_timestamp – дата и время старта маршрута (может быть None);
- finished_timestamp – дата и время завершения маршрута (может быть None);
- document – объект класса документ, связанный с маршрутом;
- initiator — инициатор маршрута, объекта класса пользователь;
- tasks — список задач, добавленных в маршрут, набор объектов класса task.
Код класса route:
В этом коде определены следующие методы:
- __init__() — инициализация маршрута с заданными документом, инициатором и временными метками;
- add_task() – функция добавления задачи к маршруту и обновления его статуса;
- delete_task() – функция удаления задачи из маршрута и обновления его статуса;
- start_route() – функция старта маршрута с установкой соответствующего статуса и временной метки старта;
- cancel_route() – функция отмены маршрута с установкой соответствующего статуса и временной метки завершения;
- perform_route() – функция выполнения маршрута с установкой соответствующего статуса;
- complete_route() – функция завершения маршрута с установкой соответствующего статуса и временной метки завершения;
- get_all_tasks() – функция получения информации обо всех задачах маршрута в формате словарей, включая название, временные метки и исполнителя;
- update_route_status() – функция обновления статуса маршрута в зависимости от статусов входящих в него задач. Если все задачи завершены, маршрут помечается как завершённый. Если любая задача отклонена исполнителем, документ отклоняется, а маршрут переводится в состояние выполняется. Если все задачи согласованы исполнителями, документ становится согласован, а маршрут находится в состоянии выполняется до тех пор, пока инициатор не примет каждую задачу. Когда любая задача начата, маршрут тоже стартует, а неопубликованный документ публикуется. Если любая задача назначена или выполняется, маршрут переводится в состояние выполняется, а документ отправляется на согласование. Иначе маршрут остаётся новым и время создания обновляется.
- info() – функция получения информации о маршруте в виде словаря, включая информацию о документе, инициаторе и задачах. Для форматирования временных меток в соответствующих атрибутах вызывается метод isoformat(), если они установлены.
После определения всех классов, наконец, можно приступить к созданию объектов и работе с ними. Это рассмотрим далее.
Манипулирование объектами
Сперва создадим объекты классов:
Выведем информацию о созданном маршруте:
Выведем информацию о задачах маршрута, вызвав метод get_all_tasks() у объекта route_obj:
Теперь реализуем логику управления документами, стартовав маршрут. Входящие в него задачи перейдут в состояние поставлена.
Предположим, один из исполнителей согласовал документ, а другой – нет.
Хотя итоговый код немного отличается от первичной постановки задачи и проекта в виде набора UML-диаграмм, этап проектирования помог реализовать не только структуру доменных объектов, но и логику их поведения. Поэтому умение разрабатывать UML-диаграммы очень полезный навык для аналитика. Освоить его вы сможете на моих курсах в Школе прикладного бизнес-анализа на базе нашего лицензированного учебного центра обучения и повышения квалификации системных и бизнес-аналитиков в Москве:
Курс: BUML
Копирование, размножение, распространение, перепечатка (целиком или частично), или иное использование материала допускается только с письменного разрешения правообладателя ООО "УЦ Коммерсант"