Как стать автором
Обновить

Использование диаграммы классов UML при проектировании и документировании программного обеспечения

Время на прочтение19 мин
Количество просмотров148K

Предисловие

Парадигма объектно-ориентированного программирования (далее просто ООП) повсеместно используется при создании современного программного обеспечения. Модель объектов, заложенная в данную парадигму, способна достаточно точно описывать свойства и возможности сущностей реального мира. Разумеется, эти объекты не существуют обособленно друг от друга, они взаимодействуют друг с другом для достижения какой-то глобальной цели разрабатываемой системы.

Стандартная библиотека некоторого языка программирования – замечательный сборник полезных утилит. Однако разнообразие решаемых программистами задач так велико, что одной только стандартной библиотекой ограничиться не получится. Программисту часто приходится самому создавать необходимый ему набор функциональности. Это можно сделать, создав пакет функций или набор классов.

Создание собственных классов при разработке программы добавляет в проект новый уровень абстракции, который позволяет определить некоторый функционал системы и работать в дальнейшем только с ним.

Чем выше уровень абстракции, которым пользуется программист, тем выше уровень его продуктивности при разработке приложения.
Чем выше уровень абстракции, которым пользуется программист, тем выше уровень его продуктивности при разработке приложения.

«Хорошая абстракция превращает практически неподъемную задачу в две, решить которые вполне по силам. Первая из этих задач состоит в определении и реализации абстракции, а вторая - в использовании этих абстракций для решения текущей проблемы.»

Э.С. Таненбаум

Использование ООП может существенно упросить жизнь программисту. Это достигается за счёт сокрытия особенностей внутренней реализации классов. Программисту остаётся лишь пользоваться её удобствами. Кажется, что ООП – панацея от всех проблем. Однако на практике, если не иметь чёткого представления о том, какие классы нужно реализовать и как ими потом пользоваться, в результате может получиться очень запутанная система, которая начнёт порождать спагетти-коду (от англ. “spaghetti code”), который будет лишь мешаться, когда вы захотите добавить что-то новое в систему.

Чтобы избежать большинства проблем, возникающих при использовании ООП, нужно:

  1. Иметь некоторый опыт создания программ и использования классов.

  2. Строить структурные диаграммы классов.

Первое придёт со временем, а со вторым я могу вас познакомить прямо сейчас. Сегодня мы разберём диаграмму классов UML.


Содержание

  1. Назначение диаграммы классов

  2. Постановка задачи и её анализ

  3. Класс

    1. Статический класс

    2. Абстрактный класс

  4. Поля класса

    1. Уровень видимости

    2. Идентификатор

    3. Тип поля

    4. Кратность

  5. Методы класса

  6. Классы, отвечающие за графику

  7. Виды отношений

    1. Отношение ассоциации

    2. Отношение зависимости

    3. Отношение наследования

    4. Отношение агрегации

    5. Отношение композиции

Назначение диаграммы классов

Диаграмма классов (от англ. "class diagram") предназначена для представления внутренней структуры программы в виде классов и связей между ними. 

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

Взаимосвязь диаграммы классов с другими диаграммами

Диаграмма классов UML тесно связана с другими диаграммами, поскольку в них используются экземпляры классов (объекты), описанные на диаграмме классов. Например, на диаграмме кооперации (англ. "cooperation diagram") показывается структурные связи при взаимодействии объектов, а на диаграмме последовательности (англ. "sequence diagram") изображается последовательность обмена сообщений между объектами.

Постановка задачи и её анализ

Представьте, что мы хотим разработать программу для построения графиков функций.  Давайте кратко изобразим основной функционал такой программы на диаграмме вариантов использования UML:

Зачем нужен вариант использования "построить график функции"

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

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

  1. Построить диаграмму для классов, ответственных за работу с пользовательскими математическими функциями.

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

Зачем плодить множество диаграмм, когда можно сделать одну большую?

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

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

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

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

Давайте определимся с тем, как мы собираемся работать с функциями. Во-первых, договоримся, что мы будем работать не напрямую с функциями, а только с их телами.

