Редактор диаграмм классов Mermaid
Диаграмма классов показывает типы и их связи: что что содержит, что от чего наследуется, что от чего зависит. Она уместна, когда суть — форма кода: доменная модель, интерфейс расширения, дерево наследования. Если нужно показать, что происходит во время выполнения, а не как устроены типы, берите диаграмму последовательности.
Доменная модель оплаты
Три вида связей на одной диаграмме: композиция для частей, которые не переживают целое, наследование для иерархии способов оплаты и обычная ассоциация с кратностью. Смысл здесь несут прежде всего стрелки; прямоугольники классов почти второстепенны.
classDiagram
class Заказ {
+String номер
+СтатусЗаказа статус
+Деньги итого()
+void добавитьСтроку(Товар т, int количество)
}
class СтрокаЗаказа {
+Товар товар
+int количество
+Деньги сумма()
}
class СпособОплаты {
<<abstract>>
+авторизовать(Деньги сумма) bool
}
class БанковскаяКарта {
+String последние4
+авторизовать(Деньги сумма) bool
}
class БыстрыйПлатёж {
+String телефон
+авторизовать(Деньги сумма) bool
}
class Наличные {
+Деньги сдача
+авторизовать(Деньги сумма) bool
}
Заказ "1" *-- "1..*" СтрокаЗаказа : содержит
Заказ --> СпособОплаты : оплачивается через
СпособОплаты <|-- БанковскаяКарта
СпособОплаты <|-- БыстрыйПлатёж
СпособОплаты <|-- НаличныеРазобранные примеры
1. Один класс
`+` — публичный, `-` — приватный, `#` — защищённый. Член со скобками рисуется как метод; без скобок это поле.
classDiagram
class Пользователь {
+String почта
-String хэшПароля
+bool проверить(String кандидат)
}2. Наследование и интерфейсы
`<|--` — это наследование, читается как «правый расширяет левого». Аннотация `<<interface>>` — просто пометка, а не поведение, но именно она делает диаграмму читаемой.
classDiagram
class Репозиторий {
<<interface>>
+найти(String id) Сущность
+сохранить(Сущность с) void
}
class РепозиторийPostgres {
-Соединение соединение
+найти(String id) Сущность
+сохранить(Сущность с) void
}
class РепозиторийВПамяти {
-Map хранилище
+найти(String id) Сущность
+сохранить(Сущность с) void
}
Репозиторий <|.. РепозиторийPostgres
Репозиторий <|.. РепозиторийВПамяти3. Композиция против агрегации
Разница во времени жизни. Закрашенный ромб (`*--`) означает, что часть умирает вместе с целым: удалите счёт — и его строки исчезнут. Полый ромб (`o--`) означает, что часть существует самостоятельно.
classDiagram
class Счёт {
+String номер
}
class СтрокаСчёта {
+String наименование
}
class Контрагент {
+String название
}
Счёт "1" *-- "1..*" СтрокаСчёта : состоит из
Контрагент "1" o-- "0..*" Счёт : выставил4. Дженерики
Тильды задают параметры типа: `Репозиторий~Пользователь~`. Вложенность тоже работает, что иногда необходимо и редко бывает хорошей идеей. Кириллица внутри дженерика работает без проблем.
classDiagram
class Репозиторий~T~ {
+найти(String id) T
+все() List~T~
}
class Кэш~K, V~ {
+получить(K ключ) V
+положить(K ключ, V значение) void
}
class РепозиторийПользователей {
+найтиПоПочте(String почта) Пользователь
}
Репозиторий~Пользователь~ <|-- РепозиторийПользователей5. Заметки и направление
`direction LR` раскладывает диаграмму слева направо, что дереву наследования обычно подходит лучше, чем раскладка по умолчанию. Заметка — правильное место для ограничения, которое не влезает в прямоугольник класса.
classDiagram
direction LR
class ХранилищеСобытий {
+добавить(Событие с) void
+воспроизвести(String поток) List~Событие~
}
class Снимок {
+int версия
+byte[] содержимое
}
ХранилищеСобытий --> Снимок : пишет каждые 100 событий
note for ХранилищеСобытий "Только на добавление. События никогда не меняются и не удаляются."Справочник синтаксиса диаграммы классов
Запоминать стоит именно стрелки связей: они отличают диаграмму классов от рисунка из прямоугольников и линий, и читаются справа налево — что долго сбивает с толку.
| Синтаксис | Значение |
|---|---|
| classDiagram | Открывает диаграмму. Регистр важен. |
| class Имя { ... } | Класс с членами. Закрывающая скобка — на отдельной строке. |
| +член | Публичный. |
| -член | Приватный. |
| #член | Защищённый. |
| +метод(Тип арг) ТипВозврата | Метод — методом его делают именно скобки. |
| <<interface>> / <<abstract>> | Стереотип, пишется первой строкой внутри класса. |
| A <|-- B | Наследование: B расширяет A. |
| A <|.. B | Реализация: B реализует интерфейс A. |
| A *-- B | Композиция: B не переживает A. |
| A o-- B | Агрегация: B может существовать без A. |
| A --> B | Направленная ассоциация. |
| A ..> B | Зависимость — A использует B, но не хранит его. |
| A "1" --> "0..*" B : подпись | Кратность на каждом конце плюс подпись связи. |
| class Репо~T~ | Параметр обобщённого типа. |
| note for A "текст" | Заметка, прикреплённая к классу. |
| direction LR | Меняет направление раскладки. |
Ошибки, ломающие диаграмму классов
Воспроизведены на Mermaid 11.12.2. Диаграмма классов — из самых снисходительных здесь типов, поэтому половина этих ошибок спокойно рисуется и выдаёт неверную картину.
Что вы видите
Parse error, заканчивается на: got 'EOF_IN_STRUCT'
Почему
Тело класса открыто через `{` и не закрыто. Здесь имя токена на удивление полезно: оно означает, что файл закончился, пока мы всё ещё были внутри класса.
Решение
Закройте фигурную скобку на отдельной строке.
classDiagram
class Заказ {
+String номерclassDiagram
class Заказ {
+String номер
}Что вы видите
Parse error, заканчивается на: got 'ANNOTATION_END'
Почему
В диаграмме классов использована стрелка из диаграммы последовательности. `->>` здесь ничего не значит, а разборщик заходит в неё достаточно далеко, чтобы выдать сбивающее с толку имя токена.
Решение
Используйте связь диаграммы классов: `-->` для ассоциации, `<|--` для наследования, `*--` для композиции.
classDiagram
Заказ ->> КлиентclassDiagram
Заказ --> Клиент : принадлежитЧто вы видите
No diagram type detected matching given configuration
Почему
Неверный регистр ключевого слова. `classdiagram` — это не `classDiagram`.
Решение
Сделайте D заглавной.
classdiagram
class ЗаказclassDiagram
class ЗаказЧто вы видите
Стрелка указывает в сторону, противоположную задуманной
Почему
Стрелки связей читаются от наконечника назад. `A <|-- B` означает, что B наследуется от A, а не наоборот. Написанная задом наперёд, она всё равно рисуется — просто теперь она утверждает, что базовый класс расширяет собственного наследника.
Решение
Читайте так: «дальний конец расширяет тот, куда указывает наконечник». Родителя ставьте слева от `<|--`.
classDiagram
БанковскаяКарта <|-- СпособОплатыclassDiagram
СпособОплаты <|-- БанковскаяКартаЧто вы видите
Появляется поле там, где вы ждали метод
Почему
Скобки — единственное, что отличает метод от поля. `+сохранить` — это поле с именем «сохранить»; `+сохранить()` — метод. Оба варианта допустимы, поэтому никакого предупреждения не будет.
Решение
Добавьте скобки, а за ними тип возвращаемого значения, если хотите его видеть.
classDiagram
class Репо {
+сохранить
+найти
}classDiagram
class Репо {
+сохранить(Сущность с) void
+найти(String id) Сущность
}Что вы видите
Композиция и агрегация выглядят почти одинаково, а означают противоположное
Почему
`*--` и `o--` отличаются одним символом и кодируют настоящую смысловую разницу: может ли часть пережить целое. Выбор не того варианта даёт диаграмму, формально безупречную и фактически неверную относительно вашей предметной области.
Решение
Закрашенный ромб `*--`, когда удаление родителя удаляет потомка. Полый `o--`, когда нет.
classDiagram
Заказ o-- СтрокаЗаказа : содержитclassDiagram
Заказ *-- СтрокаЗаказа : содержитЗаметки о рендеринге
Измерено на том Mermaid 11.12.2, который использует сайт.
Растёт в высоту быстрее любого другого типа здесь
Измерено на классах с двумя членами, связанных наследованием: три класса дают viewBox около 176×548, сорок классов — 180×7726, то есть примерно 194 пикселя высоты на класс, самый крутой рост среди шести типов сайта. Диаграмма на сорок классов переваливает за семь тысяч пикселей в высоту и как единая картинка непригодна. `direction LR` помогает, но после полутора десятков классов честное решение — разрезать диаграмму по границам подсистем.
Количество членов почти не влияет на ширину
Ширину задаёт самая длинная сигнатура одного члена, а не их количество. Класс с двадцатью короткими полями не шире класса с тремя. То есть: на члены можно не скупиться, а на классы — скупиться, что прямо противоположно интуиции. По-русски сигнатуры выходят длиннее английских, так что ширину обычно определяет один метод с подробным именем.
Кириллица работает и в именах классов, и внутри дженериков
Проверено: `Заказ`, `хэшПароля`, `БыстрыйПлатёж` годятся как имена классов и членов, и `Репозиторий~Пользователь~` тоже собирается без ошибок. Переводить модель на латиницу ради того, чтобы она нарисовалась, не нужно. Единственное настоящее ограничение задаёт тильда, которая является синтаксисом.
Дженерики используют тильды, и у этого есть следствие
`Репозиторий~T~` существует потому, что угловые скобки конфликтовали бы с HTML в подписях. Отсюда же следует, что буквальная тильда в имени класса или члена будет прочитана как начало параметра типа. Встречается редко, но сбивает с толку основательно.
Подписи — это HTML, поэтому экспорт в PNG перерисовывает
Подписи классов рисуются внутри `<foreignObject>` в SVG, который браузеры отказываются растрировать на canvas. Раньше экспорт в PNG на этом сайте молча давал сбой и возвращал SVG-файл; теперь он сначала перерисовывает диаграмму подписями обычным SVG-текстом. PNG получается правильным и в полном размере, с чуть иной типографикой по сравнению с экраном.
Когда лучше взять другую диаграмму
Если вы документируете базу данных, а не систему типов, берите ER-диаграмму. Различие содержательное: диаграммы классов моделируют поведение и наследование, которых у таблиц нет, а ER-диаграммы как следует моделируют ключи и кратность, которые диаграммы классов заминают.
Если на диаграмме в основном прямоугольники со стрелками `-->` и без членов, вы рисуете архитектуру, а не диаграмму классов. Блок-схема с подграфами будет выглядеть лучше и утверждать меньше.
А если список классов генерируется из кода, подумайте, не должна ли и диаграмма генерироваться. Диаграмма классов, которую ведут вручную для кодовой базы, меняющейся каждую неделю, становится неверной за месяц, а неверная диаграмма обходится дороже, чем её отсутствие.
Другие типы диаграмм
Автор Dominik Malsch · Обновлено: