[Перевод] Как построить четкие модели классов и получить реальные преимущества от UML. Часть 2

04f4810ce9cda6de94333e34fa9770da.png

Вторая часть перевода статьи Леона Старра, инженера программных моделей. Первая часть вот здесь. В этой части — о семантике и о том, что отличает хорошую модель.

UML и семантика, лежащая в его основе

Итак, мы рассмотрели пример лишь одного класса и заметили, что даже он охватывает сразу несколько правил. Используя табличное представление, мы получаем довольно жесткую структуру данных, поэтому можем думать о механике доступа к ним без учета конкретной реализации. Мы уже усвоили, что можем делать те или иные утверждения о том, что мы знаем, что можем вычислить, а что — не можем. И все это без знания каких-то алгоритмов, диаграмм, действий или состояний. У нас просто есть схема одного класса, и этого уже достаточно, чтобы отвечать на вопросы и оценивать соответствие миру, которым планируем управлять. А представьте, что будет, когда бы добавим сотни классов и отношения.

Соблюдение ограничения идентификатора

У вас может возникнуть резонный вопрос — где на самом деле применяется ограничение {I}. Модель классов устанавливает лишь требование идентификации. Когда код пишет программист или генератор модели, то должен иметь место систематический механизм обработки идентичности. При этом встроенная архитектура может использовать хэш-таблицу, а корпоративная — полагаться на доступные механизмы БД для обнаружения и отклонения повторяющихся записей.

Хорошая модель

Чтобы сравнить модели, мы постараемся провести четкую разницу между хорошей моделью и плохой. Может показаться, что во мне говорит ЧСВ, когда я называю свои модели хорошими, а все остальные — плохими, это довольно ошибочная практика. Поэтому давайте сконцентрируемся на ощутимых различиях, которые помогут нам улучшить методы анализа и моделирования, а как следствие — создавать лучший код.

На рисунке ниже — диаграмма классов условно хорошей модели

Хорошая модельХорошая модель

Некоторые части нотации выше представляют собой уже знакомый вам UML, но со своими особенностями. Поэтому мы пройдёмся по ним, рассмотрим структуру данных базовой таблицы, после чего ещё разок оценим общую картину. И потом уже сможем противопоставить хорошую модель плохой.

Классы авиадиспетчеров

В левой половине модели есть три класса: авиадиспетчер (Air Traffic Controller), выходной диспетчер (он же Off Duty Controller) и дежурный диспетчер (On Duty Controller). Вот как может выглядеть таблица суперкласса, заполненная примерными данными.

ID {I}

Имя

Дата рождения

Рейтинг

диспетчер 53

Тошико

12 июня 1975 года

A

диспетчер 67

Гвен

28 марта 1981 года

B

диспетчер 51

Оуэн

23 декабря 1974 года

C

Смотрите, что имеем. Три авиадиспетчера, для каждого из которых мы точно знаем имя, дату рождения и квалификацию (рейтинг). В таблице у значения ID есть тег {I} — что это значит? Правильно, что эти значения должны быть уникальными. В таблице есть данные, которые относятся к диспетчеру, независимо от того, находится ли сейчас авиадиспетчер на своём рабочем месте или у него выходной.

Обратите внимание на текст {disjoint, complete} около отношения обобщения R1 — это пара стандартных тегов UML. Это значит, что каждый объект «Авиадиспетчер» может быть в состоянии «дежурный диспетчер» или «выходной диспетчер». А элемент disjoint означает, что находиться в двух состояниях сразу авиадиспетчер не может. Complete же уточняет, что каждый авиадиспетчер точно находится или на работе, или не на работе. При этом статус каждого точно определен в любой момент времени.

Само собой, невозможно, чтобы какой-то объект, находящийся на дежурстве или вне дежурства, не был объектом «Авиадиспетчер». Вот и получается, что R1 не представляет собой обобщение стиля наследования — это, скорее, подтипирование реляционных подмножеств. Это XOR-моделирование классов, отличный инструмент для включения логики в структуру данных.

Отношение обобщения Disjoint — CompleteОтношение обобщения Disjoint — Complete

Дежурный диспетчер

ID {I, R1}

Время регистрации в системе

Станция {R3}

диспетчер 53

27.09.08, 15:00

S2

диспетчер 67

27.09.08, 11:00

S1

Когда диспетчер находится на дежурстве, он должен быть зарегистрирован на месте дежурства, то есть на станции. При этом и ID самой станции, и время регистрации в системе для каждого из диспетчера сохраняются, и данные эти актуальны только во время дежурства.

