Построение диаграммы классов при проектировании ПО

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

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

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

уровень абстракции

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

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

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

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

  • Иметь некоторый опыт создания программ и использования классов;
  • Строить структурные диаграммы классов;

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

Оглавление:

  1. Назначение диаграммы классов
  2. Постановка задачи и её анализ
  3. Класс
  1. Поля класса
  1. Методы класса
  2. Виды отношений

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

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

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

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

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

диаграмма вариантов

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

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

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

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

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

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

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

математическое выражение

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

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

восприятие компьютером информации

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

упрощенная схема разработки

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

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

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

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

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

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

  • Класс для хранения математического выражения. Назовём его MathExpression.
  • Класс для разбиения строкового представления выражения на список токенов - MathParser.
  • Класс для построения постфиксной формы выражения из списка токенов - MathFormConverter
  • Класс для работы с именованными константами - MathConstantManager.
  • Класс для проверки корректности пользовательского математического выражения - MathChecker.
  • Класс для подсчёта таблицы значений математического выражения - MathCalculator.

Класс

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

  1. Имя класса;
  2. Список полей класса;
  3. Список методов класса;

диаграмма класса

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

вид блока диаграммы

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

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

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

диаграмма математических выражений

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

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

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

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

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

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

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

схема абстрактного класса

Поля класса

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

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

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

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

уровень видимости поля

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

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

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

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

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

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

идентификатор поля

Тип поля

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

тип поля на диаграмме

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

Кратность

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

кратность

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

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

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

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

схема объектов

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

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

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

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

описание метода класса

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

основная UML диаграмма

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

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

  • Отношение ассоциации;
  • Отношение зависимости;
  • Отношение обобщения, также известное как отношение наследования;
  • Отношение агрегации;
  • Отношение композиции;

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

виды ассоциаций в проектировании

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

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

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

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

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

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

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

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

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

диаграмма отношения зависимости

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

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

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

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

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

диаграмма отношения наследования

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

цепочка наследования классов Qt5

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

диаграмма виджетов

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

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

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

диаграмма отношения агрегации

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

составные части объекта другого класса

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

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

  • Объект класса MainWindow содержит в себе по одному объекту классов PaintingArea, ConstantBoxList, FunctionBoxList;
  • Неограниченное количество объектов класса Graph могут содержаться в объекте класса PaintingArea;
  • Класс-контейнер ConstantBoxList может содержать в себе неограниченное количество объектов класса ConstantBox;
  • Класс-контейнер FunctionBoxList может содержать в себе неограниченное количество объектов класса FunctionBox;

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

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

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

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

диаграмма классов FunctionBox и ConstantBox

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

Ссылка на источник