Именно поэтому в этой статье мы будем оперировать термином математическое выражение, а не термином функция.

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

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

Для того чтобы мы могли работать с телом функции, записанным в виде строки, необходимо разбить эту строку на элементарные части. Их чаще всего называют лексемами (от англ. "lexeme") или токенами (от англ. "token"). В данной статье мы будем использовать термин токен. Получить список токенов математического выражения можно, используя алгоритм сортировочной станции.

Затем из полученного списка токенов мы будем строить постфиксную запись выражения.

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

Для удобства работы с нашим приложением нужно добавить возможность определения и использования именованных констант. Например, использование распространённых математических констант, таких как π = 3,141592.. или e = 2,71828.. будет очень удобным для пользователей.

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

Соглашение об именовании объектов и классов

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

  1. Когда вы начнёте реализовывать классы, описанные на диаграмме, в коде, вы будете давать им название на английском языке. Разные программисты по-разному могут перевести одно и то же слово. Например, класс "МатематическоеВыражение" некоторые могут перевести как "MathExpression", другие, исходя из специфика задачи, как "Function" или просто как "Expression". Таким образом, когда вы начнёте сопоставлять написанные классы с элементами на диаграмме, вы можете запутаться.

  2. Технический английский язык обычно более ёмок, чем русский. Это позволит сократить объём текста на диаграмме.

Давайте подведём итог и выясним, какие классы нам будут нужны для решения поставленной задачи:

  • Класс для хранения математического выражения. Назовём его MathExpression.

  • Класс для разбиения строкового представления выражения на список токенов - MathParser.

  • Класс для построения постфиксной формы выражения из списка токенов - MathFormConverter.

  • Класс для работы с именованными константами - MathConstantManager.

  • Класс для проверки корректности пользовательского математического выражения - MathChecker.

  • Класс для подсчёта таблицы значений математического выражения - MathCalculator.

Почему все имена классов имеют префикс Math?*

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

Более правильным решением было бы вынести все эти классы в пространство имён Math и убрать префикс из имён. Однако продолжим работать с нашим Legacy-кодом.

Класс

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

  1. Имя класса

  2. Список полей класса

  3. Список методов класса

Выбор терминологии

В различной технической литературе вы можете встретить альтернативные названия для этих терминов:

  1. Поля (от англ. “field”) <=> свойства (от англ. “properties”), атрибуты (от англ. “attributes”)

  2. Методы (от англ. “method”) <=> функции (от англ. “functions”), операции (от англ. “operations”)

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

Обязательным элементом класса является только его название.
Обязательным элементом класса является только его название.

Оранжевым цветом мы будем выделять обязательные части элементов.

Пример класса "Покупатель". У покупателя есть баланс (balance) денег и список желаемого (wishList). Пользователь может пополнять баланс на некоторую сумму денег (topUpBalance()), может совершать покупки (makePurchase()) и может добавлять товары в список желаемого (appendToWishList()). Также мы можем проверить, подтверждена ли электронная почта пользователя.
Пример класса "Покупатель". У покупателя есть баланс (balance) денег и список желаемого (wishList). Пользователь может пополнять баланс на некоторую сумму денег (topUpBalance()), может совершать покупки (makePurchase()) и может добавлять товары в список желаемого (appendToWishList()). Также мы можем проверить, подтверждена ли электронная почта пользователя.

Обычно в качестве имени класса выбирается существительное в единственном числе. Разумеется, это имя должно быть уникальным в пределах диаграммы. Если имя класса состоит из нескольких слов, мы ,по практическим соображениям, будем записывать их слитно в верблюжьем стиле (от англ. "CamelCase").

Рекомендация по именованию классов

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

Настало время изобразить наши классы на диаграмме. Пока что давайте изобразим только имена этих классов.

Первая версия диаграмма классов, которые отвечают за работу
с математическими выражениями.
Первая версия диаграмма классов, которые отвечают за работу с математическими выражениями.

Пока что эта диаграмма не даёт никакого понимания того, как будет устроена наша система, однако к концу статьи диаграмма значительно преобразится.

