Class Diagram (Диаграмма классов)

Урок 5 из 5

Урок 04.05: Class Diagram (Диаграмма классов)

Цель урока

Научиться проектировать объектную модель системы с помощью Class Diagram. После этого урока вы сможете:

  • Понимать, как Class Diagram помогает аналитику увидеть структуру данных «до того, как написан код»
  • Строить классы с атрибутами, методами и уровнями видимости
  • Различать 6 типов связей: Ассоциация, Зависимость, Агрегация, Композиция, Наследование, Реализация
  • Обоснованно выбирать между Агрегацией и Композицией
  • Транслировать Class Diagram в схему реляционной БД
  • Писать PlantUML-код для диаграммы классов

1. Что такое Class Diagram?

Class Diagram (диаграмма классов) — это UML-диаграмма, которая показывает статическую структуру системы: набор сущностей (классов), их атрибуты, методы и связи между ними.

1.1. Аналогия для быстрого понимания

Class Diagram — это генеральный план здания на этапе архитектурного проекта:

Class Diagram Архитектурный план
Класс Комната / помещение
Атрибут Размеры комнаты (ширина, высота)
Метод Функция комнаты (спальня — для сна, кухня — для готовки)
Связь (ассоциация) Коридор между комнатами
Композиция Стена, которая является частью дома (не существует без него)
Агрегация Мебель, которая стоит в комнате (может быть вынесена)
Наследование «Кухня-столовая» — частный случай «Кухня», но с дополнительной функцией

Аналитик — это архитектор. Разработчик — строитель. Без плана строитель построит «как получится».

1.2. Зачем Class Diagram аналитику?

Аналитик не пишет код. Зачем ему Class Diagram?

Причина 1: Понять предметную область

Когда аналитик рисует классы и связи, он выявляет сущности, которые заказчик упоминает в требованиях. Заказчик говорит «заказ содержит позиции», аналитик рисует Order * — * OrderItem и уточняет: «Одна позиция может быть в нескольких заказах? Или позиция уникальна для заказа?»

Причина 2: Обнаружить нестыковки

  • «У заказа есть статус. А у позиции заказа есть свой статус?» — вопрос, который возникает при рисовании связей
  • «Может ли заказ быть без позиций?» — кратность 0..* или 1..*?
  • «Кто назначает исполнителя задачи?» — роли на связях

Причина 3: Коммуникация с командой

Class Diagram — общий язык аналитика, архитектора, разработчика и DBA. Если аналитик нарисовал Project * — * User через связку ProjectMember, архитектор БД сразу понимает, что нужна промежуточная таблица. Разработчик видит, какие поля будут в DTO.

Причина 4: Основа для документации

Class Diagram — это ядро SRS (Software Requirements Specification). Из неё вырастают:

  • Схема БД (SQL DDL)
  • OpenAPI-спецификация (DTO)
  • Тест-планы (какие сущности нужно тестировать)
  • Архитектурная документация (C4 — уровень containers)

1.3. Class Diagram vs ERD

Аспект UML Class Diagram ERD (Entity-Relationship)
Стандарт OMG UML Crow's Foot / Chen
Методы ✅ Да ❌ Нет (только данные)
Атрибуты ✅ Да, с типами ✅ Да, с типами
Связи 6 типов (ассоциация, агрегация, композиция, зависимость, наследование, реализация) Только relationship
Для чего Проектирование ООП-кода, доменная модель Проектирование реляционной БД
Когда использует аналитик Всегда (моделирование предметной области) Когда нужна только схема БД

На практике: Если вы проектируете объектно-ориентированную систему — Class Diagram обязателен. Если проектируете только схему БД — достаточно ERD. Но Class Diagram покрывает и ERD-задачи (трансляция в SQL — прямой путь).


2. Структура класса

2.1. Прямоугольник класса

Класс изображается прямоугольником, разделённым на три секции:

┌─────────────────────┐
│       Заказ         │  ← Секция 1: ИМЯ КЛАССА
├─────────────────────┤
│ - id: UUID          │  
│ - датаСоздания: Date│  ← Секция 2: АТРИБУТЫ
│ - статус: String    │
│ - сумма: Decimal    │
├─────────────────────┤
│ + создать()         │
│ + оплатить()        │  ← Секция 3: МЕТОДЫ (ОПЕРАЦИИ)
│ + отменить()        │
└─────────────────────┘

2.2. Имя класса

Правила именования:

  • Существительное в единственном числе: User, Order, Product, Category
  • Избегать сокращений: CustAddrCustomerAddress
  • Язык: на русском для аналитика (допустимо для документирования), на английском для кода
  • UpperCamelCase: первая буква каждого слова заглавная

Как выделить классы из требований:

  1. Прочитайте тексты требований и User Stories
  2. Выпишите все именные существительные (реальные объекты)
  3. Отфильтруйте:
    • Будут классами: встречаются многократно, имеют атрибуты и поведение
    • Будут атрибутами: описывают свойство другого объекта («имя», «email»)
    • Будут методами: описывают действие («проверить», «отправить»)

Пример:

«Пользователь создаёт заказ с несколькими товарами и оплачивает его через платёжную систему. После оплаты заказ отправляется на адрес доставки.»

Существительное Решение
Пользователь Класс User
Заказ Класс Order
Товар Класс Product
Платёжная система Класс PaymentGateway (или внешняя система)
Адрес доставки Класс Address (свои атрибуты: город, улица, индекс)
Оплата — действие Метод pay() у класса Order

2.3. Атрибуты

Формат записи атрибута:

[видимость] имя [ : тип [ = начальное_значение ] ]

Примеры:

+ id: UUID
- email: String
# status: OrderStatus = PENDING
- createdAt: DateTime
+ total: Decimal {readOnly}

Таблица типов:

