...

Реализация кода на основе UML–диаграмм: пример на Python

UML-диаграммы код на Python примеры курсы обучение, UML для аналитика с примерами, обучение аналитиков, Школа прикладного бизнес-анализа

Практическое использование UML-диаграмм: реализация классов для работы с документами на Python. Пример СЭД на минималках с блэк-джеком задачами согласования и девушками пользователями.

Простые конструкторы классов на Python

Чтобы реализовать постановку задачи, описанную в прошлой статье, создадим классы, определенные на UML-диаграмме.

UML class
UML-диаграмма классов

Класс – это конструктор для создания объектов. Помимо определения статической структуры объекта, т.е. набора его полей (атрибутов) с типом данных для каждого из них, в конструкторе класса также описываются его методы – функции для работы с объектом этого класса. Например, согласно UML-диаграмме класса, документ можно опубликовать, а в маршрут можно добавить задачу. Для этого в конструкторах классов document и route предусмотрены соответствующие методы.

Чтобы вы могли повторить это упражнения, не устанавливая среду разработки, далее я приведу код на Python для запуска его в интерактивной среде Google Colab. Для удобства разделим код на ячейки. Сперва импортируем необходимые модули: datetime для работы с датой и временем, Enum для работы с перечислениями, а также List и Optional из пакета typing, который предоставляет инструменты для указания типов данных в аннотациях.

################################# ячейка 1 - импорт модулей ###############
from datetime import datetime
from enum import Enum
from typing import List, Optional

Для работы с перечислениями в Python используется модуль Enum. Для перечислений не нужен метод инициализации, поскольку Enum – базовый класс, на основе которого создаются перечисления, автоматически управляет созданием своих элементов. Когда создается класс-перечисление, Python автоматически генерирует нужные методы для создания и управления элементами перечисления. Создадим классы-перечисления, в которых определены ограниченные списки значений, такие как роли пользователей, состояния жизненного цикла документов, задач и маршрутов, а также допустимые форматы файлов. В терминах домено-ориентированного проектирования (DDD, Domain-Driven Design) эти классы будут объектами-значениями (Value Object), которые не имеют собственной идентичности.

Код классов-перечислений:

################################# ячейка 2 - создание перечислений ############### 
class role(Enum):
    secretary = 'секретарь'
    administrator = 'администратор'
    employee = 'сотрудник'

class task_status(Enum):
    new = 'новая'
    assigned = 'поставлена'
    started = 'принята к исполнению'
    declined_by_executor = 'отклонена исполнителем'
    performing = 'выполняется'
    canceled_by_initiator = 'отменена инициатором'
    completed_by_executor = 'согласована исполнителем'
    rejected_by_executor = 'несогласована исполнителем'
    frozen = 'заморожена'
    accepted_by_initiator = 'принята инициатором'
    finished = 'завершена'

class route_status(Enum):
    new = 'новый'
    started = 'начат'
    in_progress = 'выполняется'
    canceled = 'отменен'    
    completed = 'завершен'

class file_format(Enum):
    pdf = 'pdf'
    doc = 'doc'
    xls = 'xls'
    ppt = 'ppt'

class document_status(Enum):
    new = 'новый'
    published = 'опубликован'
    on_review = 'на согласовании'
    approved = 'согласован'
    rejected = 'несогласован'

Далее создадим сами классы сущностей и агрегатов в терминах DDD. Поскольку для сущностей и агрегатов явно создаются объекты, нужно использовать метод __init__ в объявлении класса. Метод __init__ в Python называется конструктором класса: он используется для инициализации объектов класса и вызывается автоматически при создании нового экземпляра. Первый аргумент метода __init__ всегда должен быть self, который представляет собой экземпляр создаваемого класса. Это позволяет методу обращаться к атрибутам и методам объекта. Дополнительные аргументы могут быть добавлены для передачи данных при создании объекта. В методе __init__ обычно задаются начальные значения атрибутов объекта.

Для наглядности визуализации данных об объектах каждого класса добавим в конструкторы метод info. Он будет выводить информацию о значениях атрибутов объекта в виде словаря формата JSON (ключ-значение). Это особенно пригодится, чтобы посмотреть информацию об объектах-агрегатах: маршруте согласования документа, в который пользователь-инициатор добавил несколько задач. Например, следующий код показывает объявление класса user, который вызывается при создании нового пользователя.