Статический класс

Класс, в котором есть только статические поля и методы и на основе которого не создаются объекты,  называется статическим классом. Чтобы показать на диаграмме, что наш класс статический, нужно добавить к имени модификатор «utility».

Формально, такие модификаторы называется стереотипами. Стереотип – именованный набор свойств. В данном случае, стереотип «utility» означает, что объекты указанного класса не создаются.

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

В нашей системе классы MathParser, MathFormConverter, MathConstantManager являются статическими, потому что они представляют собой «сборник» полезных функций, которые мы объединили в класс. Давайте изобразим это на нашей диаграмме.

2 версия диаграммы
2 версия диаграммы

Абстрактный класс

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

В UML принято соглашение, согласно которому все элементы, относящиеся к абстрактному классу, должны быть помечены курсивом (жирный шрифт при этом сохраняется).

Поля класса

Вернёмся к нашему примеру с классом Customer. Обратите внимание на центральную секцию.

Давайте рассмотрим первую строчку. Что вообще означает запись "- balance: Integer"? Сейчас будем разбираться.

В общем случае, каждое поле класса должно описываться следующим образом:

Общий вид поля класса
Общий вид поля класса

Нами в данной статье не будет рассмотрено использование свойств в полях класса, поскольку это достаточно специфичная тема.

Уровень видимости

Уровень видимости (от англ. "visibility") - свойство поля, которое показывает, из какой части программы можно обратиться к данному полю.

В нашем случае, поле balance - закрытое
В нашем случае, поле balance - закрытое

Обычно может принимать следующие значения:

  • "+" - открытое поле. Аналог public в языках программирования. Означает, что к полю можно обратиться из любой части программы.

  • "-" - закрытое поле. Аналог private в языках программирования. Означает, что получить доступ к полю можно только внутри класса.

  • "#" - защищённое поле. Аналог protected в языках программирования. Означает, что получить доступ к полю можно внутри класса и внутри производных классов.

Может показаться, что как-то неудобно для каждого поля указывать его уровень видимости. Почему бы не группировать поля по уровню видимости? Например, именно такой подход используется в языке программирования C++. Давайте попробуем напрямую использовать ключевые слова public, private и protected.

Несмотря на удобство такого подхода, мы будем указывать уровень видимости для каждого элемента.

А что будет, если не указывать уровень видимости вовсе?*

В некоторых языках программирования, к примеру, в C++ или C# отсутствие уровня видимости поля или метода по умолчанию означает, что такой элемент считается закрытым. Тем не менее, обычно уровень видимости указывается для каждого поля, чтобы код становился более читабельным.

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

Идентификатор

Идентификатор (от англ. "identificator") - название поля. Является обязательным элементом для описания переменной на диаграмме классов, поскольку однозначно её определяет (все идентификаторы на диаграммах уникальны).

Тип поля

Тип поля (англ. "type of field") показывает, какой тип имеет данное поле в нашей программе. На ранней стадии проектирования можно и не уточнять, какой тип имеет то или иное поле.

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

Кратность

Кратность (от англ. "multiplicity") – интервал, определяющий диапазон количества элементов в массиве. Если для поля указана кратность, то его следует считать массивом. Количество элементов в таком массиве и будет определяться указанным интервалом.

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

Multiplicity is a definition of an inclusive interval of non-negative integers to specify the allowable number of instances of described element.

Кратность определяет отрезок с неотрицательными целочисленными границами, который показывает допустимое количество объектов определённого типа.

“Кратность, если она присутствует, определяет данный атрибут как массив (определенной или неопределенной длины).”

Учебно-методическое пособие по дисциплине «Анализ и проектирование на UML». ИТМО.

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

Для кратности указывают одно или два значения:

  • [m..n] - интервал от m до n включительно (m <= n). Такая запись будет означать, что в коллекции может храниться от m до n значений включительно.

  • [n] – интервал, который можно рассматривать, как сокращённую запись [0..n].

Может случиться так, что мы захотим показать, что в массиве может храниться неограниченное количество элементов. В таком случае верхняя граница n заменяется символом *.

Примеры интервалов:

  • [1] - ровно один объект. То же самое, что и интервал [1..1]

  • [0..1] - ноль или один объект.

  • [0..*] - ноль или неограниченное количество объектов. Часто такой интервал обозначают просто как [*].

  • [1..*] - один или неограниченное количество объектов.

Наиболее часто используют кратность [0..*] или [1..*]. Можно заметить, что динамические структуры данных вообще очень удобны в использовании. В нашей статье мы откажемся от использования кратности, а будем использовать такие коллекции: связный список (List) и ассоциативный массив (Map).

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

3 версия диаграммы
3 версия диаграммы

Чтобы отличать статические элементы класса от обычных, статические поля и методы будут подчёркиваться.

Назначение каждого поля построенных классов

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

Класс MathExpression:

  • initial – строковое математическое выражение, записанное в инфиксной форме.

  • postfix – строковое математическое выражение, записанное в постфиксной форме.

  • parameters – параметры в математическом выражении. Всего параметров четыре: a, b, c, d. У каждого параметра могут быть произвольные действительные значения. Значения параметров хранятся в ассоциативном массиве.

Класс MathParser:

  • delimiters - список разделителей. С помощью этих разделителей удаётся простым образом разбить входное выражение на список токенов.

Класс MathFormConverter:

  • precedence - ассоциированный массив, хранящий приоритет каждой операции/функции. Это необходимо для перевода выражения из инфиксной формы в постфиксную.

Класс MathConstantManager:

  • userDefinedConstants - ассоциированный массив, хранящий значения определённых пользователем констант.

  • predefinedConstants - ассоциированный массив, хранящий значения предопределённых констант.

Класс MathChecker:

  • userExpression - строковое представление пользовательского математического выражения, которое будет проверяться.

  • errorPlace - участок строки, в котором содержится ошибка (например, неизвестный системе токен).

  • errorType - тип ошибки.

  • operations - список корректных операций.

  • functions - список корректных функций.

  • operandQuantity - ассоциированный массив, хранящий количество аргументов для каждой операции/функции.

Класс MathCalculator:

  • expression - объект математического выражения, значения которого будут подсчитываться.

Аналогичное примечание будет сделано и для методов классов.

Методы класса

Снова разберём наш пример с классом Customer. На этот раз обратим внимание на третью секцию - секцию методов.

Описание методов очень похоже на описание полей класса. На рисунке ниже представлен общий вид описания метода класса.

Аргументы методов в общем случае описываются следующим образом:

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

Теперь давайте добавим на нашу основную диаграмму  методы классов.

Назначение каждого метода построенных классов

Класс MathExpression:

  • setParameter(parameter, value) - устанавливает параметру parameter значение value.

  • setExpression(expression) - устанавливает новое тело функции математического выражения.

  • getStringRepresentation() - возвращает тело функции.

Класс MathParser:

  • CreateTokenList(expression) - разбивает математическое выражение на список токенов и возвращает его в виде списка строк.

Класс MathFormConverter:

  • InfixToPostfix(infixExpression) - переводит математическое выражение в постфиксную форму.

Класс MathConstantManager:

  • addConstant(constant, value) - добавляет в систему новую пользовательскую константу.

  • alterConstant(constant, newValue) - изменяет значение пользовательской константы constant на newValue.

  • deleteConstant(constant) - удаляет пользовательскую константу constant.

  • getConstantValue(constant) - возвращает значение пользовательской константы constant.

  • isConstant(token) - проверяет, является ли token константой.

Класс MathChecker:

  • areAllTokensCorrect() - проверяет все токены математического выражения на корректность.

  • areBracketsCorrespond() - проверяет все ли скобки расставлены правильно.

  • hasEmptyBrackets() - проверяет, есть ли в выражении пустые скобки

  • hasMissedOperations() - проверяет, есть ли в выражении пропущенные операции. Например, для отслеживания случаев "18 354" - между числами пропущена операция.

  • IsOperation(token) - проверяет, является ли токен корректной операцией.

