В первых трех частях(первая, вторая, третья) мы поговорили о UML в целом, семантике и преимуществах хорошей модели.
Настало время обсудить ещё и плохую модель, а также её ключевые отличия от хорошей
Плохая модель классов
Пришло время посмотреть на тип модели классов UML, который можно встретить во множестве проектов. А ещё, увы, который часто поощряется в книгах по UML.
Хорошая модель
Понадобится нам для сравнения с плохой моделью
Здесь у нас та же самая область применения, но совершенно другая модель. Вот что сразу видно:
Меньше классов и отношений
Более короткие и неполные названия связей
Нет ссылочных атрибутов или тегов идентификаторов
Менее точные (ориентированные на реализацию) типы данных по атрибутам
Более того, если посмотреть на эти 4 элемента суммарно, будет заметно, что некоторых правил приложения вообще нет, а те, что есть, неправильно сформулированы. Пока поговорим о поверхностных отличиях.
Меньше элементов модели
В плохой модели меньше классов, так как роли диспетчеров «дежурный» и «выходной» не моделируются. Что сводит на ноль смысл значения поля «Время регистрации в системе» для каждого диспетчера, который выходной. Это вводит определенную логику «если — то»(для обработки пустых значения, чего не требовалось в хорошей модели) в действия и/или диаграммы состояний.
А ещё такая модель будет требовать от программиста (или компилятора) зарезервированного места для заведомо ненужного атрибута. В принципе, это не такая уж проблема, если у вас немного диспетчеров. А если их тысячи? А если мы при этом оставили элементы вида «не применимо» в большом количестве классов? Для некоторых систем наличие сотен тысяч экземпляров — обычное дело, это всего лишь один из вариантов того, как более компактная модель приводит к менее эффективному коду. На практике можно встретить много случаев, когда более детализированная и крупная модель несёт в себе ДНК, которая нужна для более точной реализации правил приложения.
Само собой, у нас тут принцип “Чем меньше — тем лучше”. Но лишь до тех пор, пока это не начинает работать за счет выражения правил. Ведь моделирование в целом — процесс аналитический, разбирающий вещи на составляющие. А разработка и реализация — это, наоборот, синтез. Разработки моделей, который выполняет объектно-ориентированный анализ, старается выявить и раскрыть все правила приложения. При этом используя столько компонентов, сколько для этого нужно.
Программист (или компилятор) модели сопоставляет элементы анализа и переупаковывает их по мере необходимости, чтобы получить на выходе эффективный код на нужной платформе. При этом такая переупаковка должна учитывать каждое моделируемое правило, хоть никто и не запрещает уменьшать общее количество элементов в процессе. Так что разработка — это своего рода умная упаковка.
Вот что еще вытекает из разницы между анализом и синтезом. Цели аналитика и исполнителя могут противоречить друг другу, более того, так обычно и бывает. Если попытаться сделать одну модель, которая будет стараться удовлетворять обе цели сразу, то она получится провальной. В итоге вы получаете сжатый анализ и раздутую реализацию (да-да, именно поэтому хорошие программисты часто избегают UML, кому такое понравится).
Наша же с вами цель — разделить процессы разработки и моделирования, чтобы получить максимальные преимущества от обоих.
Более короткие имена
Плохая модель использует для обозначения связей общий стиль именования ролей. Когда вы создаете диаграмму классов как прямое представление реализации своего кода, роли хороши, тут спору нет. Но для целей анализа куда как эффективнее глагольные фразы.
Правило R1 в плохой модели гласит, что диспетчер играет роль контролера по отношению к зонам управления (нулю или нескольким). Придумать существительное, которое описывало бы в одно слово противоположную роль, довольно трудновато, поэтому это упускается из виду. Конечно, если у вас весомый словарный запас, куча времени и есть желание придумать смешное слово, у вас получится. Но вам нужно думать о том, что вы на самом деле имеет в виду, а не упражняться в бессмысленных головоломках с глагольными фразами.
Так как имя роли часто совпадает с именем связанного класса, их можно смело удалять, это не уменьшит выразительность модели. Авиадиспетчер и контролер это одно и то же? ОК, так и запишем.
На самом деле, в случае с плохой моделью нам сложно сказать наверняка, имеем мы в виду, что зона управления назначается диспетчеру постоянно (как место на парковке или стол в опенспейсе), или же мы обсуждаем именно текущий момент. Обратите внимание — в хорошей модели пара глагольных фраз «управляет трафиком внутри» / «содержит трафик, управляемый» как раз и проясняют возможную путаницу, дополняя объяснение, что же на самом деле значит «управлять».
Когда аналитик помещает точную глагольную фразу с обеих сторон каждой связи, он вынужден тщательно рассматривать мощность связи и обусловленность с каждой стороны. Это критично — именно здесь выражены многие правила. Следует избегать общих, всеохватывающих глагольных фраз, таких как «содержит», «является группой», «имеет» и — мое любимое — «связано с». Просто погуглите «композиция и агрегация», чтобы понять, насколько легкомысленно использовать общие термины для выражения точных ассоциаций. Какое утверждение скажет вам больше? Блок памяти «разбит на взаимоисключающие части» |..* Область или блок памяти «является (выберите один вариант — агрегация / композиция) из» области?
Если вы говорите, что диспетчер «управляет трафиком внутри <некоторого числа> зон управления», это вынуждает вас подробно рассматривать правила приложения. Вы наверняка уже поняли, что это 0..* и что нулевой случай соответствует выходному диспетчеру. Если перефразировать все в качестве класса «дежурный контролер» «управляет трафиком внутри» <некоторого числа> зон управления», то у вас получится отразить множественность 1..* и закрепить важное правило. Перевернув глагольную фразу с активного залога на пассивный, вы получите контрольную зону, в которой содержится трафик, управляемый только одним дежурным контроллером. Он должен всегда быть, плюс должен быть на дежурстве в соответствии с правилами приложения. Глагольные фразы ведут модель к повышенной точности.
Зачем вообще со всем этим заморачиваться, если фразы не переведены в код? На самом деле, они могут появляться как сокращенные имена или как комментарии. Но компилятор модели недостаточно умен, чтобы понять текст в глагольной фразе и выбрать в соответствии с ним тот или иной тип разработки. Наверное, это тоже хорошо. Но всё равно — ведь именно множественность и определяет выбор структуры данных и реализацию доступа.
Но как вы пришли к правильному множеству? Модели, у которых общие ассоциативные имена, почти всегда опровергают настоящую природу ассоциации и скрывают интересные граничные условия, которые и приводят к неправильной множественности, порождающей неправильный код.
Отсутствие тегов идентификаторов или ссылочных атрибутов
В наших примерах выше теги не играют какой-то роли, поэтому подробно на них останавливаться я не буду. Ссылочные атрибуты нужны лишь для того, чтобы показать, что данные на самом деле связаны в табличной форме. Современные компиляторы моделей предполагают существование ссылочных атрибутов и могут управлять ими в фоновом режиме. Тем не менее, существует ряд простых методов, с помощью которых можно распространять и объединять ссылочные атрибуты в рамках нескольких отношений, дабы выразить строгие атрибуты. Я так часто поступаю, поэтому сохраняю ссылочные атрибуты по умолчанию.
Предположим, что каждый экземпляр класса уникален. Автоматически. То есть код сгенерируется так, что каждая строка таблицы соответствует однозначно выбираемой сущности. Поэтому нам не нужно накладывать атрибут {I} на каждый класс. Получается, что если у вас есть какой-то класс под названием Thing, то вы можете дать ему искусственный атрибут Thing.ID {I}, но это необязательно — это верно только для искусственных идентификаторов.
Однако, все ограничения уникальности реального мира должны быть выражены всегда. Представьте, что вы моделируете взлетно-посадочные полосы с одинаковым курсом и стороной в нескольких аэропортах. У вас не может быть две ВПП с одинаковым курсом и стороной (скажем, два 28R). Это ограничение, и его надо выразить.
Как вы это сделаете?
Вот модель, позволяющая это сделать, если включить ссылочный атрибут в качестве компонента идентификатора.
При помощи тегов {I} и {R} в классе взлетно-посадочной полосы(Runway) эта модель сообщает нам, что уникальный экземпляр ВПП можно выбрать с помощью подставления заголовка, стороны и кода аэропорта. 28L в SFO, например, это взлетно-посадочная полоса 28 слева в Международном аэропорту Сан-Франциско. Так у вас появляется возможность объединять идентификационные данные и ссылочные теги, чтобы выразить тот или иной факт о реальном мире. А именно — несколько взлетно-посадочных полос могут иметь один и тот же курс и сторону, но не в одном аэропорту.
Менее точные типы данных
В хорошей модели мы видели строго определенные типы данных приложения. Плохая же полагается на нечетко определенные типы реализации. Давайте на примере атрибута ControlZone.Name. В хорошей модели он определялся как тип Czone_ID. На рисунке видно, что имена зон управления состоят из CZ и целочисленного значения, например, CZ1, CZ2 и прочее в таком духе. Возможно, это получится реализовать и при помощи нечетко определенных типов строк. А если мы в модели говорим, что нам нужна строка, что мы просим в реализации? Если программист (или компилятор) модели работает с более специфичным типом приложения, то сможет при необходимости выбрать более жесткий тип реализации.
Вернемся к модели одного класса. У нас есть тип данных под названием «Высота», это можно определить как количество метров в диапазоне 70 000..-400 с точностью, скажем, 0,01. В отношении данного применения это было бы более точным. Конечно же, программист может захотеть реализовать это в виде данных типа float в C или любого другого типа, который соответствует нужной платформе.
Тут всё сводится к тому же принципу — модель должна выражать истинные требования области применения. В максимально сжатом и точном объеме. Без ограничений в плане выбора метода написания кода. Ваша цель — сделать модель такой, чтобы она выражала реальные условия области применения максимально сжатым образом. Это пригодится, чтобы расширить реальное применение с учетом возможных будущих требований. Допустим, сегодня модель используют только для самолетов с неподвижным крылом. А завтра захотят добавить еще и вертолеты, и вместо взлетно-посадочных полос у нас будут вертолетные площадки. Значит, есть смысл пересмотреть и самоопределение ВПП и посадочной поверхности.
Расширяете вы правила приложения или нет — не так важно, главное построить точную модель любой реальности. Оставить модель расплывчатой — это не попытка удовлетворить будущие требования, это попытка оправдать собственную лень.
Итак, атрибуты. Даже если мы позволим атрибуту быть свободно определенной строкой (такой как имя), мы сможем использовать типы данных «длинное имя» и «короткое имя». Длинное может быть определено как строка до 80 символов длиной, а короткое — до 10. Нацелены они могут быть как на один тип строк реализации, так и на разные.
Поведенческие ограничения в плохой модели не выражены, а вопросы оставлены без ответов
Пора рассмотреть модель в целом поглубже. Что говорит плохая модель о поведении приложения диспетчеров, правильна ли она?
В плохой модели ассоциация «контролер»(controller) равна «1 : *». Сторона «*» допускает нуль. В итоге мы теряем то тонкое различие между дежурным диспетчером, которому не назначена зона управления, и выходным диспетчером.
А вот как выглядит правило R2 в плохой модели: «0..1: 0..1». Эта ни к чему не обязывающая ассоциация охватывает и выходных диспетчеров, которые не вошли в систему, и станции дежурства, которые в данный момент не используются. Вот мы с вами и потеряли ограничение, указывающее, что во время дежурства диспетчер должен быть авторизован в системе.
Если диспетчер не использует станцию дежурства, может ли он все еще курировать зону управления? На этот вопрос модель отвечает “Да”, поскольку эти две ассоциации на плохой модели полностью независимы друг от друга.
Если диспетчер не курирует какие-либо зоны управления, может ли он при этом быть авторизованным на станции дежурства? И хорошие, и плохие модели позволяют это. И согласно правилам применения, это нормально.
На примере таблицы «Самолеты»(из первой части статьи) одного класса мы рассмотрели вопросы, на которые могла бы ответить модель. Попробуйте спросить у плохой модели: «Какие авиадиспетчеры сейчас не работают?». Она не может ответить на этот вопрос. Она может показать, какие диспетчеры не курируют зоны управления. Но ведь эти диспетчеры могут быть на дежурстве. Можно ли исправить это, добавив атрибут статуса в класс диспетчера в плохой модели? Само собой. Однако при этом вы все равно упускаете диспетчеров со значением «выходной», которые при этом курируют зону управления.
Решение, включающее использование атрибута состояния, также требует императивного языка для управления ограничением. К чему это ведет? Больше кода и больше тестирования — и все из-за одного атрибута статуса. Один атрибут статуса — это не страшно. Временами. Но если злоупотреблять этим (а именно этим часто и занимаются), то сложность в моделях состояний и действий может стать очень велика.
Вот один пример невалидного сценария, разрешенного в плохой модели:
Изображение выше выглядит нормально, за исключением диспетчера AT51 (Owen), который курирует зону управления CZ1, но не авторизован на станции дежурства.
В пятой (и заключительной части) мы поговорим о преимуществах и недостатках обеих моделей (да, у плохой тоже есть преимущества) и о том, как сделать хорошую модель более лаконичной.
Актуальные вакансии компании Retail Rocket
С переводом помогали: Бюро переводов Allcorrect
Редактор: Алексей @Sterhel Якшин