Код класса user:

class user:
    def __init__(self, login: str, email: str, phone: str, role: role):
        self.login = login
        self.email = email
        self.phone = phone
        self.role = role

    def info(self):
        return {
            "login": self.login,
            "email": self.email,
            "phone": self.phone,
            "role": self.role.value
        }

Код метода  инициализации принимает четыре аргумента:

  • login — строка, представляющая логин пользователя.
  • email — строка, представляющая адрес электронной почты пользователя.
  • phone — строка, представляющая номер телефона пользователя.
  • role — объект типа данных role, представляющий роль пользователя как объект класса role, ранее описанного как перечисление.

Метод info() возвращает информацию о пользователе в виде словаря. Ключи словаря — это строки, представляющие названия атрибутов, а значения — соответствующие атрибуты объекта класса user.

Аналогичный по набору методов код для создания объекта файл документа выглядит так:

class doc_file:
    def __init__(self, file_format: file_format, size: int):
        self.file_format = file_format
        self.size = size

    def info(self):
        return {
            "file_format": self.file_format.value,
            "size": self.size
        }

Класс, описывающий документ, будет сложнее, поскольку он содержит методы управления документом, а также атрибуты документа:

  • number – строка с номером документа;
  • created_timestamp – время и дата создания документа, устанавливаемая при инициализации объекта;
  • published_timestamp – время и дата публикации документа, которая не задана (None), когда документ ещё не опубликован;
  • status – статус, состояние жизненного цикла документа, который имеет значения из перечисления document_status, описанного ранее, например, new, published, on_review, approved, rejected;
  • author – автор документа, т.е. объект класса user, создавший документ;
  • doc_file — файл, связанный с документом, объект класса doc_file, который может быть не задан (None), если к документу не присоединен файл.

Код класса document:

class document:
    def __init__(self, number: str, author: user, published_timestamp: Optional[datetime] = None, doc_file: Optional[doc_file] = None):
        self.number = number
        self.created_timestamp = datetime.now()
        self.published_timestamp = published_timestamp
        self.status = document_status.new
        self.author = author
        self.doc_file = doc_file

    def add_file(self, doc_file: doc_file):
        self.doc_file = doc_file

    def delete_file(self):
        self.doc_file = None

    def publish_document(self):
        self.status = document_status.published
        self.published_timestamp = datetime.now()

    def review_document(self):
        self.status = document_status.on_review

    def approve_document(self):
        self.status = document_status.approved

    def reject_document(self):
        self.status = document_status.rejected

    def info(self):
        return {
            "number": self.number,
            "created_timestamp": self.created_timestamp.isoformat() if self.created_timestamp else None,
            "published_timestamp": self.published_timestamp.isoformat() if self.published_timestamp else None,
            "author": self.author.info(),
            "status": self.status.value,
            "file": self.doc_file.info() if self.doc_file else None
        }

Класс 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(), если они установлены.

Разобравшись с относительно простыми классами, далее рассмотрим более сложные классы задач и маршрутов, которые включают логику изменения жизненного цикла.

DDD, ООП и UML для аналитика

Код курса
BUML
Ближайшая дата курса
9 декабря, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Взаимозависимые состояния и обратные вызовы

Как уже было отмечено выше, с классами task и route немного сложнее. Опишем класс задачи со следующими атрибутами:

  • name – строка с названием;
  • created_timestamp – время и дата создания задачи, устанавливается в момент создания объекта;
  • status — текущий статус задачи, по умолчанию равен значению, заданном в перечислении, task_status.new;
  • started_timestamp – время и дата начала выполнения задачи, может быть None;
  • finished_timestamp – время и дата завершения задачи, может быть None;
  • executor — исполнитель задачи, объект класса пользователь;
  • _status_change_callback**: Приватный атрибут для хранения функции обратного вызова, которая будет вызываться при изменении статуса задачи.

Код класса task:

class task:
    def __init__(self, name: str, executor: user, started_timestamp: Optional[datetime] = None, finished_timestamp: Optional[datetime] = None):
        self.name = name
        self.created_timestamp = datetime.now()
        self.status = task_status.new
        self.started_timestamp = started_timestamp
        self.finished_timestamp = finished_timestamp
        self.executor = executor
        self._status_change_callback = None

    def set_status_change_callback(self, callback):
        self._status_change_callback = callback

    def _notify_status_change(self):
        if self._status_change_callback:
            self._status_change_callback(self)
        self.update_task_status()

    def start_task(self):
        self.status = task_status.started
        self.started_timestamp = datetime.now()
        self._notify_status_change()

    def receive_task(self):
        self.status = task_status.assigned
        self._notify_status_change()

    def decline_task(self):
        self.status = task_status.declined_by_executor
        self._notify_status_change()

    def cancel_task(self):
        self.status = task_status.canceled_by_initiator
        self.finished_timestamp = datetime.now()
        self._notify_status_change()

    def perform_task(self):
        self.status = task_status.performing
        self._notify_status_change()

    def accept_task(self):
        self.status = task_status.accepted_by_initiator
        self._notify_status_change()

    def complete_task(self):
        self.status = task_status.completed_by_executor
        self._notify_status_change()

    def reject_task(self):
        self.status = task_status.rejected_by_executor
        self._notify_status_change()

    def freeze_task(self):
        self.status = task_status.frozen
        self._notify_status_change()

    def finish_task(self):
        self.status = task_status.finished
        self.finished_timestamp = datetime.now()
        self._notify_status_change()

    def update_task_status(self):
        if self.status in [task_status.accepted_by_initiator]:
            self.finish_task()

    def info(self):
        return {
            "name": self.name,
            "created_timestamp": self.created_timestamp.isoformat() if self.created_timestamp else None,
            "started_timestamp": self.started_timestamp.isoformat() if self.started_timestamp else None,
            "finished_timestamp": self.finished_timestamp.isoformat() if self.finished_timestamp else None,
            "executor": self.executor.info(),
            "status": self.status.value
        }

В этом коде определены следующие методы класса:

  • __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:

def add_task(self, task: task):
    task.set_status_change_callback(self.update_route_status)
    self.tasks.append(task)
    self.update_route_status()

Сам метод set_status_change_callback() описан в классе task. Он сохраняет переданную функцию обратного вызова в атрибуте _status_change_callback задачи:

def set_status_change_callback(self, callback):
        self._status_change_callback = callback

Каждый раз, когда статус задачи изменяется, вызывается внутренний метод задачи _notify_status_change(), который проверяет наличие функции обратного вызова и вызывает её, передавая текущую задачу в качестве аргумента. Также здесь вызывается функция обновления статуса задачи:

def _notify_status_change(self):
    if self._status_change_callback:
       self._status_change_callback(self)
    self.update_task_status()

Методы, изменяющие статус задачи, такие как start_task(), receive_task(), decline_task() и пр., вызывают метод _notify_status_change() после изменения статуса. Например, метод, который переводит задачу в состояние начата (started) и устанавливает ей время старта:

def start_task(self):
        self.status = task_status.started
        self.started_timestamp = datetime.now()
        self._notify_status_change()

При вызове метода _notify_status_change() переданная функция обратного вызова, которая является методом update_route_status() у класса route, вызывается и обновляет статус маршрута в зависимости от текущего состояния всех задач:

def update_route_status(self, _=None):
        if any(task.status == task_status.rejected_by_executor for task in self.tasks):
            self.document.reject_document()
        elif all(task.status == task_status.completed_by_executor for task in self.tasks):
            self.document.approve_document()
        elif all(task.finished_timestamp is not None for task in self.tasks):
            self.complete_route()
        elif any(task.status == task_status.started for task in self.tasks):
            self.start_route()
            self.document.publish_document()
        elif any(task.status == task_status.assigned or task.status == task_status.performing for task in self.tasks):
            self.status = route_status.in_progress
            self.document.review_document()
        else:
            self.status = route_status.new
            self.created_timestamp = datetime.now()

Таким образом, класс маршрута (route), который управляет процессом маршрутизации документа и связанными с ним задачами, будет иметь следующие атрибуты:

  • created_timestamp – дата и время создания маршрута;
  • status — статус маршрута, который может принимать значения из перечисления route_status;
  • started_timestamp – дата и время старта маршрута (может быть None);
  • finished_timestamp – дата и время завершения маршрута (может быть None);
  • document – объект класса документ, связанный с маршрутом;
  • initiator — инициатор маршрута, объекта класса пользователь;
  • tasks — список задач, добавленных в маршрут, набор объектов класса task.

