UML -- стандартизированная нотация для визуального моделирования программных систем. Разработана в 1990-х, стандарт OMG. Включает 14 типов диаграмм, но на практике используются 4-5 основных.
Какие диаграммы реально нужны
Диаграмма
Когда
Частота использования
Class Diagram
Структура домена, ООП дизайн
Высокая
Sequence Diagram
Взаимодействие компонентов
Высокая
Component Diagram
Архитектура модулей
Средняя
Activity Diagram
Бизнес-процессы, алгоритмы
Средняя
State Machine
Состояния объекта
Низкая
Use Case Diagram
Функциональные требования
Низкая
Class Diagram
Class diagram показывает классы, их атрибуты, методы и связи.
<?php
declare(strict_types=1);
namespace App\Domain\Order;
interface OrderRepository
{
public function findById(string $id): ?Order;
public function save(Order $order): void;
public function delete(string $id): void;
}
enum OrderStatus: string
{
case Pending = 'pending';
case Confirmed = 'confirmed';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
final class Order
{
/** @var array<OrderItem> */
private array $items = [];
public function __construct(
private readonly string $id,
private OrderStatus $status,
private Money $total,
private readonly \DateTimeImmutable $createdAt,
) {}
public function addItem(OrderItem $item): void
{
$this->items[] = $item;
$this->recalculateTotal();
}
public function cancel(): void
{
if ($this->status === OrderStatus::Shipped) {
throw new \DomainException('Cannot cancel shipped order');
}
$this->status = OrderStatus::Cancelled;
}
private function recalculateTotal(): void
{
$sum = 0;
foreach ($this->items as $item) {
$sum += $item->getSubtotal();
}
$this->total = new Money($sum);
}
}
final readonly class OrderItem
{
public function __construct(
private string $product,
private int $quantity,
private Money $price,
) {}
public function getSubtotal(): int
{
return $this->price->getAmount() * $this->quantity;
}
}
package order
import (
"errors"
"time"
)
// OrderRepository defines the persistence interface for orders.
type OrderRepository interface {
FindByID(id string) (*Order, error)
Save(order *Order) error
Delete(id string) error
}
// OrderStatus represents the lifecycle state of an order.
type OrderStatus string
const (
StatusPending OrderStatus = "pending"
StatusConfirmed OrderStatus = "confirmed"
StatusShipped OrderStatus = "shipped"
StatusDelivered OrderStatus = "delivered"
StatusCancelled OrderStatus = "cancelled"
)
// Order is the aggregate root for the order domain.
type Order struct {
id string
status OrderStatus
total int // minor currency units
items []OrderItem
createdAt time.Time
}
// NewOrder creates a new order.
func NewOrder(id string, status OrderStatus, total int, createdAt time.Time) *Order {
return &Order{id: id, status: status, total: total, createdAt: createdAt}
}
// AddItem adds a line item and recalculates the total.
func (o *Order) AddItem(item OrderItem) {
o.items = append(o.items, item)
o.recalculateTotal()
}
// Cancel transitions the order to cancelled state.
func (o *Order) Cancel() error {
if o.status == StatusShipped {
return errors.New("cannot cancel shipped order")
}
o.status = StatusCancelled
return nil
}
func (o *Order) recalculateTotal() {
sum := 0
for _, item := range o.items {
sum += item.Subtotal()
}
o.total = sum
}
// OrderItem represents a line item in an order.
type OrderItem struct {
Product string
Quantity int
Price int // minor currency units
}
// Subtotal returns price * quantity.
func (i OrderItem) Subtotal() int {
return i.Price * i.Quantity
}
namespace App.Domain.Order;
/// Defines the persistence contract for orders.
public interface IOrderRepository
{
Order? FindById(string id);
void Save(Order order);
void Delete(string id);
}
/// Represents the lifecycle state of an order.
public enum OrderStatus
{
Pending,
Confirmed,
Shipped,
Delivered,
Cancelled,
}
/// Value object holding an amount in minor currency units.
public readonly record struct Money(int Amount)
{
public static Money operator *(Money money, int multiplier) => new(money.Amount * multiplier);
}
/// Aggregate root for the order domain.
public sealed class Order
{
private readonly List<OrderItem> _items = [];
public Order(string id, OrderStatus status, Money total, DateTimeOffset createdAt)
{
Id = id;
Status = status;
Total = total;
CreatedAt = createdAt;
}
public string Id { get; }
public OrderStatus Status { get; private set; }
public Money Total { get; private set; }
public DateTimeOffset CreatedAt { get; }
public IReadOnlyList<OrderItem> Items => _items;
// Add a line item and recalculate the total.
public void AddItem(OrderItem item)
{
_items.Add(item);
RecalculateTotal();
}
// Transition the order to the cancelled state.
public void Cancel()
{
if (Status is OrderStatus.Shipped)
{
throw new InvalidOperationException("Cannot cancel shipped order");
}
Status = OrderStatus.Cancelled;
}
private void RecalculateTotal() => Total = new Money(_items.Sum(item => item.Subtotal.Amount));
}
/// Represents a line item in an order.
public sealed record OrderItem(string Product, int Quantity, Money Price)
{
public Money Subtotal => Price * Quantity;
}
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Protocol
class OrderStatus(str, Enum):
"""Represents the lifecycle state of an order."""
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class OrderError(Exception):
"""Domain error raised on invalid order transitions."""
@dataclass(frozen=True, slots=True)
class Money:
"""Value object holding an amount in minor currency units."""
amount: int
def __mul__(self, multiplier: int) -> "Money":
return Money(self.amount * multiplier)
@dataclass(frozen=True, slots=True)
class OrderItem:
"""Represents a line item in an order."""
product: str
quantity: int
price: Money
@property
def subtotal(self) -> Money:
return self.price * self.quantity
@dataclass(slots=True)
class Order:
"""Aggregate root for the order domain."""
id: str
status: OrderStatus
total: Money
created_at: datetime
items: list[OrderItem] = field(default_factory=list)
def add_item(self, item: OrderItem) -> None:
"""Add a line item and recalculate the total."""
self.items.append(item)
self._recalculate_total()
def cancel(self) -> None:
"""Transition the order to the cancelled state."""
if self.status is OrderStatus.SHIPPED:
raise OrderError("Cannot cancel shipped order")
self.status = OrderStatus.CANCELLED
def _recalculate_total(self) -> None:
self.total = Money(sum(item.subtotal.amount for item in self.items))
class OrderRepository(Protocol):
# Python has no interfaces; typing.Protocol gives the same structural
# contract the UML diagram expresses with <<interface>>.
def find_by_id(self, order_id: str) -> Order | None: ...
def save(self, order: Order) -> None: ...
def delete(self, order_id: str) -> None: ...
## Sequence Diagram
Sequence diagram показывает взаимодействие объектов во времени (кто кого вызывает и в каком порядке).
State Machine показывает состояния объекта и переходы между ними.
<?php
declare(strict_types=1);
namespace App\Domain\Order;
/**
* Order state machine:
*
* (●) ──▶ [Pending] ──confirm()──▶ [Confirmed]
* │ │
* cancel() ship()
* │ │
* ▼ ▼
* [Cancelled] [Shipped]
* │
* deliver()
* │
* ▼
* [Delivered]
*/
final class OrderStateMachine
{
private const TRANSITIONS = [
'pending' => ['confirm' => 'confirmed', 'cancel' => 'cancelled'],
'confirmed' => ['ship' => 'shipped', 'cancel' => 'cancelled'],
'shipped' => ['deliver' => 'delivered'],
'delivered' => [],
'cancelled' => [],
];
public function transition(OrderStatus $current, string $action): OrderStatus
{
$allowed = self::TRANSITIONS[$current->value] ?? [];
if (!isset($allowed[$action])) {
throw new \DomainException(
sprintf('Cannot perform "%s" on order in "%s" status', $action, $current->value),
);
}
return OrderStatus::from($allowed[$action]);
}
/**
* Get available actions for current state.
*
* @return array<string>
*/
public function availableActions(OrderStatus $current): array
{
return array_keys(self::TRANSITIONS[$current->value] ?? []);
}
}
package order
import "fmt"
// OrderStateMachine manages order state transitions.
type OrderStateMachine struct{}
// transitions maps state -> action -> next state.
var transitions = map[OrderStatus]map[string]OrderStatus{
StatusPending: {"confirm": StatusConfirmed, "cancel": StatusCancelled},
StatusConfirmed: {"ship": StatusShipped, "cancel": StatusCancelled},
StatusShipped: {"deliver": StatusDelivered},
StatusDelivered: {},
StatusCancelled: {},
}
// Transition applies an action to the current state.
func (OrderStateMachine) Transition(current OrderStatus, action string) (OrderStatus, error) {
allowed, ok := transitions[current]
if !ok {
return "", fmt.Errorf("unknown state: %s", current)
}
next, ok := allowed[action]
if !ok {
return "", fmt.Errorf("cannot perform %q on order in %q status", action, current)
}
return next, nil
}
// AvailableActions returns the valid actions for the current state.
func (OrderStateMachine) AvailableActions(current OrderStatus) []string {
allowed := transitions[current]
actions := make([]string, 0, len(allowed))
for action := range allowed {
actions = append(actions, action)
}
return actions
}
namespace App.Domain.Order;
public enum OrderAction
{
Confirm,
Cancel,
Ship,
Deliver,
}
/// Order state machine:
///
/// (●) ──▶ [Pending] ──Confirm──▶ [Confirmed]
/// │ │
/// Cancel Ship
/// │ │
/// ▼ ▼
/// [Cancelled] [Shipped]
/// │
/// Deliver
/// │
/// ▼
/// [Delivered]
public sealed class OrderStateMachine
{
// Modelling actions as an enum makes illegal action names unrepresentable.
private static readonly IReadOnlyDictionary<OrderStatus, IReadOnlyDictionary<OrderAction, OrderStatus>> Transitions =
new Dictionary<OrderStatus, IReadOnlyDictionary<OrderAction, OrderStatus>>
{
[OrderStatus.Pending] = new Dictionary<OrderAction, OrderStatus>
{
[OrderAction.Confirm] = OrderStatus.Confirmed,
[OrderAction.Cancel] = OrderStatus.Cancelled,
},
[OrderStatus.Confirmed] = new Dictionary<OrderAction, OrderStatus>
{
[OrderAction.Ship] = OrderStatus.Shipped,
[OrderAction.Cancel] = OrderStatus.Cancelled,
},
[OrderStatus.Shipped] = new Dictionary<OrderAction, OrderStatus>
{
[OrderAction.Deliver] = OrderStatus.Delivered,
},
[OrderStatus.Delivered] = new Dictionary<OrderAction, OrderStatus>(),
[OrderStatus.Cancelled] = new Dictionary<OrderAction, OrderStatus>(),
};
// Apply an action to the current state.
public OrderStatus Transition(OrderStatus current, OrderAction action)
{
if (!Transitions.TryGetValue(current, out var allowed))
{
throw new ArgumentOutOfRangeException(nameof(current), current, "Unknown state");
}
if (!allowed.TryGetValue(action, out var next))
{
throw new InvalidOperationException(
$"Cannot perform \"{action}\" on order in \"{current}\" status");
}
return next;
}
// Get the valid actions for the current state.
public IReadOnlyList<OrderAction> AvailableActions(OrderStatus current)
=> Transitions.TryGetValue(current, out var allowed) ? allowed.Keys.ToArray() : [];
}
from enum import Enum
from types import MappingProxyType
from typing import Mapping
class OrderAction(str, Enum):
CONFIRM = "confirm"
CANCEL = "cancel"
SHIP = "ship"
DELIVER = "deliver"
class OrderStateMachine:
"""Order state machine:
(●) ──▶ [Pending] ──confirm──▶ [Confirmed]
│ │
cancel ship
│ │
▼ ▼
[Cancelled] [Shipped]
│
deliver
│
▼
[Delivered]
"""
# MappingProxyType makes the transition table read-only at runtime,
# the closest equivalent to a constant map.
_TRANSITIONS: Mapping[OrderStatus, Mapping[OrderAction, OrderStatus]] = MappingProxyType(
{
OrderStatus.PENDING: MappingProxyType(
{
OrderAction.CONFIRM: OrderStatus.CONFIRMED,
OrderAction.CANCEL: OrderStatus.CANCELLED,
}
),
OrderStatus.CONFIRMED: MappingProxyType(
{
OrderAction.SHIP: OrderStatus.SHIPPED,
OrderAction.CANCEL: OrderStatus.CANCELLED,
}
),
OrderStatus.SHIPPED: MappingProxyType(
{OrderAction.DELIVER: OrderStatus.DELIVERED}
),
OrderStatus.DELIVERED: MappingProxyType({}),
OrderStatus.CANCELLED: MappingProxyType({}),
}
)
def transition(self, current: OrderStatus, action: OrderAction) -> OrderStatus:
"""Apply an action to the current state."""
allowed = self._TRANSITIONS.get(current)
if allowed is None:
raise OrderError(f"Unknown state: {current.value}")
next_status = allowed.get(action)
if next_status is None:
raise OrderError(
f'Cannot perform "{action.value}" on order in "{current.value}" status'
)
return next_status
def available_actions(self, current: OrderStatus) -> list[OrderAction]:
"""Get the valid actions for the current state."""
return list(self._TRANSITIONS.get(current, {}))
## PlantUML примеры
Sequence Diagram в PlantUML
@startuml
participant Client
participant "API Gateway" as GW
participant "Order Service" as OS
participant "Payment Service" as PS
database "PostgreSQL" as DB
Client -> GW: POST /orders
GW -> OS: createOrder()
OS -> DB: INSERT order
DB --> OS: ok
OS -> PS: processPayment()
PS --> OS: paymentResult
alt success
OS -> DB: UPDATE status = confirmed
OS --> GW: Order (201)
else failure
OS -> DB: UPDATE status = failed
OS --> GW: Error (402)
end
GW --> Client: Response
@enduml
Практические советы
Совет
Описание
Не моделируйте всё
Только ключевые части системы
Начинайте с Sequence
Самая полезная диаграмма для обсуждений
Class Diagram = домен
Моделируйте domain, не infrastructure
Diagram as Code
PlantUML/Mermaid для версионирования
Обновляйте
Устаревшая диаграмма вводит в заблуждение
Правило: UML -- инструмент коммуникации, а не документации. Рисуйте диаграммы для обсуждения и принятия решений, а не для отчётности.