Посмотрите на тег {R3} в атрибуте ссылки станции. Он значит, что «станция» соответствует идентификатору на другой стороне R3. В нашем случае это Duty_Station.ID.

Это очередной пример механики структуры данных, независимой от платформы. Допустим, вам интересно, на какой станции зарегистрирован диспетчер 53? Из структуры нашей таблицы понятно, что для ответа на этот вопрос достаточно просто найти строку с диспетчер 53 в столбце ID, а в столбце «Станция» получить значение S2.

А можно пойти дальше и получить данные из таблицы станций, используя S2 как ключ. Тогда получится ответить и на более сложный вопрос, например, «Кто из диспетчеров зарегистрировался в системе станции S2?». Опять ищем нужную строку в таблице дежурного диспетчера в столбце «Станции» и получаем значение ID  диспетчер 53.

Суть этой проверки просто в том, чтобы показать, что данные в любой реализации так или иначе связаны. Но это не значит, что нам надо реализовывать свои действия в SQL и подобном. Я работаю с системами, которые удаляют ссылочные атрибут и заменяют их указателями, дескрипторами или индексами какого-либо рода.

Направления доступа для каждой связи обычно оптимизируются или отключаются при необходимости (ряд компиляторов моделей, например, сканируют язык действий, видят, что в определенном направлении связи не выполняется никаких операций записи, и пропускают код сеттера).

Есть компиляторы моделей, которые проделывают все это сами, выдавая хороший C, Java, C++ и (да-да) ассемблер. Но вот генерация какого-либо SQL — это, конечно, вариант для систем типа БД.

Заметьте, что на атрибуте ID присутствует тег R1, который указывает, что он одновременно является идентифицирующим атрибутом дежурного диспетчера, соответствующего его ID в суперклассе «диспетчер». Все отношения в хорошей модели «склеены» со ссылочными атрибутами.

Выходной диспетчер

ID {I, R1}

диспетчер 51

Как это ни странно, для выходных диспетчеров данные не хранятся, по крайней мере, мы об этом пока не знаем. Главное назначение этого класса в том, что он показывает, что время регистрации в системе и ID станций не сохраняются для тех диспетчеров, которые не на службе. Этот трюк дает нам возможность не использовать значение «не применимо» в наших таблицах. Что плохого в этом значении? Если вы в принципе разрешаете такие пустые значения, то для обработки особых случаев в своем коде вы напрашиваетесь на логику «если — то». А еще требуете место для хранения, которое на самом деле не нужно приложению.

А еще смотрите — как только диспетчер уходит с работы, данные станции и времени регистрации будут удалены (или заархивированы). Так как диспетчер не может одновременно и работать, и не работать, то при миграции объекта «диспетчер» он или получает, или теряет данным. Вот как это отображается в таблицах.

Авиадиспетчер

ID {I}

Имя

Дата рождения

Рейтинг

диспетчер 53

Тошико

12 июня 1975 года

A

диспетчер 67

Гвен

28 марта 1981 года

B

диспетчер 51

Оуэн

23 декабря 1974 года

C

Перемещение ролиПеремещение роли

Так проводится различие между объектом «диспетчер», который является суммой общих и конкретных данных, представленных в любой момент времени двумя экземплярами класса.

Зона управления (Control Zone)

Имя {I}

Трафик

Диспетчер {R2}

CZ1

12

диспетчер 53

CZ2

4

диспетчер 53

CZ3

6

диспетчер 67

Зона управления — это область воздушного пространства, находящаяся в ведении одного ATC. Правило, которое мы зафиксируем, состоит в том, что каждая зона управления всегда должна быть подконтрольна какому-то диспетчеру. При этом для каждой зоны управления у нас тоже есть уникальное имя, количество трафика и назначенный для неё диспетчер. Возможно, вы придумаете и еще какие-то полезные атрибуты, скажем, объем, местоположение и прочее. Но пока для примера все же упростим.

Теперь мы можем ответить на вопрос «Каким объемом трафика управляет диспетчер 53?».

Станция дежурства

ID {I}

Местоположение

Вместимость

S1

Впереди

20

S2

Впереди

45

S3

В центре

30

У каждой станции дежурства есть уникальный идентификатор, общее местоположение объекта и максимальная пропускная способность (она же — объем трафика, которым станция может управлять).

