UML как язык модели: метамодель, семантика и расширение нотации
Quick recap
Заголовок раздела «Quick recap»К этому моменту мы уже посмотрели на UML с практической стороны. Мы использовали диаграмму использования, чтобы описать систему глазами акторов. Мы использовали диаграмму деятельности, чтобы раскрыть сценарий как процесс. Мы строили диаграмму классов, чтобы зафиксировать понятия предметной области и возможную программную модель. Потом переходили к пакетам, компонентам, развёртыванию, состояниям и последовательностям сообщений.
Если смотреть на курс только как на последовательность тем, может показаться, что UML — это набор разных диаграмм, каждая со своими значками и стрелками. В этом есть доля правды, но это не главное. UML задуман не как библиотека картинок для презентаций, а как язык моделирования. У языка есть синтаксис, семантика, правила расширения и внутренняя структура.
Эта лекция не добавляет новую диаграмму. Вместо этого она поднимает нас на уровень выше и объясняет, почему все уже изученные диаграммы являются частями одного языка.
Диаграмма не равна модели
Заголовок раздела «Диаграмма не равна модели»Начнём с различия, которое кажется очевидным, но постоянно ломает учебные и реальные модели: диаграмма не равна модели.
Когда мы открываем UML-инструмент, мы видим лист, фигуры, линии, подписи и значки. Это похоже на графический редактор, поэтому возникает соблазн думать, что UML-модель и есть набор нарисованных картинок. Но в UML всё устроено иначе. Модель состоит из элементов, их свойств и отношений между ними. Диаграмма только показывает часть этой модели в графическом виде.
Например, в модели может быть класс Order. Этот класс можно показать на концептуальной диаграмме классов как предметное понятие. Потом тот же смысловой объект может появиться на технической диаграмме классов как entity или model. Далее он может стать объектом с жизненным циклом на диаграмме состояний. На диаграмме последовательности мы можем показать lifeline экземпляра заказа. В матрице трассируемости этот же элемент будет связан с требованиями и сценариями.
Если всё сделано аккуратно, это не пять разных заказов, а один элемент модели, рассмотренный с разных сторон. Диаграммы становятся разными представлениями одной проектной реальности.
Именно здесь UML начинает отличаться от обычного рисования. Если удалить фигуру с диаграммы, это не всегда означает удаление элемента из модели. Иногда мы просто убрали одно представление. Если переименовать элемент на одной диаграмме, инструмент может переименовать его везде, потому что меняется не подпись на картинке, а сам элемент модели. Если на двух диаграммах есть два прямоугольника с одинаковым именем, это ещё не гарантирует, что инструмент считает их одним и тем же классом.
На практике это объясняет многие странности UML-инструментов. То, что кажется “капризом программы”, часто является следствием различия между элементом модели и его графическим представлением.
Отсюда появляется первое важное правило: в UML нужно по возможности создавать и настраивать элементы модели, а не имитировать их обычными фигурами и свободным текстом.
Синтаксис, семантика и прагматика
Заголовок раздела «Синтаксис, семантика и прагматика»Любой язык можно рассматривать на нескольких уровнях. Для UML особенно полезны три уровня: синтаксис, семантика и прагматика.
Синтаксис отвечает на вопрос “как это изображается”. Например, ассоциация на диаграмме классов обычно выглядит как сплошная линия между классификаторами. Guard на диаграмме деятельности записывается в квадратных скобках на ребре. Обобщение рисуется сплошной линией с пустым треугольником.
Семантика отвечает на вопрос “что это означает”. Ассоциация означает возможные связи между экземплярами классификаторов. Guard означает условие, при котором токен может пройти по ребру. Обобщение означает отношение общего и специального классификатора.
Прагматика отвечает на вопрос “зачем мы это используем в проектировании”. Ассоциация помогает показать предметную или техническую связанность объектов. Guard часто фиксирует бизнес-правило или условие ветвления сценария. Обобщение помогает показать классификацию понятий, если она действительно важна для модели.
Если студент работает только на уровне синтаксиса, он пытается нарисовать “похоже на UML”. Это уже лучше, чем хаотичная блок-схема, но всё ещё недостаточно. Хорошая UML-модель появляется тогда, когда студент понимает семантику элемента и использует его по делу.
Например, можно нарисовать пунктирную стрелку между двумя овалами и подписать её include. Синтаксически это может выглядеть правдоподобно. Но если эта стрелка используется для показа следующего шага процесса, семантика нарушена. Для порядка действий нужна диаграмма деятельности или сценарий, а не include.
Абстрактный и конкретный синтаксис
Заголовок раздела «Абстрактный и конкретный синтаксис»В спецификации UML есть различие между абстрактным и конкретным синтаксисом.
Абстрактный синтаксис описывает, какие элементы существуют в модели и как они могут быть связаны. Это уровень метамодели. Там говорится, что есть Class, Actor, UseCase, Association, Dependency, ActivityNode, Lifeline и другие элементы.
Конкретный синтаксис описывает, как эти элементы можно показывать человеку. Класс обычно показывается прямоугольником с compartments. Actor можно показать “человечком”, а можно прямоугольником с ключевым словом «actor». Use case показывается овалом. Association — сплошной линией. Dependency — пунктирной стрелкой.
Это различие важно, потому что одна и та же семантика может иметь несколько допустимых вариантов нотации. Например, интерфейс можно показать отдельным прямоугольником с «interface», а можно lollipop-нотацией. Внешний вид разный, но модельный смысл связан с интерфейсом.
Бывает и обратная ситуация: похожая картинка не означает одинаковую семантику. Пунктирная стрелка может обозначать обычную dependency, include, extend, template binding, manifestation или deployment. Внешне отношения похожи, но в метамодели это разные вещи. Поэтому недостаточно сказать “там же пунктирная стрелка”. Нужно понимать, какое именно отношение стоит за этой стрелкой.
Зачем нужна метамодель
Заголовок раздела «Зачем нужна метамодель»Полную метамодель UML разбирать на этом курсе не нужно. Она большая, подробная и предназначена скорее для разработчиков инструментов, профилей и стандартов. Но несколько базовых понятий полезно знать, потому что они постоянно встречаются в спецификации и помогают понимать, почему UML работает именно так.
Самое общее понятие — Element. Почти всё в UML является элементом. У элемента может быть владелец, вложенные элементы, комментарии и связи с другими элементами. Поэтому UML-модель — это не плоский набор фигур, а структурированный набор элементов.
Чуть более конкретное понятие — NamedElement. Это элемент, у которого может быть имя. Важно слово “может”: имя в UML не всегда обязательно. Например, состояние может быть безымянным, и два безымянных состояния всё равно будут разными состояниями. Но в учебной и проектной практике именовать элементы почти всегда полезно, иначе модель быстро становится нечитаемой.
Следующее важное понятие — Namespace. Пространство имён определяет, где имя элемента должно быть различимо. Простая аналогия — package или namespace в языках программирования. Но в UML это шире: пространство имён помогает организовать не только кодовую структуру, но и модель в целом.
Одно из центральных понятий UML — Classifier. Классификатор описывает множество экземпляров с общими свойствами. Классификаторами являются классы, акторы, use cases, компоненты, узлы, артефакты, интерфейсы, типы данных и сигналы. Это объясняет, почему такие разные на вид элементы в спецификации часто обсуждаются через общие свойства: у них могут быть имена, обобщения, признаки, экземпляры и типы.
У классификатора есть Feature, то есть признак. Атрибут класса — это не просто строка текста внутри прямоугольника, а feature. Операция — тоже feature. Порт компонента — тоже feature. Когда мы настраиваем атрибут, операцию или порт в UML-инструменте, мы меняем свойства модели, а не просто редактируем подпись на картинке.
Наконец, есть Relationship. Это общее понятие для отношений между элементами модели. Ассоциация, зависимость, обобщение, реализация, include, extend, template binding, manifestation и deployment — всё это отношения, но с разной семантикой.
Метамодель нужна не для того, чтобы заучивать названия метаклассов. Она нужна, чтобы понимать, где заканчивается картинка и начинается модельный смысл.
Свойства элемента важнее картинки
Заголовок раздела «Свойства элемента важнее картинки»Многие элементы UML выглядят на диаграмме как обычный текст. Из-за этого их часто добавляют свободной подписью, хотя на самом деле это свойства модели.
Например, символы +, -, # и ~ показывают видимость. Кратности 1..* и 0..1 показывают multiplicity. Запись {readOnly} показывает модификатор свойства. {abstract} показывает свойство классификатора. Guard в квадратных скобках является условием на ребре. Effect после / на переходе состояния является поведением перехода.
Если просто написать такой текст рядом с линией или фигурой, картинка может выглядеть правильно. Но инструмент не всегда поймёт, что это именно кратность, guard, visibility или constraint. Для человека на защите разница может быть незаметна с первого взгляда, но для модели она принципиальна.
Поэтому в UML-инструментах важно пользоваться свойствами элементов. Если нужно указать кратность, лучше задать multiplicity у конца ассоциации. Если нужен stereotype, лучше назначить stereotype элементу. Если нужен guard, лучше указать guard у соответствующего ребра или перехода.
Это особенно важно, когда модель потом переиспользуется. Например, если операция действительно добавлена в класс как операция, её можно потом использовать на диаграмме последовательности. Если она была просто написана текстом в прямоугольнике, инструмент не сможет связать с ней сообщение.
Семантика основных отношений
Заголовок раздела «Семантика основных отношений»Большая часть ошибок в UML возникает не из-за того, что студент забыл форму стрелки, а из-за того, что он использует отношение не по его смыслу.
Ассоциация показывает возможные связи между экземплярами классификаторов. Если на диаграмме классов есть ассоциация между Customer и Order, это значит, что экземпляр клиента может быть связан с экземплярами заказов. Ассоциация сама по себе не говорит, что один класс вызывает метод другого, что в базе данных обязательно будет внешний ключ или что связь реализована через REST-запрос. Всё это может появиться позже на техническом уровне, но в UML-семантике ассоциации этого нет автоматически.
Обобщение показывает отношение общего и специального классификатора. Если Student специализирует Person, то экземпляр Student является также косвенным экземпляром Person. На концептуальной модели обобщение стоит использовать только там, где действительно есть отношение “является видом”. Если связь звучит как “содержит”, “использует”, “оформляет”, “проверяет” или “работает с”, скорее всего, нужна не generalization.
Dependency показывает, что один элемент зависит от другого: изменение supplier может повлиять на client. Это очень общее отношение, поэтому у зависимостей много специальных вариантов и ключевых слов. «use» показывает использование, «call» — вызов операции, «create» — создание экземпляров, «trace» — трассировку между элементами разных моделей, «manifest» — материализацию элемента артефактом, «deploy» — развёртывание артефакта на цели развёртывания, «bind» — связывание шаблона с параметрами. Пунктирная стрелка похожа, но смысл разный.
Realization связывает спецификацию и реализацию. Самый привычный пример — класс реализует интерфейс. Но шире это отношение показывает, что один элемент модели реализует контракт, заданный другим элементом.
Constraint тоже важно понимать как часть модели. Ограничение не является декоративным комментарием. Если на классе написано {order.total >= 0}, это утверждение, которое должно выполняться для корректного состояния модели. Ограничение можно записать естественным языком, формальным выражением или OCL. Главное, что оно должно иметь смысл как правило модели, а не как пояснительная подпись.
Семантика поведения
Заголовок раздела «Семантика поведения»Поведенческие диаграммы похожи тем, что все они “про поведение”, но их семантика различается.
Диаграмма деятельности использует token semantics. Упрощённо можно представить, что по графу движутся токены. Управляющие токены задают ход выполнения, объектные токены переносят значения. Action начинает выполняться, когда получает нужные токены. Decision выбирает исходящее ребро по guard. Fork размножает токены по параллельным ветвям. Join синхронизирует ветви. Activity final завершает всю деятельность, а flow final завершает только один поток.
Из-за этого activity diagram не является просто блок-схемой. Она может выглядеть похоже, но за ней стоит модель выполнения. Например, fork и decision нельзя использовать как взаимозаменяемые “разветвители”: один отвечает за параллельность, другой за выбор ветви.
Диаграмма последовательности описывает interaction через трассы событий. Lifeline представляет участника взаимодействия. Сообщение имеет отправку и получение. Отправка сообщения происходит раньше его получения. Внутри одной lifeline порядок событий идёт сверху вниз. Между разными lifelines нет полного глобального времени, если оно специально не задано.
Поэтому расстояние между сообщениями на sequence diagram не является длительностью. Если нужно говорить о времени, нужно явно задавать timing constraints или использовать timing diagram. В обычной sequence diagram вертикальное расстояние помогает читать порядок, но не измеряет время.
Диаграмма состояний описывает поведение классификатора через состояния и переходы. Объект находится в состоянии, событие может вызвать переход, guard определяет, разрешён ли этот переход, effect выполняется при переходе. Поведения entry, do и exit относятся к состоянию, а не к переходу. State machine обрабатывает события по модели run-to-completion: событие обрабатывается, автомат приходит в устойчивую конфигурацию, затем обрабатывается следующее событие.
Поэтому state diagram лучше строить для объектов с настоящим жизненным циклом: заказ, заявка, документ, платёж, инцидент. Если на диаграмме состояний появляются “открыта форма”, “нажата кнопка”, “пользователь смотрит экран”, модель часто уходит в UI-состояния вместо жизненного цикла предметного объекта.
Расширение UML
Заголовок раздела «Расширение UML»UML не пытается заранее предусмотреть все термины всех предметных областей. Вместо этого он предоставляет механизм расширения. Основные инструменты расширения — stereotypes, tagged values, constraints и profiles.
Стереотип расширяет существующий метакласс UML. Например, если в проекте нужно различать обычные компоненты, внешние сервисы и адаптеры, можно использовать стереотипы «externalService», «adapter», «gateway». Но важно помнить: стереотип не создаёт новый базовый элемент языка. Компонент со стереотипом «gateway» всё равно остаётся компонентом. Он сохраняет семантику component, но получает дополнительный проектный смысл.
Tagged value — это дополнительное свойство, которое появляется у элемента благодаря стереотипу. Например, у внешнего сервиса можно хранить provider, protocol, sla, criticality. В нотации это может выглядеть как набор фигурных скобок под именем элемента:
«externalService»Payment Service{provider = "Bank API"}{protocol = "HTTPS"}{criticality = "high"}Profile — это пакет расширений UML для конкретной области, технологии или метода. Профиль может определить стереотипы, свойства стереотипов и ограничения на их применение. Например, команда может договориться о профиле для архитектуры ИС, где есть стереотипы «frontend», «backend», «database», «externalSystem», «messageBroker».
Профили полезны, когда команда регулярно моделирует похожие системы. Но профиль не должен заменять понимание базового UML. Если базовая семантика нарушена, стереотип её не исправит. Нельзя нарисовать неверную dependency, назвать её «important» и считать, что модель стала корректной.
Constraints и OCL
Заголовок раздела «Constraints и OCL»Ограничения часто удобнее писать естественным языком. Например:
{оплаченный заказ нельзя редактировать}Для учебных моделей этого обычно достаточно. Такая запись понятна человеку, хорошо связывается с бизнес-правилами и не требует изучения отдельного языка.
Но естественный язык бывает неоднозначным. Фраза “заказ должен быть корректным” почти ничего не говорит модели. Что значит корректный? Должна ли быть хотя бы одна позиция? Должен ли быть указан клиент? Может ли сумма быть нулевой? В таких случаях полезно хотя бы частично формализовать ограничение.
Для UML существует OCL — Object Constraint Language. В рамках этого курса не нужно глубоко изучать OCL, но полезно понимать его назначение. OCL-выражение ничего не меняет в системе. Оно проверяет условие или вычисляет значение. У него не должно быть побочных эффектов. С его помощью можно задавать инварианты, предусловия и постусловия.
Инвариант говорит, что должно быть истинно для корректного состояния объекта. Например, естественное правило “у заказа должна быть хотя бы одна позиция” можно выразить так:
context Orderinv: self.items->size() >= 1Предусловие говорит, когда операцию можно вызвать:
context Order::cancel()pre: self.status <> OrderStatus::DeliveredПостусловие говорит, что должно стать истинным после выполнения операции:
context Order::cancel()post: self.status = OrderStatus::CancelledГлавная идея здесь не в том, чтобы все срочно начали писать OCL. Главная идея в том, что бизнес-правило может быть частью модели. Оно не обязано жить только в тексте требований или в голове разработчика.
Почему UML-инструменты ведут себя странно
Заголовок раздела «Почему UML-инструменты ведут себя странно»Когда человек впервые работает с UML-инструментом, многие вещи кажутся неудобными. Почему нельзя просто нарисовать линию? Почему инструмент просит выбрать тип отношения? Почему одно переименование меняет элемент на нескольких диаграммах? Почему скрытие compartment не удаляет операции?
Ответ один: инструмент работает не только с картинкой, но и с моделью.
Если класс создан один раз и показан на нескольких диаграммах, переименование класса меняет сам элемент модели. Все его представления получают новое имя. Это нормальное поведение.
Если на диаграмме класса скрыть список операций, операции не исчезнут из модели. Они просто не отображаются на данном представлении. Это тоже нормально: одна диаграмма может показывать только предметные атрибуты, другая — технические операции, третья — связи между пакетами.
Если кратность ассоциации написана обычным текстом, инструмент может не понимать её как multiplicity. Если guard написан рядом со стрелкой как свободный комментарий, он может не быть свойством ребра. Визуально разница мала, но для модели она принципиальна.
Отдельный пример — экспорт. PNG или SVG сохраняет изображение. XMI сохраняет модельные элементы, свойства и связи. На практике XMI между разными инструментами переносится не идеально, но сама идея важна: UML-модель — это данные, а не только рисунок.
Как читать спецификацию UML
Заголовок раздела «Как читать спецификацию UML»Спецификацию UML не нужно читать подряд как учебник. Она для этого слишком большая и слишком формальная. Её полезнее использовать как справочник, когда возникает спорный вопрос.
Обычно разделы спецификации устроены предсказуемо. Сначала даётся обзор, потом абстрактный синтаксис, затем семантика, нотация, примеры и правила корректности. Если непонятно, как рисовать элемент, нужно смотреть notation. Если непонятно, что элемент означает, нужно смотреть semantics. Если непонятно, может ли один элемент быть связан с другим, нужно смотреть abstract syntax и well-formedness rules.
Например, если возник спор о направлении стрелки include, нужно смотреть раздел про Include, а не общую статью про use case diagrams. Если непонятно, является ли Node классификатором, нужно смотреть метамодель Node. Если непонятно, является ли «bind» реализацией, нужно смотреть TemplateBinding.
Такой способ чтения спецификации помогает не превращать UML в набор мнений. В спорной ситуации можно проверить: это действительно правило UML или просто привычка конкретного инструмента, автора учебника или команды.
Практические правила
Заголовок раздела «Практические правила»Вся лекция сводится к нескольким практическим правилам.
Во-первых, диаграмма — это представление модели, а не вся модель. Во-вторых, похожая линия не всегда означает одно и то же отношение. В-третьих, свойства UML-элементов нужно задавать как свойства модели, а не имитировать текстом. В-четвёртых, стереотип уточняет существующий элемент UML, но не отменяет его базовую семантику. В-пятых, constraint является частью модели, а не декоративным комментарием.
Если диаграмма выглядит красиво, но противоречит семантике UML, это плохая UML-модель. Если диаграмма выглядит проще, но корректно фиксирует элементы, отношения и ограничения, это обычно гораздо полезнее.
Что дальше?
Заголовок раздела «Что дальше?»После этой лекции полезно иначе посмотреть на все предыдущие диаграммы.
Диаграмма использования — это не просто овалы и человечки, а модель акторов, субъектов и поведения, полезного заинтересованным лицам.
Диаграмма деятельности — это не просто блок-схема, а модель выполнения через токены.
Диаграмма классов — это не просто прямоугольники, а модель классификаторов, признаков, ассоциаций и ограничений.
Диаграмма последовательности — это не просто вызовы методов сверху вниз, а модель допустимых трасс взаимодействия.
Диаграмма развёртывания — это не просто схема серверов, а модель артефактов, узлов и отношений deployment.
Именно это отличает UML-модель от набора иллюстраций в презентации.