Урок 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 - Избегать сокращений:
CustAddr→CustomerAddress - Язык: на русском для аналитика (допустимо для документирования), на английском для кода
- UpperCamelCase: первая буква каждого слова заглавная
Как выделить классы из требований:
- Прочитайте тексты требований и User Stories
- Выпишите все именные существительные (реальные объекты)
- Отфильтруйте:
- Будут классами: встречаются многократно, имеют атрибуты и поведение
- Будут атрибутами: описывают свойство другого объекта («имя», «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. Ключевые решения в этой модели
-
Composition (Order ●— OrderItem): Позиция заказа не имеет смысла без заказа. Её не может быть в корзине и в заказе одновременно. Если заказ удаляется — позиции удаляются.
-
Association (User — Order): Заказ — не «часть» пользователя. Заказ живёт своей жизнью (архив). Поэтому не Composition, не Aggregation — простая Association.
-
Many-to-Many (Product * — * Category): Товар может быть одновременно в категориях «Электроника», «Apple», «Акции». Категория может содержать много товаров. Требуется промежуточная таблица
ProductCategory. -
Composition (Cart ●— CartItem): Корзина — временная сущность. Если корзина очищена — CartItem удалены.
-
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. Рекомендации по оформлению
- Порядок атрибутов: сначала
id, потом обязательные поля, потом опциональные (?), потом временные метки - Порядок методов: сначала конструкторы, потом бизнес-методы, потом getters/setters (геттеры/сеттеры обычно опускают)
- Кратность: всегда указывайте на обоих концах связи (даже если это
"1") - Имена на связях: короткие, со смыслом (
owns,contains,creates) - Группировка: используйте
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: Добавьте атрибуты
Для каждого класса:
- Идентификатор:
id: UUID - Основные поля: по описанию (имя, описание, цена)
- Статусы: если есть конечный набор состояний — Enum
- Внешние ключи: для связи с другими классами (явно или через связь)
- Временные метки:
createdAt,updatedAt
Результат: у каждого класса есть 3–7 атрибутов.
Шаг 4: Определите связи
Для каждой пары классов спросите:
- Связаны ли они? Если нет — не рисуйте связь.
- Какая кратность? Сколько А у B, сколько B у A.
- Это «часть-целое»? Если да — Composition или Aggregation.
- Это «является»? Если да — 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. Вопросы для самопроверки
- Из каких трёх секций состоит прямоугольник класса?
- Чем Composition отличается от Aggregation? Приведите пример из жизни.
- Что означает кратность
0..*? А1..1? А*? - Как на Class Diagram показать, что один класс наследует другой?
- Чем Association отличается от Dependency?
- Что такое Realization и когда она используется?
- Как транслировать Class Diagram в схему реляционной БД?
- Почему наследование не подходит для моделирования ролей (Customer, Admin)?
- Какие атрибуты почти всегда есть у любого класса-сущности?
- Как на Class Diagram показать, что задача может быть назначена на пользователя, но не обязательно?
- Что такое «промежуточная таблица» и когда она нужна?
- Как разбить большую Class Diagram на пакеты?
12. Практическое задание
Кейс: Система управления задачами (Task Manager) — domain model
Спроектируйте доменную модель системы управления задачами.
Требования (фрагмент):
- Пользователь может создавать задачи
- Задача имеет название, описание, приоритет (низкий/средний/высокий/критический), статус (новая/в работе/выполнена/заблокирована), дедлайн
- Задача может быть назначена на одного исполнителя (пользователя)
- У задачи может быть несколько меток (тэгов) — например, «баг», «фича», «документация»
- К задаче можно оставлять комментарии
- Задачи группируются в проекты. Проект имеет название, описание, владельца
- Пользователь может участвовать в нескольких проектах
- У проекта может быть несколько задач
- Метка едина для всех проектов (один список меток)
- Администратор системы (AdminUser) может управлять пользователями
- У каждого пользователя есть настройки уведомлений (вкл/выкл, 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) для следующих сущностей:
userstaskscommentstask_labels(промежуточная таблица)user_settings
Требования:
- PRIMARY KEY
- FOREIGN KEY с корректным ON DELETE (CASCADE для Composition, SET NULL или без указания для Association)
- NOT NULL / NULL
- UNIQUE если нужно
- DEFAULT значения
Задание 4. Анализ
Ответьте письменно:
- Почему
Comment— Composition по отношению кTask? Может ли комментарий существовать без задачи? - Почему
Project— Composition по отношению кTask? Что произойдёт с задачами при удалении проекта? Корректно ли это с точки зрения бизнеса? - Почему
Project— Aggregation по отношению кUser? А не Composition? - В чём разница между связями Task → User (исполнитель) и Task → User (автор)? Это разные ассоциации или одна?
- Как изменится модель, если задача может быть назначена на нескольких исполнителей?
- Как изменится модель, если метка может быть привязана к задаче только один раз? (Подсказка: подумайте про 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