Тип в UML Аналог в Java Аналог в SQL
String String VARCHAR(n) / TEXT
Integer / Int int / Integer INTEGER
Decimal BigDecimal DECIMAL(10,2)
Boolean boolean BOOLEAN
Date java.util.Date DATE
DateTime java.time.LocalDateTime TIMESTAMP
UUID java.util.UUID UUID
Enum enum VARCHAR + CHECK / ENUM
Text String (длинный) TEXT
Money BigDecimal (с валютой) DECIMAL + currency

Какие атрибуты есть у любого класса-сущности:

+ id: UUID            (первичный ключ)
- createdAt: DateTime  (метка создания)
- updatedAt: DateTime  (метка обновления, опционально)

Multivalued (множественные) атрибуты: Нотация: [1..*] или [*] после типа:

+ phoneNumbers: List<String> [*]
+ tags: String [0..5]

2.4. Методы (Операции)

Формат записи метода:

[видимость] имя ( [параметр: тип, ...] ) [ : возвращаемый_тип ]

Примеры:

+ createOrder(items: List<OrderItem>): Order
+ pay(amount: Decimal, method: PaymentMethod): Boolean
- calculateDiscount(): Decimal
+ getStatus(): OrderStatus

Для аналитика методы не обязательны. Основное внимание — на атрибутах и связях. Методы добавляются, когда:

  • Есть ключевая бизнес-операция, которая меняет состояние объекта (approve(), cancel())
  • Нужно показать интерфейс (методы без реализации)
  • Есть CRUD-операции (но их обычно опускают, они подразумеваются)

2.5. Уровни видимости

Символ Уровень Аналог Java Смысл
+ public public Виден всем. Те методы, которые вызываются извне.
- private private Виден только внутри класса. Внутренняя логика.
# protected protected Виден классу и его наследникам.
~ package (default) package-private Виден внутри пакета/модуля.

Для аналитика: уровни видимости указываются факультативно. На ранних этапах анализа все атрибуты можно считать - (private), методы — + (public). Но если вы работаете над архитектурой API, видимость помогает:

  • + на методе → этот метод будет в публичном API
  • - на атрибуте → поле не возвращается клиенту (скрыто в DTO)
  • # на атрибуте → поле доступно классам-наследникам (важно при наследовании)

3. Типы связей между классами

Это самый важный раздел. Неверный выбор связи — ошибка в модели.

3.1. Полная таблица связей

Тип связи Нотация PlantUML Смысл Аналогия
Association Простая линия A -- B «Знает о» — структурная связь Друзья в соцсети
Dependency Пунктирная стрелка A ..> B «Использует временно» — не хранит ссылку Читатель берёт книгу в библиотеке
Aggregation Линия + пустой ромб ◇ A o-- B «Целое-часть, часть может жить отдельно» Команда и игроки
Composition Линия + залитый ромб ● A *-- B «Целое-часть, часть не живёт без целого» Дом и комнаты
Generalization Линия + пустая стрелка △ `A < -- B` «Является частным случаем»
Realization Пунктир + пустая стрелка `A < .. B` «Реализует контракт»

3.2. Association (Ассоциация)

Нотация: ─── (простая линия)

Смысл: класс A «знает о» классе B. Это самая общая связь — «имеет ссылку».

Аналогия: В записной книжке телефона есть контакты. Записная книжка знает о контактах, но контакт — это не «часть» записной книжки.

Когда использовать: когда у класса есть ссылка на другой класс, но нет строгой иерархии «часть-целое».

Примеры:

Читатель ──── Книга         (читатель берёт книгу)
Врач ──── Пациент           (врач лечит пациента)
Студент ──── Курс           (студент записан на курс)

Кратность на ассоциации:

┌──────────┐  1          0..* ┌──────────┐
│  Врач    │──────────────────│ Пациент  │
└──────────┘    лечит         └──────────┘

Один врач лечит от нуля до многих пациентов.
Один пациент лечится у одного врача (или не назначен — 0..1).

Роль на конце ассоциации:

┌──────────┐  1          0..* ┌──────────┐
│  Заказ   │──────────────────│  User    │
└──────────┘  ────────        └──────────┘
              создатель       исполнитель

Роль «создатель» — заказ создан пользователем. Роль «исполнитель» — заказ назначен на пользователя. Это разные роли, хотя класс один (User).

3.3. Dependency (Зависимость)

Нотация: - - - > (пунктирная стрелка от A к B)

Смысл: класс A временно использует класс B, но не хранит ссылку на него. A «зависит» от B — если B изменится, A может сломаться.

Аналогия: Человек вызывает такси через приложение. После поездки связь заканчивается. Человек не «хранит» такси у себя.

Примеры:

  • ReportGenerator - - -> Order — генератор отчёта получает данные из Order, но не хранит заказы
  • EmailService - - -> User — сервис отправки email использует email пользователя, но не хранит пользователя
  • PaymentController - - -> PaymentGateway — контроллер вызывает шлюз, но не держит ссылку постоянно

Когда Dependency, а когда Association?

Критерий Association Dependency
Хранит ссылку? Да (поле класса) Нет (параметр метода)
Связь постоянная? Да (на время жизни объекта) Нет (только на время вызова)
Пример в коде class A { B b; } class A { void method(B b) { } }

Правило для аналитика: Dependency — редкость на диаграммах аналитика. Обычно достаточно Association. Dependency рисуют, когда хотят подчеркнуть: «Этот класс временно использует другой».

3.4. Aggregation (Агрегация) — пустой ромб ◇

Нотация: ◇──── (линия + пустой ромб со стороны «целого»)

Смысл: отношение «часть-целое», где часть может существовать независимо от целого. Целое (aggregate) содержит ссылку на часть, но если целое уничтожить — часть продолжает жить.