И тут уже нет никаких ссылочных атрибутов, потому что у нас уже есть один в классе дежурного диспетчера. Теперь, если надо ответить на вопрос «Сколько лет человеку, авторизованному на станции S2?», то мы сможем это сделать. Для этого берем значение S2 и проверяем таблицу дежурных диспетчеров на предмет нужной строки. Если она есть, переходим в таблицу «диспетчер», ищем строку с S2 и получаем из столбца «Дата рождения» нужное значение. Сравните её с текущей датой (она вам всегда доступна как системный параметр), проведите несложные вычисления — и возраст диспетчера у вас есть.

Как я отмечал выше, такая процедура актуальна только в контексте чтения или выполнения модели.  Когда вы добавляете модели состояний и действий, вы можете прогонять их в симуляторе, который управляет переходами состояний, очередями событий, совместной работой и доступом к данным/вычислениями. Как только вы сможете заставить это работать, вы сможете сгенерировать или написать код.

Программист или компилятор модели должен разработать эффективный способ «склеивания» всех частей вместе. Это позволяет весьма эффективно разделять ответственность между разработчиком модели (аналитиком), который определяет, какие данные надо соединить на любой из платформ, программистом (компилятором модели), который уже ищет способы реализации этих соединений для конкретных платформ.

Может показаться сложным, но это не так — ведь к многим элементам модели можно применить один системный паттерн переноса, вместе с несколькими параметрами конфигурации для локальной настройки. Поэтому нескольких таких паттернов вам хватит если не для обработки всего кода сразу, то для большей его части уж точно. В кропотливой ручной разработке кода для каждого класса нет необходимости.

Иллюстрированный сценарий

Таблицы — хороший способ описания абстрактной структуры данных. Но при тестировании может быть полезна иллюстрация, позволяющая увидеть, могут ли эти таблицы соответствовать конкретному сценарию реального мира.

Иллюстрированный сценарий ATCИллюстрированный сценарий ATC

На сценарии выше у нас три диспетчера, два на дежурстве, третий на заслуженном выходном. Каждый из них авторизован на одной выделенной станции, время регистрации каждого из них в системе мы тоже записали. Каждая зона управления подконтрольна одному дежурному диспетчеру. Что до выходного диспетчера, он не авторизован в системе, поэтому время его входа в систему не зарегистрировано.

Одна станция бездействует, и ни она, ни выходной диспетчер не имеют связи с зонами управления.

В общем, наша хорошая модель на самом деле позволяет обработать реалистичный сценарий.

В этом стиле диаграммы намеренно избегаются любые обозначения UML, и он очень полезен в плане избавления от так называемого аналитического паралича. Если ограничить себя безликим набором всяких фигурок и квадратиков, то можно очень легко погрузиться в какие-то придуманные вами абстракции и пропустить ситуации, которые в эти правила не вписываются. В этом есть своя ирония — уделяя кучу внимания элементам модели, вы на раз-два пропускаете паттерны в реализации данных, которые могут привести к четким абстракциям.

Поэтому я всегда стараюсь рисовать диаграмму в свободной форме, потом модель UML, и уже после этого сравниваю их. Такая валидация работает в обе стороны. Можно ли заполнить таблицы данными, приведенными на иллюстрации сценария? Могу ли я создать убедительную иллюстрацию сценария, используя только данные в моих таблицах? Если я обнаруживаю новый сценарий, я проверяю, подходят ли его данные модели.

Когда я расширяю модель, я могу нарисовать и обновленный сценарий, дабы посмотреть, имеет ли он вообще смысл. Переходя то туда, то обратно, я могу обнаруживать недостатки в собственном мышлении, и в конце концов приду к четкой и согласованной модели. Плюс здесь отличная побочка в виде полезной документации.

Ещё одно преимущество в том, что пользователи и эксперты по иллюстрированной модели могут дать мне отличную детальную обратную связь, нежели по модели или диаграмме последовательности. Увы, существующие инструменты UML поддерживают лишь аспект моделирования и анализа в виде тех самых фигурок-квадратиков-линий, огромный пласт инструментов анализа еще не освоен.

Поэтому не бойтесь придумывать свои собственные значки и обозначения элементов, берите понравившиеся клипарты и используйте обозначения UML для воплощения абстракций.

В третьей части мы поговорим о правилах и ограничениях, присущих хорошей модели, а также начнём разбирать плохую модель классов.

С переводом помогали: Бюро переводов Allcorrect

Редактор: Алексей @Sterhel Якшин

© Habrahabr.ru