Пример UML-диаграммы классов для выдачи книг: читатель, экземпляр и история выдач. Разбираем кратности связей и проверяем модель на трёх учебных сценариях.
Андрей
Преподаватель, научный редактор
8 мин чтения
«Одну книгу нельзя выдать двум читателям». Кажется, достаточно нарисовать возле книги единицу. Я проверил эту идею на истории обычной библиотеки: экземпляр вернули, а потом выдали снова. И сразу понадобилось уточнение. Этот пример диаграммы классов UML показывает, как сохранить обе выдачи и при этом запретить две активные выдачи одного экземпляра. Получится небольшой текстовый рисунок, который можно прочитать прямо здесь, без запуска редактора диаграмм.
Я беру три понятия: читатель, физический экземпляр и запись о выдаче. Проверять их будем на одной книге с инвентарным номером E17. Учебные читатели R1 и R2 получат её по очереди. Потом попробуем добавить выдачу, которая должна быть запрещена. На этом третьем действии станет видно, что одних чисел у линий недостаточно.
Пример диаграммы классов UML: что именно мы выдаём
Сначала я разделил произведение и вещь на полке. В библиотеке могут стоять два экземпляра одного названия. Возврат одного не означает, что вернули второй. Поэтому в нашей задаче нужен именно класс «Экземпляр» с инвентарным номером. Название книги я оставил его атрибутом: для нынешнего объяснения отдельный каталог произведений не требуется.
Следом появился вопрос об истории. Если просто связать читателя с экземпляром, где хранить дату конкретного получения и возврата? Эти сведения относятся к отдельной выдаче. У того же человека та же книга может побывать повторно, уже в другой период. Я вынес событие учёта в самостоятельный класс «Выдача».
Получилось такое распределение:
«Читатель» обозначает участника, которому выдают экземпляры;
«Экземпляр» обозначает конкретную учётную вещь, различаемую по инвентарному номеру;
«Выдача» хранит сведения об одном получении экземпляра определённым читателем.
Это диаграмма классов, то есть описание типов объектов и связей между ними. Она не изображает последовательность нажатий библиотекаря. Для текущего разбора я использую обозначения UML 2.5.1 из спецификации OMG. Другие виды диаграмм UML решают свои задачи; привычные прямоугольники не делают любой рисунок диаграммой классов.
Вот модель в текстовом виде. Вертикальные линии соединяют соседние классы; числа относятся к концам этих линий.
```text
+---------------------------+
| Читатель |
+---------------------------+
| id: String |
+---------------------------+
1
|
0..*
+---------------------------+
| Выдача |
+---------------------------+
| номер: String |
| выдано: Дата |
| возвращено: Дата [0..1] |
+---------------------------+
0..*
|
1
+---------------------------+
| Экземпляр |
+---------------------------+
| инвНомер: String |
| название: String |
+---------------------------+
Ограничение для Экземпляра:
{Связанных выдач без значения «возвращено» не больше одной}
```
В верхней части прямоугольника стоит название класса, под разделителем — его атрибуты. `String` означает строковое значение. Тип «Дата» здесь введён для нашего календарного примера; я не называю его обязательным встроенным типом UML. Раздел с операциями в этом эскизе не показываю: нам нужно разобраться со структурой учёта.
Запись `[0..1]` возле `возвращено` означает, что значение даты может отсутствовать либо присутствовать одно. В этом примере отсутствие даты соответствует активной выдаче. Это наше правило учёта. Оно не возникает автоматически из русского названия атрибута и должно быть понятно человеку, который читает модель.
Что означают единица и звёздочка возле выдачи
Возьмём верхнюю линию. `1` у «Читателя» говорит: каждой выдаче соответствует ровно один читатель. А `0..*` возле «Выдачи» означает: с одним читателем может быть связано ноль или больше записей о выдачах. В них входят и текущие, и уже завершённые. Смотри на оба конца. Одна подпись без другой даёт неполное описание.
Нижняя линия читается похоже. У каждой выдачи ровно один экземпляр. У экземпляра может быть много записей о его прошлых и нынешних выдачах, а может пока не быть ни одной. Так в модели помещается новая книга, которую ещё никто не брал. Я намеренно указал все кратности, чтобы читателю не приходилось угадывать пропущенные значения.
В учебном условии одна выдача относится к одному экземпляру, а не к стопке книг. Если библиотекарь оформляет несколько экземпляров одновременно, в нашей модели появится несколько записей «Выдача». Для общего документа получения можно было бы вводить дополнительное понятие, но я не добавляю его без такой задачи.
Пока все связи здесь обычные ассоциации. Выдача не является разновидностью читателя, поэтому стрелка наследования между ними означала бы другой смысл. Ромбы композиции тоже не добавляю: условие не устанавливает особых правил владения объектами и их жизненного цикла. Нужна связь с участником, а не украшение линии.
Проверить рисунок полезно чтением вслух. Я делаю это двумя фразами для каждой связи:
Для выбранной выдачи назови, сколько читателей ей разрешено иметь.
Для выбранного читателя назови допустимое количество его выдач.
Повтори оба вопроса для пары «Выдача — Экземпляр».
Сравни ответы с исходной задачей про конкретные книги на полке.
Если получилось «у читателя обязательно должна быть выдача», проверь нулевую нижнюю границу. Если вышло «одну выдачу можно оформить без экземпляра», проверь единицу у класса «Экземпляр». Это разные ошибки. Перерисовывать всю модель ради исправления одного конца связи необязательно.
Для общей подготовки описаний полезен разбор того, как сформулировать задачу для схемы. Но в диаграмме классов я обязательно уточняю, какие объекты связываются и сколько их допустимо. Просьба «соедини всё правильно» не сообщает этих условий.
Три операции с E17: где проходит граница допустимого
Теперь проверю рисунок на синтетических записях. Никаких читательских формуляров для этого не нужно. Начальное состояние: существуют R1, R2 и экземпляр E17, а выдач ещё нет. Обе подписи `0..*` допускают такое состояние. Библиотека не обязана выдавать каждую поступившую книгу сразу.
Первая операция. 3 ноября создаём V1: читатель R1 получил E17. Дата `выдано` заполнена, дата `возвращено` отсутствует. У V1 есть один читатель и один экземпляр. Для E17 насчитывается одна активная выдача. Оба вида условий выполнены: кратности и ограничение возле экземпляра.
Вторая операция. 6 ноября возвращаем E17, заполняя дату возврата V1. Саму V1 не удаляем. 7 ноября создаём V2 для R2 и того же E17, оставляя её дату возврата пустой. Теперь у экземпляра две связанные выдачи, но активна только одна. Именно такое состояние мы и хотели разрешить.
Третья операция. 8 ноября пытаемся создать V3 для R1 и E17, пока V2 ещё активна. У новой записи снова есть ровно один читатель и один экземпляр. Исторических записей станет больше, и `0..*` этому не препятствует. Однако у E17 окажутся две выдачи без возврата. Ограничение нарушено: V3 в таком состоянии принимать нельзя.
Действие · История E17 после действия · Активных выдач E17 · Результат
Создать V1 для R1 — V1 без возврата — 1 — Допустимо
Закрыть V1, затем создать V2 для R2 — V1 завершена, V2 активна — 1 — Допустимо
Попытаться добавить V3, не закрыв V2 — V1 завершена, V2 и V3 активны — 2 — Отказ по ограничению, хотя кратности соблюдены
В последней строке показана попытка изменения, а не принятое состояние учёта. После отказа остаются V1 и V2. Я отдельно сделал это уточнение, потому что таблица проверок не должна незаметно узаконить запись, которую только что запретила.
Экземпляр E17 выдавали 2 раза: сначала R1, затем R2. В истории нужны две записи. Если заменить `0..*` у выдач на `0..1`, чтобы победить третью операцию, вторая тоже перестанет помещаться в модель: V1 ведь сохранена после возврата. Так короткая правка кратности уничтожает нужную историю.
Я проверил и другой соблазнительный вариант: оставить звёздочку и написать в пояснении «книга свободна». У такой фразы нет критерия. Поэтому в ограничении использовано ровно то состояние, которое видно у записей, — отсутствие даты возврата. Читатель может сам пересчитать подходящие выдачи, а не угадывать смысл слова «свободна».
Что стоит объяснить рядом с рисунком
Под схемой я оставляю короткое описание принятых решений: выдача хранит историю, завершённые записи не удаляются, активность определяется отсутствием возврата. Это условия нашего примера. Если в другой системе активность задаётся статусом, условие нужно выразить через статус и согласовать его с датами, а не оставить два независимых признака с неизвестным приоритетом.
Ограничение в фигурных скобках сформулировано обычным текстом. UML допускает такое описание условия; здесь я не выдаю его за формулу OCL или исполняемый код. Рисунок помогает обнаружить ошибку в модели. Сам по себе он не блокирует кнопку выдачи в программе и не обеспечивает согласованность одновременных действий библиотекарей.
Мне удобно обсуждать именно спорный сценарий, а не просить оценить красоту всех прямоугольников. LibraChat помогает разбирать условия и объяснять связи. Открой чат для разбора модели и предложи такой запрос:
Проверь учебную диаграмму классов выдачи книг. У каждой Выдачи один Читатель и один Экземпляр; у Читателя и Экземпляра может быть много Выдач, завершённые сохраняются. Возврат — необязательная дата; её отсутствие обозначает активную выдачу. Для каждого Экземпляра разрешена максимум одна активная Выдача. Объясни, почему вторая активная выдача нарушает отдельное ограничение, но не кратность 0..*. Затем проверь повторную выдачу после возврата. Не добавляй новые правила библиотеки.
Доступные возможности работы в LibraChat перечислены в описании тарифов. Сам пример можно разобрать и на бумаге: имена классов и записи V1–V3 уже заданы.
Для небольшого задания поменяй последнюю операцию: R1 просит другой свободный экземпляр E18. Попробуй объяснить, какое условие теперь проверяется у E18 и почему активная V2 у E17 не запрещает эту выдачу. Если можешь ответить, ты уже читаешь предметную модель, а не просто узнаёшь значки у линий.