Аналогия: Университет и студенты. Университет — целое, студенты — части. Если университет закрыть, студенты остаются (они люди, а не свойство университета). Студент может перейти в другой университет.

Ещё аналогия: Коробка и карандаши. Карандаши в коробке. Если коробку выбросить — карандаши останутся. Карандаш может быть в другой коробке.

Примеры:

Отдел ◇──── Сотрудник      (сотрудник может перейти в другой отдел)
Проект ◇──── Участник      (участник может выйти из проекта, но остаться в системе)
Библиотека ◇──── Книга     (книга может быть передана в другую библиотеку)
Команда ◇──── Игрок        (игрока могут продать в другой клуб)

Ключевой вопрос Aggregation: «Может ли часть существовать без целого?»

  • Да → Aggregation
  • Нет → Composition

3.5. Composition (Композиция) — залитый ромб ●

Нотация: ●──── (линия + залитый ромб со стороны «целого»)

Смысл: отношение «часть-целое», где часть не может существовать без целого. Часть создаётся и уничтожается вместе с целым. Если целое уничтожается — все его части уничтожаются автоматически.

Аналогия: Дом и комнаты. Комнаты — часть дома. Если дом снесли — комнат больше нет. Комната не может «переехать» в другой дом.

Ещё аналогия: Автомобиль и двигатель. Двигатель — часть автомобиля. Если автомобиль утилизирован — двигатель тоже идёт в утиль (да, формально его можно вынуть, но в контексте системы он «не существует без автомобиля»).

Примеры:

Заказ ●──── ПозицияЗаказа       (позиция не существует без заказа)
Документ ●──── ВерсияДокумента   (версия не существует без документа)
Счёт ●──── ОперацияПоСчёту       (операция не существует без счёта)
Задача ●──── Комментарий         (комментарий не существует без задачи)

Внимание: В реальной жизни «комнату можно перестроить», «двигатель можно вынуть». Composition — это семантическое отношение: в контексте системы часть не имеет смысла без целого. Не физическая, а концептуальная связь.

3.6. Aggregation vs Composition — главная разница

Критерий Aggregation (◇) Composition (●)
Сила связи Слабая Сильная
Часть без целого ✅ Может существовать ❌ Не может существовать
Целое без части ✅ Может ❌ Не может (обычно)
Время жизни Разное (часть переживает целое) Одинаковое (часть умирает с целым)
Пример Команда ◇ Игрок Заказ ● Позиция
Аналог в коде Ссылка (поле) Ссылка + создание в конструкторе
В БД Foreign Key с SET NULL Foreign Key с CASCADE DELETE
Каскадное удаление Нет (часть остаётся) Да (часть удаляется)

Проверочный вопрос: «Если я удалю целое, должен ли удалиться объект-часть?»

  • ДА → Composition ()
  • НЕТ → Aggregation () или Association

Когда не уверены — выбирайте простую Ассоциацию. Агрегация и Композиция — уточнения, которые не обязательны на ранних этапах. Если сомневаетесь — линия без ромба.

3.7. Generalization (Наследование) — пустая стрелка △

Нотация: ────△ (линия + пустая треугольная стрелка к родительскому классу)

Смысл: класс-ребёнок (подкласс) «является» родительским классом + добавляет свои атрибуты и методы. Отношение «is-a».

Аналогия: Собака — это частный случай Млекопитающего. Собака наследует все свойства млекопитающего (дышит лёгкими, имеет шерсть) + добавляет свои (порода, лает).

Примеры:

User ▲── AdminUser           (админ — это пользователь с правами)
Payment ▲── CreditCardPayment (оплата картой — частный случай оплаты)
Document ▲── Invoice         (счёт — частный случай документа)
Employee ▲── Manager         (менеджер — это сотрудник с подчинёнными)

Правила наследования:

  • Наследник имеет все атрибуты и методы родителя
  • Наследник может добавить новые атрибуты/методы
  • Наследник может переопределить (override) метод родителя
  • В UML стрелка идёт от наследника к родителю

Ошибка: наследование там, где не нужно

«Врач и Пациент наследуют от Человек. У них есть имя, возраст, пол.»

Проблема: Врач и пациент — это роли, а не разные классы. Класс User с ролью doctor / patient — проще и правильнее.

Наследование нужно, когда:

  • Наследник имеет дополнительное поведение (методы), а не только атрибуты
  • Наследник по-разному реализует один и тот же метод
  • Есть полиморфизм: код работает с User, а подставляет AdminUser

3.8. Realization (Реализация) — пунктир + пустая стрелка

Нотация: - - - △ (пунктирная линия + пустая треугольная стрелка)

Смысл: класс реализует интерфейс (контракт). Интерфейс определяет методы без реализации, класс — с реализацией.

Аналогия: Контракт («Я обязуюсь доставить товар за 3 дня») и Курьер («Я доставляю товар за 2 дня»). Интерфейс — контракт, класс — исполнитель.

Примеры:

«PaymentGateway» (interface)
       △
       | реализует
       |
┌──────────────────┐
│ StripeGateway     │
├──────────────────┤
│ + charge()        │
│ + refund()        │
└──────────────────┘

Правило: если в вашей системе есть интерфейс (замена одного сервиса другим), используйте Realization. Пример: Аналитик пишет, что система должна поддерживать несколько платёжных шлюзов (Stripe, PayPal). Интерфейс PaymentGateway, реализации — StripeGateway, PayPalGateway.

3.9. Шпаргалка по связям

Вы хотите сказать… Используйте
A знает о B Association
A временно использует B Dependency
B — часть A, но может жить без A Aggregation
B — часть A, умрёт без A Composition
B — частный случай A (is-a) Generalization
A — интерфейс, B — его реализация Realization

4. Multiplicity (Кратность)

4.1. Полная таблица кратности

Нотация Значение Пример
1 Ровно один Заказ имеет ровно одного создателя
0..1 Ноль или один Задача может быть назначена на одного исполнителя (или никого)
0..* Ноль или более Заказ может содержать любое количество позиций (включая ноль)
1..* Один или более Заказ содержит минимум одну позицию
* Любое количество (синоним 0..*) То же, что 0..*
2 Ровно два (конкретное число) У паспорта ровно две страницы с отметками
2..5 От двух до пяти В команде от 2 до 5 администраторов
3, 5 Три или пять Скидка на 3 или 5 товаров

4.2. Как определять кратность

Задайте себе два вопроса для каждой связи:

Вопрос 1 (слева направо): «Сколько объектов B может быть связано с одним объектом A?» Вопрос 2 (справа налево): «Сколько объектов A может быть связано с одним объектом B?»

Пример — User и Order:

Вопрос 1: Сколько заказов может создать один пользователь? → 0..* (много, а может и ни одного)
Вопрос 2: Сколько пользователей создали один заказ? → 1 (ровно один)

Результат: User "1" — "0..*" Order

Пример — Task и Comment:

Вопрос 1: Сколько комментариев у одной задачи? → 0..*
Вопрос 2: Сколько задач может комментировать один комментарий? → 1 (комментарий привязан к задаче)

Результат: Task "1" — "0..*" Comment

4.3. Типичные ошибки кратности

Ошибка ✅ Правильно Почему?
Project "1" — "1" Task Project "1" — "0..*" Task В проекте много задач
Order "1" — "1" OrderItem Order "1" — "1..*" OrderItem Заказ минимум с одной позицией (пустой заказ не имеет смысла)
Category "1" — "0..*" Product Всё верно В категории может быть много товаров или ни одного
User "1" — "1" Address User "1" — "0..1" Address У пользователя может не быть адреса (ещё не указал)

5. Пример: Class Diagram для интернет-магазина

5.1. Постановка

Спроектируем объектную модель интернет-магазина.

Основные сущности:

  • User — зарегистрированный пользователь (покупатель)
  • AdminUser — администратор магазинас расширенными правами (наследует User)
  • Product — товар (может быть в нескольких категориях)
  • Category — категория товаров
  • Order — заказ
  • OrderItem — позиция заказа (конкретный товар в количестве)
  • Payment — платёж по заказу (один заказ — один платёж)
  • Address — адрес доставки
  • Review — отзыв на товар
  • Cart — корзина (временное хранилище)

5.2. Классы и атрибуты

┌──────────────────────────┐
│          User            │
├──────────────────────────┤
│ - id: UUID               │
│ - email: String [unique] │
│ - passwordHash: String   │
│ - name: String           │
│ - phone: String?         │
│ - role: UserRole         │
│ - createdAt: DateTime    │
│ - updatedAt: DateTime    │
├──────────────────────────┤
│ + register()             │
│ + updateProfile()        │
│ + getOrders(): List      │
└──────────────────────────┘
          △
          │
┌──────────────────────────┐
│       AdminUser          │
├──────────────────────────┤
│ - accessLevel: Int       │
├──────────────────────────┤
│ + manageProducts()       │
│ + manageUsers()          │
│ + getReports()           │
└──────────────────────────┘


┌──────────────────────────┐
│         Product          │
├──────────────────────────┤
│ - id: UUID               │
│ - name: String           │
│ - description: Text      │
│ - price: Decimal         │
│ - currency: Currency     │
│ - stockQuantity: Int     │
│ - weight: Decimal?       │
│ - isActive: Boolean      │
│ - createdAt: DateTime    │
├──────────────────────────┤
│ + updateStock(qty: Int)  │
│ + getRating(): Decimal   │
└──────────────────────────┘

┌──────────────────────────┐
│        Category          │
├──────────────────────────┤
│ - id: UUID               │
│ - name: String           │
│ - slug: String [unique]  │
│ - description: Text?     │
│ - parentCategory: UUID?  │
│ - sortOrder: Int         │
└──────────────────────────┘

┌──────────────────────────┐
│         Order            │
├──────────────────────────┤
│ - id: UUID               │
│ - orderNumber: String    │
│ - status: OrderStatus    │
│ - totalAmount: Decimal   │
│ - currency: Currency     │
│ - shippingCost: Decimal  │
│ - discount: Decimal      │
│ - createdAt: DateTime    │
│ - paidAt: DateTime?      │
│ - shippedAt: DateTime?   │
├──────────────────────────┤
│ + calculateTotal()       │
│ + pay()                  │
│ + cancel()               │
│ + addItem(product, qty)  │
└──────────────────────────┘

┌──────────────────────────┐
│       OrderItem          │
├──────────────────────────┤
│ - id: UUID               │
│ - productId: UUID        │
│ - productName: String    │
│ - quantity: Int          │
│ - unitPrice: Decimal     │
│ - totalPrice: Decimal    │
│ - createdAt: DateTime    │
└──────────────────────────┘

┌──────────────────────────┐
│        Payment           │
├──────────────────────────┤
│ - id: UUID               │
│ - orderId: UUID          │
│ - amount: Decimal        │
│ - currency: Currency     │
│ - status: PaymentStatus  │
│ - method: PaymentMethod  │
│ - transactionId: String? │
│ - paidAt: DateTime?      │
│ - gatewayResponse: JSON? │
├──────────────────────────┤
│ + process()              │
│ + refund()               │
└──────────────────────────┘

┌──────────────────────────┐
│        Address           │
├──────────────────────────┤
│ - id: UUID               │
│ - label: String          │
│ - country: String        │
│ - city: String           │
│ - street: String         │
│ - building: String       │
│ - apartment: String?     │
│ - zipCode: String        │
│ - isDefault: Boolean     │
└──────────────────────────┘