Класс MathCalculator:

  • calculate(value) - подсчитывает значение математического выражения, когда значение переменной равно value

Классы, отвечающие за графику

Помните, мы в начале статьи договорились, что у нас будет две диаграммы: одна для классов, отвечающих за работу с функциями, другая – для классов, отвечающих за графику? Настало время построить вторую диаграмму. Результат представлен на картинке ниже.

Слишком много непонятных классов

Давайте вместе разбираться в этой куче классов:

  • QWidget – стандартный класс фреймфорка Qt, который является базовым почти для всех создаваемых виджетов (графических элементов). В нашем проекте все элементы созданы на основе этого класса.

  • QDialog – стандартный класс для создания диалоговых окон.

  • AboutProgramDialog – окно информации о программе. Такое окно есть почти в каждой программе. В нём находится краткое описание проекта, его авторы и, возможно, информация о лицензировании.

  • HelpDialog – окно справочной информации. В больших системах справочник просто необходим.

  • MainWindow – главное окно программы «Построитель графиков функций». В нём содержатся основные элементы программы: плоскость для построения графиков (PaintingArea), список блоков ввода функций (FunctionBoxList), список блоков ввода констант (ConstantBoxList).

  • PaintingArea – плоскость для построения графиков. Пользователь может захотеть нарисовать графики некоторых определённых им функций. За отображения этих графиков и отвечает данный класс.

Координатная плоскость для построения графиков. Является объектом класса PaintingArea. В качестве примера, на координатной плоскости построен график функции y=sin(x)
Координатная плоскость для построения графиков. Является объектом класса PaintingArea. В качестве примера, на координатной плоскости построен график функции y=sin(x)
  • Graph – график функции на координатной плоскости. Пример объекта класса Graph представлен на рисунке выше.

  • FunctionBox – блок ввода функции. Представлен на рисунке ниже.

  • FunctionBoxList – список блоков ввода функций.

  • ConstantBox – блок ввода константы. Представлен на рисунке ниже.

  • ConstantBoxList – список блоков ввода констант.

Виды отношений

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

  • Отношение ассоциации

  • Отношение зависимости

  • Отношение обобщения, также известное как отношение наследования.

  • Отношение агрегации

  • Отношение композиции

Обозначение каждого вида отношения представлено на рисунке ниже. Далее мы начнём рассматривать каждое отношение в подробностях.

Рассматриваемые нами виды отношений
Рассматриваемые нами виды отношений

Отношение ассоциации

Отношение ассоциации используют, чтобы показать, что между классами (например, между двумя классами) существует некоторая связь. Обычно с помощью него на диаграмме классов показывают, что один класс пользуется функционалом другого класса.

Методы класса LogSystem используют метод Console::WriteLine() и, возможно, некоторые 
другие  для вывода результатов.
Методы класса LogSystem используют метод Console::WriteLine() и, возможно, некоторые другие для вывода результатов.

В общем случае, использование отношения ассоциации выглядит следующим образом:

Как вы можете заметить, стрелка ассоциации направлена от класса пользователя к классу владельцу используемой функциональности. Для пояснения того, каким образом один класс использует другой класс, вы можете описать данный процесс в вспомогательном тексте.

Обратите внимание на кратность ассоциации, которая расположена под стрелкой. С кратностью мы уже встречались ранее. Здесь у нее несколько иное значение. Кратность ассоциации обозначает количество объектов, которые участвуют во взаимодействии. Как показано на рисунке выше, во взаимодействии могут участвовать от m до n пользователей и от q до r владельцев.

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