Код класса route:

class route:
    def __init__(self, document: 'document', initiator: 'user', started_timestamp: Optional[datetime] = None, finished_timestamp: Optional[datetime] = None):
        self.created_timestamp = datetime.now()
        self.status = route_status.new
        self.started_timestamp = started_timestamp
        self.finished_timestamp = finished_timestamp
        self.document = document
        self.initiator = initiator
        self.tasks: List[task] = []

    def add_task(self, task: task):
        task.set_status_change_callback(self.update_route_status)
        self.tasks.append(task)
        self.update_route_status()

    def delete_task(self, task: task):
        self.tasks.remove(task)
        self.update_route_status()

    def start_route(self):
        self.status = route_status.started
        self.started_timestamp = datetime.now()
        self.document.publish_document()
        for task in self.tasks :
            task.status = task_status.assigned

    def cancel_route(self):
        self.status = route_status.canceled
        self.finished_timestamp = datetime.now()
        for task in self.tasks :
            task.status = task_status.canceled_by_initiator

    def perform_route(self):
        self.status = route_status.in_progress
        self.update_route_status()

    def complete_route(self):
        self.status = route_status.completed
        self.finished_timestamp = datetime.now()

    def get_all_tasks(self) -> List[dict]:
        return [task.info() for task in self.tasks]

    def update_route_status(self, _=None):
        if any(task.status == task_status.rejected_by_executor for task in self.tasks):
            self.document.reject_document()
        elif all(task.status == task_status.completed_by_executor for task in self.tasks):
            self.document.approve_document()
        elif all(task.finished_timestamp is not None for task in self.tasks):
            self.complete_route()
        elif any(task.status == task_status.started for task in self.tasks):
            self.start_route()
            self.document.publish_document()
        elif any(task.status == task_status.assigned or task.status == task_status.performing for task in self.tasks):
            self.status = route_status.in_progress
            self.document.review_document()
        else:
            self.status = route_status.new
            self.created_timestamp = datetime.now()

    def info(self):
        return {
            "created_timestamp": self.created_timestamp.isoformat() if self.created_timestamp else None,
            "started_timestamp": self.started_timestamp.isoformat() if self.started_timestamp else None,
            "finished_timestamp": self.finished_timestamp.isoformat() if self.finished_timestamp else None,
            "initiator": self.initiator.info(),
            "document": self.document.info(),
            "status": self.status.value,
            "tasks": self.get_all_tasks()
        }

В этом коде определены следующие методы:

  • __init__() — инициализация маршрута с заданными документом, инициатором и временными метками;
  • add_task() – функция добавления задачи к маршруту и обновления его статуса;
  • delete_task() – функция удаления задачи из маршрута и обновления его статуса;
  • start_route() – функция старта маршрута с установкой соответствующего статуса и временной метки старта;
  • cancel_route() – функция отмены маршрута с установкой соответствующего статуса и временной метки завершения;
  • perform_route() – функция выполнения маршрута с установкой соответствующего статуса;
  • complete_route() – функция завершения маршрута с установкой соответствующего статуса и временной метки завершения;
  • get_all_tasks() – функция получения информации обо всех задачах маршрута в формате словарей, включая название, временные метки и исполнителя;
  • update_route_status() – функция обновления статуса маршрута в зависимости от статусов входящих в него задач. Если все задачи завершены, маршрут помечается как завершённый. Если любая задача отклонена исполнителем, документ отклоняется, а маршрут переводится в состояние выполняется. Если все задачи согласованы исполнителями, документ становится согласован, а маршрут находится в состоянии выполняется до тех пор, пока инициатор не примет каждую задачу. Когда любая задача начата, маршрут тоже стартует, а неопубликованный документ публикуется. Если любая задача назначена или выполняется, маршрут переводится в состояние выполняется, а документ отправляется на согласование. Иначе маршрут остаётся новым и время создания обновляется.
  • info() – функция получения информации о маршруте в виде словаря, включая информацию о документе, инициаторе и задачах. Для форматирования временных меток в соответствующих атрибутах вызывается метод isoformat(), если они установлены.

После определения всех классов, наконец, можно приступить к созданию объектов и работе с ними. Это рассмотрим далее.

DDD, ООП и UML для аналитика