┌──────────────────────────┐
│        Review            │
├──────────────────────────┤
│ - id: UUID               │
│ - rating: Int (1..5)     │
│ - title: String?         │
│ - text: Text?            │
│ - isVerified: Boolean    │
│ - createdAt: DateTime    │
├──────────────────────────┤
│ + edit(text)             │
└──────────────────────────┘

┌──────────────────────────┐
│         Cart             │
├──────────────────────────┤
│ - id: UUID               │
│ - createdAt: DateTime    │
│ - expiresAt: DateTime    │
├──────────────────────────┤
│ + addItem(product, qty)  │
│ + removeItem(product)    │
│ + calculateTotal()       │
│ + checkout(): Order      │
└──────────────────────────┘

┌──────────────────────────┐
│       CartItem           │
├──────────────────────────┤
│ - id: UUID               │
│ - quantity: Int          │
│ - addedAt: DateTime      │
└──────────────────────────┘

┌──────────────────────────┐
│   ProductCategory        │
├──────────────────────────┤
│ - productId: UUID        │
│ - categoryId: UUID       │
└──────────────────────────┘

5.3. Связи между классами

Класс A Связь Класс B Кратность Пояснение
User △— AdminUser Наследование: AdminUser — частный случай User
User ●— Order 1 — 0..* User создаёт заказы. Заказ не существует без User? Нет, существует (для архива). → на самом деле Association, не Composition.
User Address 1 — 0..* У пользователя может быть несколько адресов (или ни одного)
User Review 1 — 0..* Пользователь пишет отзывы
User ●— Cart 1 — 0..1 У пользователя одна корзина. Если пользователь удалён — корзина удаляется. Composition.
Cart ●— CartItem 1 — 0..* Корзина содержит позиции. Composition.
Product Review 1 — 0..* У товара много отзывов
Product * — * Category * — * Товар может быть в нескольких категориях. M:N — через ProductCategory
Product OrderItem 1 — 0..* Товар упоминается в позициях заказа
Order ●— OrderItem 1 — 0..* Composition! Позиция не существует без заказа
Order Payment 1 — 0..1 Заказ может иметь один платёж (опционально)
Order Address 0..1 — 0..* Адрес доставки (может быть общим для нескольких заказов)

5.4. Ключевые решения в этой модели

  1. Composition (Order ●— OrderItem): Позиция заказа не имеет смысла без заказа. Её не может быть в корзине и в заказе одновременно. Если заказ удаляется — позиции удаляются.

  2. Association (User — Order): Заказ — не «часть» пользователя. Заказ живёт своей жизнью (архив). Поэтому не Composition, не Aggregation — простая Association.

  3. Many-to-Many (Product * — * Category): Товар может быть одновременно в категориях «Электроника», «Apple», «Акции». Категория может содержать много товаров. Требуется промежуточная таблица ProductCategory.

  4. Composition (Cart ●— CartItem): Корзина — временная сущность. Если корзина очищена — CartItem удалены.

  5. Generalization (User △— AdminUser): Администратор — это пользователь с расширенными правами. У AdminUser есть методы manageUsers(), getReports(), которых нет у обычного User.


6. Class Diagram в PlantUML

6.1. Полный код для модели интернет-магазина

@startuml
skinparam backgroundColor #FEFEFE
skinparam classBackgroundColor #F8F9FA
title Интернет-магазин — модель данных

' ========== Перечисления ==========
enum OrderStatus {
    PENDING
    PAID
    PROCESSING
    SHIPPED
    DELIVERED
    CANCELLED
    REFUNDED
}

enum PaymentStatus {
    PENDING
    SUCCEEDED
    FAILED
    REFUNDED
}

enum PaymentMethod {
    CREDIT_CARD
    DEBIT_CARD
    APPLE_PAY
    GOOGLE_PAY
    PAYPAL
}

enum UserRole {
    CUSTOMER
    ADMIN
    MODERATOR
}

enum Currency {
    RUB
    USD
    EUR
}

' ========== Классы ==========
class User {
    - id: UUID
    - email: String {unique}
    - passwordHash: String
    - name: String
    - phone: String? {optional}
    - role: UserRole
    - createdAt: DateTime
    - updatedAt: DateTime
    + register(email, password): User
    + updateProfile(name, phone)
    + getOrders(): List<Order>
    + addAddress(address): Address
}

class AdminUser {
    - accessLevel: Int
    + manageProducts()
    + manageUsers()
    + getReports(): Report
}

class Product {
    - id: UUID
    - name: String
    - description: Text
    - price: Decimal
    - currency: Currency
    - stockQuantity: Int
    - weight: Decimal? {optional}
    - isActive: Boolean
    - createdAt: DateTime
    + updateStock(quantity: Int)
    + getRating(): Decimal
    + getReviews(): List<Review>
}

class Category {
    - id: UUID
    - name: String
    - slug: String {unique}
    - description: Text? {optional}
    - parentCategory: UUID? {optional}
    - sortOrder: Int
    + getChildCategories(): List<Category>
}

class ProductCategory {
    - productId: UUID
    - categoryId: UUID
}

class Order {
    - id: UUID
    - orderNumber: String {unique}
    - status: OrderStatus
    - totalAmount: Decimal {readOnly}
    - currency: Currency
    - shippingCost: Decimal
    - discount: Decimal
    - createdAt: DateTime
    - paidAt: DateTime? {optional}
    - shippedAt: DateTime? {optional}
    + calculateTotal(): Decimal
    + pay(method: PaymentMethod): Payment
    + cancel(): Boolean
    + addItem(product: Product, quantity: Int): OrderItem
}

class OrderItem {
    - id: UUID
    - productName: String {readOnly}
    - quantity: Int
    - unitPrice: Decimal {readOnly}
    - totalPrice: Decimal {readOnly}
}

