[Перевод] Как построить четкие модели классов и получить реальные преимущества от UML
Мне показался близким подход Леона Старра к объяснению чётких моделей классов и описанию их преимуществ. Настолько, что мы в Retail Rocket решили сделать перевод его большой статьи «How To Build Articulated UML Class Models». Будем выкладывать по частям, под катом — первая из трёх.
Аннотация
Типичная модель классов UML — это довольно смутное представление реальности, которое можно получить в попытках эту самую реальность представить. По крайней мере, именно из этих попыток состоял мой немалый опыт как разработчика исполняемых моделей и консультанта проектов. Давайте определимся с терминами. «Четкой» моделью классов я называю модель, ясно и точно выражающую критически важные системные правила. Контраст тут лучше всего заметен, когда мы сравниваем плохую и хорошую модели. В тексте я сравню не какие-то сферические модели в безвоздушном пространстве, а реальные примеры, которые постоянно встречаются в проектах. А еще перечислю те негативные последствия, с которыми можно столкнуться при использовании неточной модели классов в ПО. Само собой, обозначу и плюсы применения правильного подхода в работе с моделями, а также опишу ряд простых методов, которые можно для создания моделей классов, точнее выражающих правила.
Модель классов UML даст вам не просто красивое представление структур данных, а нечто большее. Если модель классов будет четко сформулированной, вы сможете выявить не самые очевидные, но весьма важные ограничения своих приложений, включая незаметные правила и предположения. Которые могут быстро переквалифицироваться в довольно неприятные баги. Хорошая модель избавит вас от появления опасных ошибок еще на уровне проектирования, чем сэкономит вам и команде уйму времени и сил. Важно — если вы не получаете таких результатов от собственных моделей классов, это звоночек: возможно, вам в принципе не стоит над ними работать.
Если вы читали отраслевую литературу, то наверняка встречали там модели классов UML, которые демонстрировали лишь верхушку айсберга в плане своего потенциала. Например, очень часто игнорируют ключевые ограничения приложений. Или модель может быть двояко интерпретируема. Такое частенько списывают на привычное (увы) самооправдание «Решим потом, при кодировании». Но погодите, если можно всё допилить при кодировании, может, стоит тогда и начать сразу?
Модель классов понята неправильно
С ключевого недопонимания обычно и начинаются все проблемы. Смотрите, в литературе по UML модели классов часто описываются как «Представление статической структуры». То есть, модель классов в принципе не отражает динамическое поведение, на это делается упор. Но ведь самое интересное заключено в диаграммах состояния, активности и последовательности, языке действий, сотрудничестве. Да и динамические же свойства вашего ПО и составляют его наблюдаемые характеристики. Вот и получается, что модели классов на UML выглядят примерно так же, как попытка притащить отличный бифштекс на вечеринку вегетарианцев. Взять-то с собой можно, но угостить будет некого. Итого вы сами не понимаете, зачем вообще заниматься моделированием классов и отношений, и хотите побыстрее с этим разделаться.
Статические модели могут выражать динамические правила
Модели классов статичны. Как и правила. Давайте на простом примере.
— Со взлетно-посадочной полосы в каждый отдельный момент времени может взлетать только один самолет.
Вроде, все понятно. Если все идет как надо, то во время выполнения программы правило не должно изменяться. Система управления воздушным движением состоит из тысяч подобных правил, причем какие-то по умолчанию понятны и даже очевидны, а вот другие не так заметны. Но это не делает их менее важными.
Так вот. Динамические свойства ПО (отказоустойчивость, поведение и прочее) определяются статическими правилами. У каждого приложения есть набор таких правил, и в идеальном мире их стоит формировать вообще без оглядки на платформу. Немногие разработчики в состоянии понять, как сильно нотация диаграмм классов может определять и ограничивать поведение сложных систем. Но к этому можно прийти. Достаточно перестать воспринимать модели классов как простые хранилища данных.
Чем плоха нечёткая модель
Раз в вашей модели классов правила не выражены, то они попадают куда-то ещё. Все незафиксированные в модели состояний правила попадают в диаграмму состояний. Если и там они не фиксируются, то переносятся в диаграмму активностей.
Когда вы пишете код не при помощи компилятора, а вот этими вот руками, то вы можете отразить все забытые правила в коде. В остальном же вы де-факто оставляете эту задачу пользователям. Что очень нежелательно, если вы занимаетесь разработкой авиационной электроники, медицинскими системами или антиблокировочными тормозами.
В чем плюсы точных моделей классов с явно выраженными правилами
Давайте базово обозначим, зачем вообще вводить ограничения и правила в моделях классов.
Когда вы поймёте, как это делать, то осознаете, что многие правила гораздо проще (и эффективнее) описываются именно с помощью моделей классов.
Правила в модели классов довольно наглядны для пользователей и экспертов в конкретной области применения. Они смогут дать вам отличную конструктивную обратную связь и покритиковать ещё до того момента, когда структура зафиксируется.
Всё станет наглядным, что даст вам возможность принимать ключевые решения уже на этапе моделирования. А это существенно дешевле, чем на любом другом.
В диаграммах состояний и классов будут последовательности вызовов или переходов, точки синхронизаций и тайминги, что сложнее тестировать, нежели статические структуры.
Меньше правил в действиях — меньше кода в целом. Может быть, будет больше данных, но можно оптимизировать процесс разработки для их обработки будет проще, если выбирать хранилища только с доступом на чтение вместо классических переменных.
Выраженные в данных правила можно быстро настроить и обновить без правки, перекомпиляции и повторного тестирования кода.
Если вы на старте сосредоточитесь на данных, то с большей вероятностью сможете выделить настраиваемые параметры, чтобы не зашивать их в саму логику приложения. Вместе с хорошей моделью данных у вас появится возможность комфортно разместиться на двух стульях сразу — к вашим услугам и конфигурируемость, и жёсткое соблюдение основных принципов сразу.
Задавая точные вопросы (а их надо будет задавать, чтобы создать четкую модель), вы станете умнее. Ну ОК, как минимум, будете выглядеть умнее. Я сам видел, как новички благодаря этому быстро прокачивались до экспертов. Иногда даже забирая контроль над проектов у старожилов, пока те продолжали считать себя самыми умными. Без шуток, моделирование четко описанных классов — это не только технология разработки ПО, это технология развития серого вещества в вашей черепной коробке.
В общем, я могу долго рассуждать об этом довольно абстрактно, так что давайте с примерами.
Охват нескольких правил при помощи одного класса
Давайте с простого — заложим фундамент с помощью единственного класса. Разберёмся с этим и поговорим о чем-то покруче и побольше. Но начать надо именно с этого, так вы поймете, как много правил можно выразить в одном моделируемом классе.
Допустим, мы хотим отслеживать сразу несколько самолетов, которые пролетают через аэропорт. Само собой, мы хотим избежать любых столкновений и точно знать, где прямо сейчас тот или иной самолет. То есть наше с вами ПО должно поддерживать внутреннее представление каждого из имеющихся самолетов.
Абстрагирование класса от данных в реа льном мире
Итак, что мы сделали. Мы абстрагировали класс с названием «Самолет». Но что значит эта абстракция? Класс Java, класс Objective-C, или вообще Python? Или, может, таблица БД или раздел XML-файла?
Многое в моей работе связано со встраиваемыми системами, поэтому в моём случае это или структура ассемблера, или С. Каждая из разрабатываемых систем внедрения несет в себе разный набор ограничений и допущений. Но погодите-ка, наша абстракция — это не из списка выше, это класс UML. Но что это значит? Вот, знакомьтесь. Это та самая проблема с UML и моделируемыми абстракциями в целом.
Когда у вас перед глазами отличный хард-код, вы вполне в состоянии прокрутить у себя в голове механику и представить ситуации вида «Что будет, если», «Чего не будет» и прочее. Серьезному языку моделирования нужно что-то большее, что обычная графическая нотация — за ней должна стоять семантика (значение), причем однозначная. Получится, что модель, как и код, означает что-то одно и работает только конкретным образом. Никаких разночтений и субъективных толкований.
Платформонезависимое описание класса
Спецификация, в отличие от кода, должна быть максимально независимой от платформы, чтобы мы смогли нормально отделить правила реального мира (читай — приложения) от правил в рамках конкретной реализации.
В примере выше нам интересны правила управления самолетами, которые всегда должны соблюдаться. То есть правила о минимальном расстоянии по вертикали между двумя летящими самолетами ни в коем случае не должны меняться из-за того, что систему слежения перенесли с Linux на Windows. Даже если мы используем UML, нам надо думать о классе как о реальной структуре данных.
Нарисуйте на листе бумаги прямоугольник. В этом нет никакой интересной механики. А вот нарисовать впечатляющую кучу прямоугольников на том же листе — это уже внушительная трата времени. Но если договориться о структуре данных класса UML, то можно будет оценить операции, которые можно применять к данным в этой структуре.
И вот здесь-то и включается настоящее мышление.
Но для этого понадобится структура данных, которая не зависит от платформы. Бывает такое? Да, бывает, спасибо математике, теории множеств и реляционной модели данных. Вообще, структура данных для класса, нейтральная по отношению к платформе, это де-факто отношение, то есть таблица. Не таблица реляционной БД, это лишь одна из возможных реализаций.
Смотрите, вот так в виде независимой от платформы таблицы можно представить наш с вами класс самолета
Самолет
Бортовой номер {I} | Высота | Скорость | Курс |
N17846D | 8000 футов | 135 миль/ч | 178 град. |
N12883Q | 12 300 футов | 240 миль/ч | 210 град. |
Заполненная таблица для класса «Самолет»
В атрибуте «Бортовой номер» есть элемент {I} , это UML-тег, используемый как условное обозначение ограничения идентификатора [1]. Он значит, что в нашей таблице не может быть двух одинаковых значений бортовых номеров. Иными словами, в нашей абстракции прямоугольник может быть принят как конкретная структура данных. Действия по оперированию данными в таблице хорошо определены в реляционной алгебре и поддерживаются действиями UML. Если вы уже знакомы с реляционной теорией, то сразу поняли, что класс тут представлен как отношение в третьей нормальной форме (3NF). Если же нет, в Википедии есть полезная статья на эту тему. Некоторые из этих действий мы будем использовать для доступа к расписанию самолетов.
Таблицу выше можно создать с помощью различных структур программной реализации, выбор тут зависит от целевой платформы, оптимизации доступа на чтение-запись, а также от других характеристик, включая производительность и язык программирования. Наша цель — определить как можно больше правил приложения, инвариантных к платформе, при это не предъявляя к реализации особых требований. То есть программист (или же компилятор модели, да) сможет упаковать функциональность так, как ему заблагорассудится, главное, чтобы все правила приложения были в точности сохранены. Мне доводилось видеть таблицы, которые преобразовывали кто во что горазд — от классов C++/Java до массивов и списков структур на С, или вообще простых битовых полей на языке ассемблера.
Выражение правил приложения в пустой таблице (класс, независимый от платформы)
Итак, у нас есть структура данных класса, независимая от платформы. Давайте-ка вернемся к выражению того, что нам интересно — к правилам для самолетов. Так как правила приложения одинаковы вне зависимости от частных случаев на этом отрезке времени, то мы удалим данные и сосредоточимся на самой структуре. То есть на пустой таблице.
Самолет
Бортовой номер {I} | Высота | Скорость | Курс |
Пустая таблица — это независимая от платформы структура данных
Вот эта разница между структурой и содержанием (наполнением) — ключевая. Хотя для того, чтобы абстрагировать общие принципы и правила, мы изучаем данные как совокупность частных случаев, для генерации кода мы используем именно абстракцию (структуру). И получив абстракции, мы тут же отбрасываем данные.
Но на пути к абстракциям, особенно в сложной системе, в поисках закономерностей и тонких различий между экземплярами данных мы вынуждены продираться через чудовищное количество данных.
Давайте рассмотрим правила, выраженные этой простой структурой, не беспокоясь о том, какие данные в итоге попадут в таблицу.
Правила класса «Самолет»
Каждый самолет идентифицируется с помощью уникального бортового номера.
Никакие два самолета не могут иметь один и тот же бортовой номер.
У каждого самолета своя собственная высота полёта.
У каждого самолёта есть собственный курс.
Каждый самолет способен развить определенную скорость полёта.
Правила 1 и 2 устанавливаются ограничением идентификатора, описанного выше. Мы ещё поговорим поподробнее о том, как ограничение фактически применяется в реализации. Пока же всё, что мы знаем — оно однозначно заявлено в модели классов.
А вот правила 3–5 это следствие реляционного правила, «Каждая ячейка должна содержать ненулевое значение». Правило хорошее, благодаря ему мы и можем получать менее расплывчатые абстракции класса. В приведенном примере нам надо убедиться, что все летающие объекты, которые подпадают под определение самолета, на самом деле всегда имеют допустимые значения каждого атрибута.
На какие вопросы может ответить этот класс?
Можно продемонстрировать всю мощь лежащей в основе этого структуры данных. Давайте просто поставим перед нашей моделью несколько вопросов и проверим, осилит ли она дать нам ответы. Проверьте каждый вопросы — получится ли ответить на него с помощью заполненной таблицы.
Каков курс борта N12883Q?
Какие самолеты находятся ниже 10 000 футов?
Сколько самолетов находится в нашей зоне управления?
Какова скорость движения борта N17846D?
Возможно ли столкновение через 5 минут?
Оценка вопросов
Да. Мы можем просканировать столбец с бортовыми номерами, выбрать N12883Q и перейти по строке к значению в столбце «Курс». Если такого бортового номера не нашлось, значит, этого самолета нет в нашем районе.
Да. Проверяем столбец «Высота», выбирая каждую строку со значением меньше 10 000 футов, а потом смотрим в этих строках бортовой номер.
Нет. Наша модель ничего не говорит о зоне управления. Я вообще её только в этом вопросе впервые и упомянул. Предположим, что существует другой класс, «Зона управления», а в таблице «Самолеты» будет ссылка на атрибут какого-нибудь идентификатора зоны управления. Но в этом примере её нет, поэтому ответить на этот вопрос нельзя.
Да. То же самое, что в первом вопросе, найдите бортовой номер и проверьте это значение в столбце «Скорость».
Нет. Чтобы вычислить вероятность столкновения хоть с какой-то значимой степенью уверенности, у нас просто мало данных. Дело в том, что у нас недостаточно пространственных координат для каждого самолета. Да, мы знаем куда и как быстро он летит, но на самом деле не знаем, где он находится. Но это можно исправить.
Самолет
Бортовой номер {I} | Высота | Скорость | Курс | Широта | Долгота |
Расширенная таблица, которая может ответить на большее количество вопросов
Вот с такой таблицей нам по силам ответить и на пятый вопрос.
Заметьте, для оценки наших вопросов мы и вправду не нуждались в заполненной таблице, мы могли использовать и пустую, слегка перефразировав вопросы.
Каков курс указанного самолета?
Какие самолеты находятся ниже заданной высоты?
С какой скоростью летит указанный самолет?
Возможно ли столкновение в определенный период времени?
На эти вопросы мы можем ответить при помощи расширенной таблицы без данных, хотя вы явно заметили, что в неё надо добавить столбец «Скорость набора высоты». Это интересная информация, которую мы можем вычислить с помощью уже имеющихся у нас данных. Поэтому мы можем говорить о том, что новая таблица достаточно хороша, чтобы ответить на вопрос о столкновении — с этим произвольным атрибутом или же без него.
Но будет разумнее, если мы его добавим, так что возвращаемся к нашей нотации класса UML и вставляем его.
Бортовой номер {I} | Высота | Скорость | Курс | Широта | Долгота | /Скорость набора высоты |
Знак »/» перед скоростью набора высоты даёт понять, что мы получили это значение с помощью вычислений. Решение о том, стоит ли добавлять производные атрибуты, полученные путем вычислений, это тема для отдельной статьи, поэтому давайте пока просто условимся добавлять их по мере необходимости.
Продолжение следует
С переводом помогали: Бюро переводов Allcorrect
Редактор: Алексей @Sterhel Якшин