Код курса
BUML
Ближайшая дата курса
9 декабря, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Манипулирование объектами

Сперва создадим объекты классов:

################################# ячейка 3 - создание объектов ###############
# Создание пользователей
user_obj_1 = user(login="user1", email="user1@example.com", phone="1234567890", role=role.secretary)
user_obj_2 = user(login="user2", email="user2@example.com", phone="0987654321", role=role.administrator)
user_obj_3 = user(login="user3", email="user3@example.com", phone="1122334455", role=role.employee)

# Создание файла
doc_file_obj = doc_file(file_format=file_format.pdf, size=1024)

# Создание документа
doc_1 = document(number="DOC123", author=user_obj_1, doc_file=doc_file_obj)

# Создание задачи
task_obj_1 = task(name='задача 1', executor=user_obj_3)
task_obj_2 = task(name='задача 2', executor=user_obj_2)

# Создание маршрута
route_obj = route(document=doc_1, initiator=user_obj_2)

# Добавление задачи в маршрут
route_obj.add_task(task_obj_1)
route_obj.add_task(task_obj_2)

Выведем информацию о созданном маршруте:

#вывод информации о маршруте
route_obj.info()
Вывод информации о сложном объекте
Вывод информации о сложном объекте

Выведем информацию о задачах маршрута, вызвав метод get_all_tasks() у объекта route_obj:

#вывод информации о всех задачах конкретного маршрута
route_obj.get_all_tasks()

Вывод информации о сложных объектах

Вывод информации о сложных объектах

Теперь реализуем логику управления документами, стартовав маршрут. Входящие в него задачи перейдут в состояние поставлена.

#стартовать маршрут
route_obj.start_route()

#показать информацию о маршруте
route_obj.info()
Старт маршрута и вывод данных об этом агрегате
Старт маршрута и вывод данных об этом агрегате

Предположим, один из исполнителей согласовал документ, а другой – нет.

############## новая ячейка - управление задачами #######################
task_obj_1.start_task() #стартовать задачу 1
print("---------------- задача 1 стартована ---------------- ")
print("Статус задачи: ", task_obj_1.status.value)
print("Время старта задачи: ", task_obj_1.started_timestamp)
print("\n---------------- задача 1 выполнена - согласовано ---------------- ")
task_obj_1.complete_task() #согласовать задачу 1
print("Статус задачи: ", task_obj_1.status.value)
print("\n---------------- выполненная задача 1 принята инициатором---------------- ")
task_obj_1.accept_task() #принять задачу 1
print("Статус задачи: ", task_obj_1.status.value)
print("Время завершения задачи: ", task_obj_1.finished_timestamp)

task_obj_2.start_task() #стартовать задачу 2
print("\n---------------- задача 2 стартована---------------- ")
print("Статус задачи: ", task_obj_2.status.value)
print("Время старта задачи: ", task_obj_2.started_timestamp)
task_obj_2.reject_task() #отказать в согласовании документа по задачу 2
print("\n---------------- задача 2 выполнена - не согласовано---------------- ")
print("Статус задачи: ", task_obj_2.status.value)
print("\n---------------- выполненная задача 2 принята инициатором---------------- ")
task_obj_2.accept_task() #принять задачу 2
print("Статус задачи: ", task_obj_2.status.value)
print("Время завершения задачи: ", task_obj_2.finished_timestamp)

#узнаем статус маршрута
print("\nСтатус маршрута: ", route_obj.status.value)
print("Время завершения маршрута: ", route_obj.finished_timestamp)
#узнаем статус документа
print("Статус документа: ",  route_obj.document.status.value)
Изменение состояний связанных объектов
Изменение состояний связанных объектов

DDD, ООП и UML для аналитика

Код курса
BUML
Ближайшая дата курса
9 декабря, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Хотя итоговый код немного отличается от первичной постановки задачи и проекта в виде набора UML-диаграмм, этап проектирования помог реализовать не только структуру доменных объектов, но и логику их поведения. Поэтому умение разрабатывать UML-диаграммы очень полезный навык для аналитика. Освоить его вы сможете на моих курсах в Школе прикладного бизнес-анализа на базе нашего лицензированного учебного центра обучения и повышения квалификации системных и бизнес-аналитиков в Москве:

Я даю свое согласие на обработку персональных данных и соглашаюсь с политикой конфиденциальности.