class Payment {
    - id: UUID
    - amount: Decimal
    - currency: Currency
    - status: PaymentStatus
    - method: PaymentMethod
    - transactionId: String? {optional}
    - gatewayResponse: JSON? {optional}
    - paidAt: DateTime? {optional}
    + process(): Boolean
    + refund(): Refund
}

class Address {
    - id: UUID
    - label: String
    - country: String
    - city: String
    - street: String
    - building: String
    - apartment: String? {optional}
    - zipCode: String
    - isDefault: Boolean
    + validate(): Boolean
}

class Review {
    - id: UUID
    - rating: Int {range 1..5}
    - title: String? {optional}
    - text: Text? {optional}
    - isVerifiedPurchase: Boolean
    - createdAt: DateTime
    + edit(text: Text)
}

class Cart {
    - id: UUID
    - createdAt: DateTime
    - expiresAt: DateTime
    + addItem(product: Product, quantity: Int)
    + removeItem(productId: UUID)
    + calculateTotal(): Decimal
    + checkout(): Order
    + clear()
}

class CartItem {
    - id: UUID
    - quantity: Int
    - addedAt: DateTime
}

' ========== Связи ==========
' Наследование
User <|-- AdminUser

' Композиции
Cart *-- CartItem : contains
Order *-- OrderItem : contains

' Ассоциации
User "1" -- "0..*" Order : creates
User "1" -- "0..*" Address : has
User "1" -- "0..*" Review : writes
User "1" -- "0..1" Cart : owns

Product "1" -- "0..*" Review : has
Product "1" -- "0..*" OrderItem : referencedIn

Order "1" -- "0..1" Payment : has
Order "0..1" -- "0..*" Address : deliveredTo

' Many-to-Many через связку
Product "*" -- "*" Category : categorized
ProductCategory .. Product : связка
ProductCategory .. Category : связка

' Зависимость
class ReportGenerator {
    + generateSalesReport(period): Report
}
ReportGenerator ..> Order : временно использует
ReportGenerator ..> Product : временно использует

@enduml

6.2. Ключевые моменты PlantUML для Class Diagram

Конструкция Синтаксис Результат
Класс class MyClass { } Прямоугольник класса
Атрибут + name: String С видимостью и типом
Атрибут со значением - count: Int = 0 Со значением по умолчанию
Метод + getOrders(): List<Order> С возвращаемым типом
Ассоциация A -- B Простая линия
Наследование `Parent < -- Child`
Композиция Whole *-- Part Залитый ромб
Агрегация Whole o-- Part Пустой ромб
Зависимость A ..> B Пунктирная стрелка
Реализация `Interface < .. Class`
Кратность "1" -- "0..*" В кавычках на концах связи
Надпись на связи -- : текст Двоеточие и текст
Enum enum Name { VALUE1; VALUE2 } Перечисление
Пакет package "Name" { } Группировка классов
Цвет class MyClass #FFAAAA HEX или named color

6.3. Рекомендации по оформлению

  1. Порядок атрибутов: сначала id, потом обязательные поля, потом опциональные (?), потом временные метки
  2. Порядок методов: сначала конструкторы, потом бизнес-методы, потом getters/setters (геттеры/сеттеры обычно опускают)
  3. Кратность: всегда указывайте на обоих концах связи (даже если это "1")
  4. Имена на связях: короткие, со смыслом (owns, contains, creates)
  5. Группировка: используйте package для логического разделения (Core, Orders, Payments)

7. Class Diagram → SQL-трансляция

7.1. Правила трансляции

Элемент Class Diagram SQL DDL
Класс CREATE TABLE
Атрибут Колонка (с типом)
id: UUID id UUID PRIMARY KEY
email: String [unique] email VARCHAR(255) UNIQUE NOT NULL
name: String? name VARCHAR(255) NULL
createdAt: DateTime created_at TIMESTAMP NOT NULL DEFAULT NOW()
Association A 1 — * B FOREIGN KEY в B
Composition A ●— * B FOREIGN KEY + ON DELETE CASCADE
Aggregation A ◇— * B FOREIGN KEY + ON DELETE SET NULL
Generalization Одна таблица с type-полем / отдельные таблицы
Many-to-Many Промежуточная таблица

7.2. Примеры SQL из Class Diagram

Composition (Order ●— OrderItem):

CREATE TABLE orders (
    id UUID PRIMARY KEY,
    order_number VARCHAR(50) UNIQUE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    total_amount DECIMAL(10,2) NOT NULL,
    currency VARCHAR(3) NOT NULL DEFAULT 'RUB',
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE order_items (
    id UUID PRIMARY KEY,
    order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    product_name VARCHAR(255) NOT NULL,
    quantity INT NOT NULL CHECK (quantity > 0),
    unit_price DECIMAL(10,2) NOT NULL,
    total_price DECIMAL(10,2) NOT NULL
);
-- CASCADE: если заказ удалён — позиции удалены

Association (User — Order):

CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE orders (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id),
    -- Нет CASCADE — заказ не удаляется при удалении пользователя
    ...
);

Many-to-Many (Product — Category):

CREATE TABLE products (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price DECIMAL(10,2) NOT NULL
);