Если кратность ассоциации не указана, будет подразумеваться кратность [0..*]. В случае со статическими классами кратность не указывается (можно считать, что там указана кратность [1].

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

Обозначение отношения ассоциации

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

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

Давайте добавим отношения ассоциации на наши диаграммы.

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

Отношение зависимости

Отношение зависимости используют, чтобы показать, что изменение одного класса требует изменение другого класса.

В нашем случае вид графика функции зависит от самой функции или, по нашей договорённости об именовании, от объекта математического выражения. Давайте покажем это на диаграмме.

Изменение объекта математического выражения влияет на вид графика.
Изменение объекта математического выражения влияет на вид графика.

Стрелка отношения зависимости направлена от зависимого класса к независимому.

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

Отношение между этими двумя классами как бы соединяет две диаграммы воедино. Все классы нашей первой диаграммы «работают» на объекты класса MathExpression. Людям, работающим с графической частью приложения, нужно знать лишь об этом классе, что существенно снижает сложность. В результате наша вторая диаграмма приобретает следующий вид.

Отношение наследования

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

Итак, отношение наследования используется, чтобы показать, что один класс является родителем (базовым классом или суперклассом) для другого класса (потомка, производного класса).

Если вы работали с какой-нибудь библиотекой для создания графического интерфейса (OpenGL в чистом виде не в счёт!), вы могли заметить, что все классы графических элементов обычно выстраиваются в цепочку наследования.  Например, взгляните на цепочку наследования классов фреймворка Qt5, представленную на рисунке ниже.

Отношение наследования здесь изображается обычной стрелкой.
Отношение наследования здесь изображается обычной стрелкой.
Полная версия диаграммы

Полную версию диаграммы вы можете посмотреть по ссылке

Разумеется, такие цепочки наследования просто необходимы, поскольку различные виджеты (графические элементы) имеют схожие свойства и поведение. В нашем приложении классы виджетов тоже были созданы на основе нескольких базовых классов: QWidget и QDialog. Давайте покажем это на диаграмме.

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

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

Отношение агрегации

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

В переводе с английского, слово aggregation означает соединение частей. Это значение очень точно отражает суть данного отношения – показать, из каких частей состоит класс.

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

class A
{
	class B
	{
	};
…
}; 

На самом деле, это отношение означает, что объект одного класса включает в себя в качестве составной части объект другого класса.

Объект класса PersonalComputer (упрощённо) состоит из объекта класса Monitor, объекта класса ComputerMouse и объекта класса Keyboard.
Объект класса PersonalComputer (упрощённо) состоит из объекта класса Monitor, объекта класса ComputerMouse и объекта класса Keyboard.

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

На нашей диаграмме есть много мест, где нам может пригодиться отношение агрегации:

  • Объект класса MainWindow содержит в себе по одному объекту классов PaintingArea, ConstantBoxList, FunctionBoxList.

  • Неограниченное количество объектов класса Graph могут содержаться в объекте класса PaintingArea.

  • Класс-контейнер ConstantBoxList может содержать в себе неограниченное количество объектов класса ConstantBox.

  • Класс-контейнер FunctionBoxList может содержать в себе неограниченное количество объектов класса FunctionBox.

Отношение композиции

Отношение композиции является частным случаем отношения агрегации. Однако у него есть одно отличие – классы-части, которые он соединяет с классом-целым, не могут существовать обособленно.

Одним из переводов слова composition является слово смесь. Если допустить, что из смеси не получится получить исходные компоненты, то это хорошо помогает понять, что части, соединённые отношением композиции не могут существовать сами по себе.

Давайте в качестве примера рассмотрим окно интерпретатора Python.

Понятное дело, что ни полоса прокрутки (ScrollBar), ни заголовок окна (Title), ни поле ввода команд (TextInput) не могут существовать отдельно от окна программы (Window). Это можно изобразить на диаграмме классов следующим образом.

В нашей диаграмме объекты классов FunctionBox и ConstantBox не могут существовать отдельно от их контейнеров. Кроме того, объекты класса Graph тоже не могут существовать обособленно от координатной плоскости.

Вот и всё! Мы рассмотрели достаточно элементов диаграммы классов, чтобы начать делать собственные диаграммы классов.

P.S. Спасибо всем, кто дошёл до этого момента. Статья получилась очень массивной, поскольку хотелось разобрать все основные элементы на практическом примере.

Теги:
Хабы:
+5
Комментарии3

Публикации

Истории

Работа

Ближайшие события