Введение
Диаграмма классов занимает центральное место в проектировании объектно-ориентированной системы. Нотация классов используется на разных этапах проектирования и строится с различной степенью детализации. Язык UML применяется не только для проектирования, но и с целью документирования, а также эскизирования проекта. Я (в отличии от Гради Буча) не являюсь сторонником разработки проекта с использованием всех видов UML диаграмм, а также детального проектирования. Чаще всего я применяю UML для эскизирования, а также для проектирования по процессу ICONIX [Rosenberg]. В статье описана часть нотации классов UML, применение которой достаточно в большинстве случаев. Тут не будет информации о кратности ассоциаций и атрибутов, особенностях изображения параллельных операций, шаблонах (параметризованных классах) и ограничениях. При необходимости всю эту информации можно посмотреть в других книгах [Buch, Leonenkov]. Мы же ограничимся базовой частью нотации и больше внимания уделим применению диаграммы классов.
1 Элементы диаграммы классов
На диаграмме классов с помощью специальных символов изображаются типы данных программы и отношения между ними, хотя в некоторых случаях могут использоваться и некоторые другие элементы — пакеты и даже экземпляры классов (объекты) [Leonenkov].
1.1 Символ класса
Символ класса на диаграмме может выглядеть различным образом в зависимости от детализации диаграммы:
Вопросы детализации будут рассмотрены в следующих разделах, а сейчас надо обратить внимание, что символ класса содержит имя (Player
), набор операций (move
, get_gealth
) и атрибутов (pos
, state
). Для элементов класса могут задаваться тип, кратность, видимость и т.д.:
Формат спецификации атрибута:
видимость имя : тип [кратность] = значение_по_умолчанию
Формат спецификации операции:
видимость имя(аргумент: тип) = тип_возвращаемого_значения
В зависимости от параметра видимости элемент может быть:
- приватным (
private
, доступен только внутри класса) — задается символом «минус» (-
), может отображаться в виде квадрата; - защищенным (
protected
, доступен внутри класса, а также внутри классов-наследников) — задается символом «решетка» (#
), может отображаться в виде ромба; - открытым (
public
, доступен всем) — задается символом «плюс» (+
), может отображаться в виде круга.
Виртуальная функция и имя абстрактного класса выделяются курсивом, а статическая функция — подчеркивается.
1.2 Отношения классов
Диаграмма классов допускает различные виды отношений, рассмотрим их на части диаграммы модели некоторой игры:
В игре есть различные виды элементов (стены, сундуки, персонажи). Все эти элементы являются наследниками абстрактного класса AbstractItem
, при этом часть из них умеет двигаться (такие элементы должны быть унаследованы от MovingItem
). Наследование (отношение «является») изображается с помощью сплошной линии с закрытой стрелки, направленной в сторону суперкласса — на диаграмме класс MovingItem
унаследован от AbstractItem
, класс Player
— от MovingItem
и т.д. Штриховая линия с закрытой стрелкой задает отношение реализации (закрытое наследование).
Другой вид отношений между классами — включение, в объектно-ориентированном программировании различают два вида этого отношения — композицию и агрегацию. Напомню, что композиция — это разновидность включения, когда объекты неразрывно связаны друг с другом (время их жизни совпадает), в случае агрегации, время жизни различно (например, когда объект вложенного класса может быть заменен другим объектом во время выполнения программы).
Отношение композиции обозначается закрашенным ромбом, который рисуется со стороны включающего класса — так, класс MovingItem
включает в себя класс Position
, т.к. перемещающийся объект всегда имеет позицию. Отношение агрегации изображается незакрашенным ромбом — игрок (Player
) агрегирует состояние (IPlayerState
).
Если вы знакомы с паттернами State, Strategy или Delegation — секцию можно пропустить.
На приведенной выше диаграмме используется шаблон проектирования Состояние (State), являющийся разновидностью шаблона Делегирование (Delegation
) и близкой к паттерну Стратегия (Strategy
). Суть делегирования заключается в том, что для упрощения логики работы класса, часть его работы может быть передана (делегирована) вспомогательному классу. В свою очередь, паттернState
может быть добавлен, например, на этапе рефакторинга если в нескольких функциях класса встречается разлапистая проверка состояния объекта для выполнения тех или иных действий. В нашем случае персонаж может взаимодействовать с ежом, предположим, что если персонаж движется сидя и контактирует с ежом — у него должно уменьшится здоровье, а если стоя — увеличится счет (points
). Кроме ежа могла быть еда, противники, патроны и т.д. Для демонстрации такого паттерна создан абстрактный классIPlayerState
и два наследникаStayState
иSeatState
. В классеPlayer
, при нажатии кнопкиCtrl
состояние могло бы меняться наSeatState
, а при отпускании — наStayState
. Таким образом, при выполненииstate->process_hedgehog(this)
наш игрок каким-то образом, определенным объектомstate
, проконтактирует с ежиком.
Шаблон проектирования Delegation
(и все его разновидности) — хороший пример для демонстрации агрегации. В нашем случае состояние игрока может меняться за счет изменения объекта по указателю, т.е. время жизни объектов различается.
Наиболее общий вид отношений между классами — ассоциация, обозначается сплошной линией (иногда со стрелкой). Вообще, и композиция, и агрегация, и обобщение (наследование) — являются частными случаями ассоциации. В нашей диаграмме с помощью ассоциации показано, что класс IPlayerState
изменяет stats
(health
и points
) объекта Player
. Ассоциация может иметь название связи, поясняющую суть отношения. В качестве названия связей композиции и агрегации часто используется имя соответствующей переменной. Кроме того, ассоциация может иметь кратность, она задается на концах линии:
1
— одна связь (на нашей диаграмме показано, что один игрок включает в себя один экземпляр классаIPlayerState
);*
любое число связей (если бы на диаграмме был класс игрового поля, то с помощью звездочки можно было бы показать, что оно может содержать произвольное число игровых элементов);[от..до]
— может задаваться диапазоном. Так диапазон[0..*]
эквивалентен звездочке, но если мы захотим показать, что должно присутствовать более одного объекта — можем записать[1..*]
Последний вид отношений, который мы рассмотрим — зависимость, изображается штриховой (прерывистой) линией. Если есть стрелка — то направлена от зависимого к независимому классу, если стрелки нет — то классы зависят друг от друга. Под зависимостью понимается зависимость от интерфейса, т.е. если интерфейс независимого класса изменится — то придется вносить изменения в зависимый класс. В нашей диаграмме SeatState
и StayState
зависят от класса Player
, т.к. обращаются к его методам для изменения характеристик игрока. Для изображения отношения дружбы между классами используется отношение зависимости с подписью friend
.
Очевидно, что не все виды отношений стоит отображать на диаграмме и одни отношения могут быть заменены другими. Так, я убрал бы из нашего примера отношения зависимости, однако при некоторых обстоятельствах (например при эскизировании на маркерной доске) они были бы вполне уместны. Расстановка кратности и имен связей тоже выполняется далеко не во всех случаях. Вообще, не стоит помещать на диаграмму лишнюю информацию. Главное — диаграмма должна быть наглядной.
2 Использование диаграммы классов
Мы рассмотрели основные обозначения, используемые на диаграммах классов — их должно быть достаточно в подавляющем большинстве случаев. По крайней мере, владея этим материалом вы легко сможете разобраться в диаграммах шаблонов проектирования и понять эскиз любого проекта. Однако, как правильно строить такие диаграммы? В каком порядке и с какой степенью детализации? — ответ зависит от целей построения диаграммы, поэтому приведенный материал будет разбит на подразделы в соответствии с целями моделирования.
Стоит отметить, что у Гради Буча советы по использованию UML даны в книге «Руководство пользователя» [Buch_Rambo], но в его «Объектно-ориементированном анализе» [Buch] можно найти хорошие примеры и критерии качества проекта. Леоненков [Leonenkov] и вовсе избегает этой темы, оставляя лишь ссылки на литературу, конкретные рекомендации я нашел у Лармана [Larman] и Розенберга [Rosenberg], часть материала основана на моем личном опыте. Фаулер рассматривает UML как средство эскизирования, поэтому у него свой (сильно отличающийся от Буча и Розенберга) взгляд на диаграмму классов [Fauler].
2.1 Диаграмма классов как словарь системы, концептуальная модель
Словарь системы формируется параллельно с разработкой диаграммы прецедентов, т.е. технического задания. Выглядит это следующим образом — вы задаете заказчику вопросы типа «что еще может сделать пользователь?», «что произойдет (должна выдать система) если пользователь сделает нажмет на <эту>
кнопку?», а ответы на них записываете в виде описания прецедентов. Однако, заказчик, давая ответы может называть одни и те же вещи разными именами — из личного опыта: говоря «клетка», «пересечение», «узел» и «ячейка» заказчик может иметь ввиду одно и тоже. В вашей же системе все эти понятия должны быть представлены одной абстракцией (классом/функцией/…). Для этого при общении с заказчиком стоит фиксировать терминологию в виде словаря системы — очень хорошо с этим справляется диаграмма классов.
Гради Буч для построения словаря системы предлагает выполнять в следующем порядке [BuchRambo]:
- анализируя прецеденты, определить какие элементы пользователи и разработчики применяют для описания задачи или ее решения;
- выявить для каждой абстракции соответствующее ей множество обязанностей (ответственности). Проследите правильность распределения обязанностей (в том числе, соблюдение принципа единой обязанности [solid_refactoring]);
- разработайте процедуры и операции для выполнения классами своих обязанностей.
В качестве примера рассмотрим словарь системы для игры «Сапер». На приведенной ниже диаграмме показан вариант, который получился в результате обсуждения задачи у моего студента. Видно, что на диаграмме изображены сущности и их атрибуты, понятные для заказчика, эту диаграмму стоит иметь перед глазами при составлении прецедентов чтобы не называть «Клетку» — «Полем», вводя всех в заблуждение. При построении словаря системы следует избегать нанесения на диаграмму функций классов, т.к. настолько детализированное распределение обязанностей лучше выполнять после построения диаграмм взаимодействия.
В процессе проектирования словарь системы может дополняться, Розенберг очень хорошо демонстрирует это в своей книге описывая итеративный процесс проектирования ICONIX [Rosenberg]. Например, после рассмотрения нескольких прецедентов может оказаться, что несколько классов реализуют один и тот же функционал — для решения проблемы надо более четко прописать обязанности каждого класса, возможно, добавить новый класс и перенести часть этих обязанностей ему.
Ларман предлагает строить концептуальную модель системы [Larman] — это примерно то, что мы описали как словарь системы, но помимо терминов предметной области в ней фиксируются некоторые отношения, понятные заказчику. Например, заказчик понимает (и фиксирует в техническом задании), что <покупку>
оформляет <продавец>
— следовательно, между продавцом и покупкой существует отношение ассоциации "оформляет"
. Я рекомендую строить концептуальную модель, дорабатывая словарь системы, хотя Ларман рекомендует сначала добавлять ассоциации, а затем — атрибуты.
2.2 Диаграмма классов уровня проектирования
В любом объектно-ориентированном процессе проектирования диаграмма классов является результатом, т.к. является моделью, наиболее близкой к реализации (коду). Существуют инструменты, способные преобразовать диаграмму классов в код — такой процесс называется кодогенерацией и поддерживается множеством IDE и средств проектирования. Например, кодогенерацию выполняет Visual Paradigm (доступно в виде плагинов для множества IDE), новые версии Microsoft Visual Studio, такие средств UML-моделирования как StarUML, ArgoUML и др. Чтобы построить по диаграмме хороший код, она должна быть достаточно подробной. Именно о такой диаграмме идет речь в этом разделе.
До Ларману [Larman] до начала построения диаграммы классов уровня проектирования должны быть построены диаграммы взаимодействия и концептуальная модель системы. При этом порядок построения диаграммы следующий:
- перенести классы с диаграммы последовательности;
- добавить атрибуты концептуальной модели;
- добавить имена методов по анализу диаграмм взаимодействия (например, диаграмм последовательностей [uml_sequence_diag]);
- добавить типы атрибутов и методов;
- добавить ассоциации (на основании атрибутов — отношения композиции и агрегации);
- добавить стрелки (направление ассоциаций)
- добавить ассоциации, определяющие другие виды отношений (в первую очередь, наследование).
Отношения, добавляемые на диаграмму классов уровня проектирования отличаются от тех, что были в концептуальной модели тем, что они могут быть не очевидны для заказчика (эту диаграмму он вообще смотреть не должен — она разрабатывается для программистов). Если на этапе анализа технического задания мы могли выделить основные сущности, не задумываясь о том, как это будет реализовано, то теперь обязанности между нашими классами должны быть окончательно распределены.
Например, при анализе задания на игру «Сапер» мы выделили классы <Флажок> и <Мина>, но будут ли эти классы в окончательном проекте или останутся только в воображении? — решение можно принять только проанализировав диаграммы взаимодействия. Ведь возможен и такой код:
enum class CellType { EmptyOpened, EmptyClose, EmptyCloseFlagged, MineOpened, MineClose, MineCloseFlagged }; class PlayingGround { // ... CellType **m_ground; }
Поясню (для тех, кто не пишет на С++) — тут создается перечисление, которое задает тип ячейки. Ячейка может принимать одно из этих шести значений (пустая открытая
, пустая закрытая
, пустая закрытая с флажком
и т.п.). В таком случае, ячейка никак не сможет сама реагировать на нажатия мыши и отвечать за свое отображение (например пустая открытая
должна выводить число мин вокруг себя) — все эти обязанности, видимо, лягут на класс PlayingGround
.
Пример выше утрированный и однозначно не является образцом хорошего проектирования — на класс PlayingGround
возложено слишком много обязанностей, но могли ли мы учесть это при анализе технического задания? Сможем ли мы это сделать до разработки диаграмм взаимодействия для проекта любой сложности? — именно поэтому построение диаграммы классов является последним этапом проектирования.
2.3 Диаграмма классов для эскизирования, документирования
Под эскизированием понимают моделирование некоторой (интересной нам в данный момент) части системы. Например, эскизирование может выполняться на маркерной доске когда в вашу компанию попадет новый сотрудник и вы будете помогать ему «влиться» в существующий проект. Очевидно, что если если дать человеку диаграмму классов уровня проектирования — разбираться он будет долго. Суть эскизирования в избирательности — вы выносите на диаграмму только те элементы, которые важны для пояснения того или иного механизма.
Сторонником применения UML для эскизирования является Фаулер [Fauler], который считает, что целостный процесс проектирования с использованием UML слишком сложен. Эскизирование применяется очень часто (не только при объяснении проекта на маркерной доске):
- в любой книге, посвященной паттернам проектирования, вы найдете массу UML диаграмм, выполненных в этом стиле;
- при моделировании прецедента выбираются классы, за счет которых этот прецедент реализуется. Моделирование прецедента выполняется при рефакторинге;
- в документацию для разработчиков нет смысла вставлять диаграмму классов уровня проектирования — гораздо полезнее описать наиболее важные (ключевые) моменты системы. Для этого строятся эскизные диаграммы классов и диаграммы взаимодействия. Также существуют специальные инструменты построения документацию по готовому коду — такие как JavaDoc или Doxygen [doxygen_codegeneration], в частности они строят диаграмму классов, но чтобы документация была понятной, в исходный код программы требуется вносить комментарии специального вида.
Каких-либо конкретных рекомендаций к эскизам диаграмм классов предложить невозможно, кроме того, обычно это достаточно простая задача. Важно понимать суть — избирательность представления элементов снижает сложность восприятия диаграммы.
3.4 Диаграмма классов для моделирования БД
Частным случаем диаграммы классов является диаграмма «сущность-связь» (E-R диаграмма), используемая для моделирования логической схемы базы данных. В отличии от классических E-R диаграмм, диаграмма классов позволяет моделировать поведение (триггеры и хранимые процедуры).
Обычно ситуация выглядит следующим образом — вы разработали систему, состояние которой нужно сохранять между запусками, например:
- в вашей игре надо хранить информацию о достижениях пользователя — пройденные уровни, набранные очки и т.п.;
- если игра сетевая — то может существовать сервер, на котором хранятся достижения разных игроков;
- ваше приложение для телефона записывает координаты пользователя и позволяет ему оставлять пометки на карте. Вся эта информация тоже не должна уничтожаться после закрытия приложения.
Хранимые между запусками данные должны каким-то образом загружаться по запросу пользователя, т.е. должны задаваться параметры соответствующих классов. Например, приложение должно получить из базы данных список треков (маршрутов) и отобразить его в виде списка в меню программы. При выборе элемента списка — запросить в БД параметры трека, создать объект трека и отобразить его на карте. В любом случае, данные с базы используются при инициализации объектов программы — это важно понимать.
Для моделирования схемы БД с помощью диаграммы классов нужно [Buch_Rambo]:
- идентифицировать классы, данные которых должны храниться между запусками приложения (или обращениями пользователя) и нанести эти классы на отдельную диаграмму;
- детально специфицировать атрибуты классов, ассоциации и кратности. В E-R модели кратности имеют огромное значение — так например, при наличии кратности «многие-ко-многим» придется создавать вспомогательную таблицу. Используйте специфические стереотипы классов и пометки атрибутов (для задания первичных и вторичных ключей, например) [uml_datamodeling];
- решить проблемы использования полученной диаграммы в качестве физической модели базы данных — циклические ассоциации, n-арные ассоциации и т.д. При необходимости создать промежуточные абстракции;
- раскрыть операции, важные для доступа к данным и поддержания целостности;
Заключение
В статье я постарался описать наиболее существенные элементы диаграммы классов, а также аспекты их применения. Просматривается, что диаграмма строится на начальном этапе проектирования (концептуальная модель) и является его результатом. На всех этапах проектирования созданная в начале диаграмма классов дорабатывается, т.е. я рассматриваю итеративный процесс (такой как RUP или ICONIX). Кроме того, показано, использование диаграммы классов в других целях — эскизирования, документирования, моделирования логической схемы БД. На других страницах этого блога вы можете найти множество примеров использования диаграммы классов, например:
- диаграмма шаблона проектирования Publish-Subscriber [pattern_mvc];
- множество диаграмм, демонстрирующих шаги рефакторинга для приведения проекта в соответствие принципам SOLID [solid_refactoring];
- диаграммы классов для паттерна адаптер и сетевого чата [pattern_adapter].
Литература по диаграммам классов UML
- [Buch] Буч Градди Объектно-ориентированный анализ и проектирование с примерами приложений, 3-е изд. / Буч Градди, Максимчук Роберт А., Энгл Майкл У., Янг Бобби Дж., Коналлен Джим, Хьюстон Келли А.: Пер с англ. — М.: ООО «И.Д. Вильямс», 2010. — 720 с.
- [Leonenkov] Леоненков, А.В. Самоучитель UML 2 / А.В. Леоненков. – СПб.: БХВ — Петербург, 2007. – 576с.
- [Larman] Ларман, К. Применение UML и шаблонов проектирования: Уч. Пос / К. Ларман. — М.: Издательский дом «Вильямс», 2001. — 496 с.
- [Rosenberg]Розенберг Д., Скотт К. Применение объектного моделирования с использованием UML и анализ прецедентов.: Пер. с англ. М.: ДМК Пресс, 2002
- [Badd] Бадд Т. Объектно-ориентированное программирование в действии: Пер с англ. — СПб: «Питер», 1997. — 464 с. 2002
- [Buch_Rambo] Буч Г., Рамбо Д., Джекобсон А. Язык UML. Руководство пользователя: Пер. с англ. — М.: ДМК, 2000. — 432 с.
- [Fauler] Фаулер, М. UML. Основы / М. Фаулер, К. Скотт; пер. с англ. – СПб.: Символ – Плюс, 2002. – 192 с.
- [solid_refactoring] SOLID принципы. Рефакторинг — URL: https://pro-prof.com/archives/1914
- [uml_sequence_diag]Основы UML. Диаграммы последовательности — URL: https://pro-prof.com/archives/2769
- [uml_datamodeling] UML data modeling — URL: http://www.agiledata.org/essays/umlDataModelingProfile.html
- [doxygen_codegeneration] Использование doxygen — URL: https://pro-prof.com/archives/887
- [pattern_mvc] Паттерны MVC и Publish-Subscriber — URL: https://pro-prof.com/archives/2400
- [pattern_adapter] Работа с сетью в Qt. Сокеты. Паттерн Adapter — URL: https://pro-prof.com/archives/1372
Прекрасная статья. Спасибо!