CREATE TABLE categories (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE product_categories (
    product_id UUID REFERENCES products(id) ON DELETE CASCADE,
    category_id UUID REFERENCES categories(id) ON DELETE CASCADE,
    PRIMARY KEY (product_id, category_id)
);

Generalization (User — AdminUser) — вариант с одной таблицей:

CREATE TABLE users (
    id UUID PRIMARY KEY,
    type VARCHAR(20) NOT NULL CHECK (type IN ('user', 'admin')),
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(255) NOT NULL,
    access_level INT NULL,    -- только для admin
    role VARCHAR(20) NOT NULL DEFAULT 'customer',
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

8. Пошаговый метод построения Class Diagram

Шаг 1: Выделите сущности из требований

Техника: прочитайте требования, выпишите все существительные.

Вопросы:

  • Какие «объекты» упоминаются?
  • Какие данные система хранит?
  • Какие отчёты/списки нужно показывать пользователю?

Результат: список кандидатов в классы.

Шаг 2: Отфильтруйте кандидатов

Если кандидат… То…
Имеет 2+ атрибута Скорее всего, класс
Имеет своё поведение (методы) Скорее всего, класс
Встречается в списке (множество экземпляров) Скорее всего, класс
Это просто свойство (имя, цвет, размер) Атрибут другого класса
Это действие (регистрация, оплата) Метод или отдельный Use Case

Результат: финальный список классов.

Шаг 3: Добавьте атрибуты

Для каждого класса:

  1. Идентификатор: id: UUID
  2. Основные поля: по описанию (имя, описание, цена)
  3. Статусы: если есть конечный набор состояний — Enum
  4. Внешние ключи: для связи с другими классами (явно или через связь)
  5. Временные метки: createdAt, updatedAt

Результат: у каждого класса есть 3–7 атрибутов.

Шаг 4: Определите связи

Для каждой пары классов спросите:

  1. Связаны ли они? Если нет — не рисуйте связь.
  2. Какая кратность? Сколько А у B, сколько B у A.
  3. Это «часть-целое»? Если да — Composition или Aggregation.
  4. Это «является»? Если да — Generalization.

Результат: все классы связаны, кратность указана.

Шаг 5: Назначьте Composition / Aggregation

Главный вопрос: «Если целое удалить — часть должна удалиться?»

  • Да → Composition
  • Нет → можно Aggregation или Association

Не уверены? Используйте простую Association (без ромба).

Шаг 6: Валидируйте

Чек-лист:

  • Каждая сущность из требований отражена
  • Нет лишних классов (которые ничего не добавляют)
  • Кратность на обоих концах каждой связи
  • Composition только там, где часть не живёт без целого
  • Generalization обоснована (общее поведение, не только атрибуты)
  • Many-to-Many решены (промежуточная таблица или явно отмечены)
  • Атрибуты имеют типы
  • Диаграмма читаема (не более 20 классов → разбить на пакеты)

9. Распространённые ошибки

Ошибка 1: Атрибут как отдельный класс

⚠️ Неправильно:
    ┌───────────┐    ┌────────────┐
    │   User    │    │    Email   │
    ├───────────┤    ├────────────┤
    │           │    │ - address  │
    └───────────┘    └────────────┘

✅ Правильно:
    ┌───────────┐
    │   User    │
    ├───────────┤
    │ - email   │
    │   String  │
    └───────────┘

Правило: Если у «атрибута» нет собственных атрибутов — он не класс.

Ошибка 2: Composition вместо Association

⚠️ Неправильно: User ●── Order
    (Пользователь «состоит из» заказов? Нет!)
✅ Правильно: User —— Order
    (Пользователь «создаёт» заказы — простая ассоциация)

Правило: Залитый ромб — только когда часть НЕ может существовать без целого.

Ошибка 3: Неправильная кратность

⚠️ Неправильно: User "1" —— "1" Address
    (У пользователя ровно один адрес — а если два?)
✅ Правильно: User "1" —— "0..*" Address
    (У пользователя 0, 1 или много адресов)

Правило: Всегда задавайте себе вопрос «А сколько может быть на самом деле?»

Ошибка 4: Пропущенная связка для Many-to-Many

⚠️ Неправильно в SQL:
    Product *——* Category  (нет таблицы связки — ошибка)
✅ Правильно:
    Product "1" —— * ProductCategory * —— "1" Category

Правило: M:N в реляционной БД требует промежуточной таблицы.

Ошибка 5: Наследование для ролей

⚠️ Неправильно:
    User △── Customer
    User △── Admin
    (Customer и Admin — роли, а не подклассы)
✅ Правильно:
    User { role: UserRole }
    enum UserRole { CUSTOMER, ADMIN }

Правило: Наследование — для разного поведения, не для разных ролей.

Ошибка 6: Всё в одной диаграмме

Симптом: 30+ классов на одной диаграмме, стрелки пересекаются.

Решение: Разбейте на пакеты (модули):

  • package "Users" { User, AdminUser, Address }
  • package "Catalog" { Product, Category, Review }
  • package "Orders" { Order, OrderItem, Payment }

10. Чек-лист: готовая Class Diagram

Критерий Как проверить
1 Каждый класс отражает сущность предметной области Сверка с требованиями
2 У каждого класса есть идентификатор (id: UUID) Визуально
3 Атрибуты имеют типы (String, Int, Decimal, DateTime…) Визуально
4 Кратность указана на обоих концах всех связей Визуально
5 Composition только где часть не живёт без целого Проверка вопросом
6 Aggregation только где часть может жить без целого Проверка вопросом
7 Many-to-Many решены (явная связка или отмечены) Проверка
8 Наследование обосновано, не имитирует роли Анализ методов
9 Нет классов-атрибутов (дублирующих поля) Проверка
10 Диаграмма не перегружена (если >20 кл. → пакеты) Счёт
11 Диаграмма транслируется в SQL DDL Тест-трансляция
12 Связи имеют имена (role names) — опционально, но желательно Визуально

11. Вопросы для самопроверки

  1. Из каких трёх секций состоит прямоугольник класса?
  2. Чем Composition отличается от Aggregation? Приведите пример из жизни.
  3. Что означает кратность 0..*? А 1..1? А *?
  4. Как на Class Diagram показать, что один класс наследует другой?
  5. Чем Association отличается от Dependency?
  6. Что такое Realization и когда она используется?
  7. Как транслировать Class Diagram в схему реляционной БД?
  8. Почему наследование не подходит для моделирования ролей (Customer, Admin)?
  9. Какие атрибуты почти всегда есть у любого класса-сущности?
  10. Как на Class Diagram показать, что задача может быть назначена на пользователя, но не обязательно?
  11. Что такое «промежуточная таблица» и когда она нужна?
  12. Как разбить большую Class Diagram на пакеты?

12. Практическое задание

Кейс: Система управления задачами (Task Manager) — domain model

Спроектируйте доменную модель системы управления задачами.

Требования (фрагмент):

  1. Пользователь может создавать задачи
  2. Задача имеет название, описание, приоритет (низкий/средний/высокий/критический), статус (новая/в работе/выполнена/заблокирована), дедлайн
  3. Задача может быть назначена на одного исполнителя (пользователя)
  4. У задачи может быть несколько меток (тэгов) — например, «баг», «фича», «документация»
  5. К задаче можно оставлять комментарии
  6. Задачи группируются в проекты. Проект имеет название, описание, владельца
  7. Пользователь может участвовать в нескольких проектах
  8. У проекта может быть несколько задач
  9. Метка едина для всех проектов (один список меток)
  10. Администратор системы (AdminUser) может управлять пользователями
  11. У каждого пользователя есть настройки уведомлений (вкл/выкл, email/push)

Задание 1. Постройте Class Diagram

Создайте Class Diagram для этой системы.

Требования:

  • Минимум 8 классов (User, AdminUser, Project, Task, Comment, Label, TaskLabel, UserSettings)
  • Минимум 3 enum (Priority, TaskStatus, UserRole)
  • Composition: Project → Task, Task → Comment
  • Aggregation: Project — User (участники)
  • Generalization: User △ AdminUser
  • Many-to-Many: Task — Label
  • Ассоциации: Task → User (автор), Task → User (исполнитель), User → UserSettings
  • Кратность на каждом конце связи
  • Атрибуты с типами и видимостью

Задание 2. PlantUML-код

Напишите полный PlantUML-код Class Diagram.

Требования:

  • @startuml / @enduml с заголовком
  • enum для перечислений
  • class для всех классов
  • Все связи с корректной кратностью в кавычках
  • Composition: *--
  • Aggregation: o--
  • Generalization: <|--
  • Many-to-Many через промежуточный класс TaskLabel
  • package для группировки (User Management, Tasks, Commons)

Задание 3. SQL-трансляция

Напишите SQL DDL (CREATE TABLE) для следующих сущностей:

  1. users
  2. tasks
  3. comments
  4. task_labels (промежуточная таблица)
  5. user_settings

Требования:

  • PRIMARY KEY
  • FOREIGN KEY с корректным ON DELETE (CASCADE для Composition, SET NULL или без указания для Association)
  • NOT NULL / NULL
  • UNIQUE если нужно
  • DEFAULT значения

Задание 4. Анализ

Ответьте письменно:

  1. Почему Comment — Composition по отношению к Task? Может ли комментарий существовать без задачи?
  2. Почему Project — Composition по отношению к Task? Что произойдёт с задачами при удалении проекта? Корректно ли это с точки зрения бизнеса?
  3. Почему Project — Aggregation по отношению к User? А не Composition?
  4. В чём разница между связями Task → User (исполнитель) и Task → User (автор)? Это разные ассоциации или одна?
  5. Как изменится модель, если задача может быть назначена на нескольких исполнителей?
  6. Как изменится модель, если метка может быть привязана к задаче только один раз? (Подсказка: подумайте про UNIQUE-ограничение в БД)

Задание 5. Найдите ошибки

Даны 3 фрагмента Class Diagram с ошибками. Определите, что не так:

Фрагмент A:

┌────────┐          ┌─────────────┐
│  User  │          │   Address   │
├────────┤          ├─────────────┤
│ - name │ ●─────── │ - city      │
│ - email│          │ - street    │
└────────┘          │ - zip       │
                    └─────────────┘

Фрагмент B:

┌────────┐          ┌────────┐
│  User  │          │  Role  │
├────────┤          ├────────┤
│ - id   │ △─────── │ - name │
│ - name │          │ - permissions│
└────────┘          └────────┘

Фрагмент C:

User "1" —— "1" Task
(без других пояснений)

13. Дополнительные материалы

  • Книга: Martin Fowler — «UML Distilled», 3-е издание, глава 3 «Class Diagrams» (классика, 35 страниц)
  • Книга: Craig Larman — «Applying UML and Patterns. An Introduction to Object-Oriented Analysis and Design and Iterative Development» (главы 8–14 по Domain Model — лучшее по теме)
  • Книга: Eric Evans — «Domain-Driven Design» (глава 5 «Entities» и глава 6 «Value Objects» — для глубокого понимания, что должно быть классом, а что — атрибутом)
  • Спецификация: OMG UML 2.5.1, раздел 9 «Classifiers» и раздел 11 «Classes» — формальное определение (только если нужно сослаться на стандарт)
  • PlantUML: plantuml.com/class-diagram — официальная документация с примерами
  • Tool: draw.io (diagrams.net) → UML → Class Diagram — бесплатный редактор
  • Tool: IntelliJ IDEA / Eclipse — reverse engineering: сгенерировать Class Diagram из Java-кода
  • Шпаргалка: «UML Class Diagram Relationships Cheat Sheet» — поищите в Google Images для одностраничной памятки по связям
  • Статья: «Association, Aggregation, Composition — what's the difference?» на lucidchart.com — коротко и наглядно
  • Практика: Генерация PlantUML из Java/Kotlin кода — плагины для IntelliJ (PlantUML Integration, SimpleUML)

Следующий модуль: 05 — Моделирование в BPMN

📚 Материалы модуля

🖼️ Схема и инфографика

🎬 Видео-лекция

🎬 UML Универсальные чертежи

📄 Дополнительные материалы (PDF)

📄The UML Blueprint
Скачать
Спросить ИИ