Как построить иерархию классов
Перейти к содержимому

Как построить иерархию классов

Иерархия классов

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

Наследование применяется для следующих взаимосвязанных целей:

  1. исключения из программы повторяющихся фрагментов кода;
  2. упрощения модификации программы;
  3. упрощения создания новых программ на основе существующих.

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

Кроме механизма наследования в данном разделе мы рассмотрим такие важные понятия ООП как полиморфизм и инкапсуляцию (см. «Технология объектно-ориентированного программирования» ), которые также принимают участие в формировании иерархии классов.

Наследование

Класс в С# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object. Синтаксис наследования:

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

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

Рассмотрим наследование классов на примере геометрических фигур на плоскости. В качестве базового класса создадим класс DemoPoint (точка на плоскости), в качестве производного класса от DemoPoint класс DemoLine (отрезок на плоскости):

Экземпляр класса DemoLine с одинаковой легкостью использует как собственные поля и методы, так и унаследованные от класса DemoPoint . При этом, если метод производного класса называется также как и метод базового класса, то вызывается метод производного. Однако компилятором будет сгенерировано предупреждение:

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

Использование защищенного доступа

В нашем примере поля x и у базового класса были открыты для доступа ( public ). Если убрать public , то поля автоматически станут закрытыми для доступа ( private ), в том числе и для доступа из производного класса. Решить проблему доступа к закрытым полям базового класса из производного можно двумя способами: используя свойства класса или спецификатор protected . При объявлении какого-то члена класса с помощью спецификатора protected, он становится закрытым для всех классов, кроме производных.

Обратите внимание на то, что доступ к полям х и y из класса Program невозможен, а из производного класса DemoLine возможен.

Наследование конструкторов

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

В предыдущем примере классы создавались за счет автоматического вызова средствами С# конструктора по умолчанию. Добавим конструктор только в производный класс DemoLine :

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

Если же конструкторы определены и в базовом, и в производном классе, то процесс создания объектов несколько усложняется, т.к. должны выполниться конструкторы обоих классов. В этом случае используется ключевое слово base , которое имеет два назначения:

  1. позволяет вызвать конструктор базового класса:

Производный класс может вызывать конструктор, определенный в его базовом классе, используя расширенную форму объявления конструктора и ключевое слово base. Формат расширенного объявления:

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

В этом случае ключевое слово base действует подобно ссылке this , за исключением того, что ссылка base всегда указывает на базовый класс для производного класса, в котором она используется. В этом случае формат ее записи выглядит следующим образом:

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

Несмотря на то, что метод Show в классе DemoLine скрывает одноименный метод в классе DemoPoint , ссылка base позволяет получить доступ к методу Show в базовом классе. Аналогично с помощью ссылки base можно получить доступ к одноименным полям базового класса.

Наследование. Управлять большим количеством разрозненных классов довольно сложно

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

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

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

Итак, наследование применяется для следующих взаимосвязанных целей:

— исключения из программы повторяющихся фрагментов кода;

— упрощения модификации программы;

— упрощения создания новых программ на основе существующих.

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

Класс в С# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object.

[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ]

Листинг 8.1 . Класс Student, потомок класса Person

public string name;

public string profession;

public Person(string name)

public Person(string name, int age)

public Person(string name, string profession)

public Person(string name, int age, string profession)

class Student : Person

public Student(string name, int age, string profession, string number_group)

public string GetInformation()

information = "Имя: " + this.name + "; Возраст: " + this.age.ToString() + "; Профессия: " + this.profession + "; № группы: " + this.number_group;

В классе Student введено поле number_group, определен собствен­ный конструктор. Все поля и свойства класса Person наследуются в классе Student.

Результат работы программы:

Имя: Миша; Возраст: 23; Профессия: Студент; № группы: 08-к-ПИ1

Имя: Иван; Возраст: 22; Профессия: Менеджер; № группы: нет

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

  • Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового класса без параметров.
  • Для иерархии, состоящей из нескольких уровней, конструкторы базовых клас­сов вызываются, начиная с самого верхнего уровня. После этого выполняются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса. Таким об­разом, каждый конструктор инициализирует свою часть объекта.
  • Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке ини­циализации (это продемонстрировано в конструкторах, вызываемых в опера­торах 1 и 2). Вызов выполняется с помощью ключевого слова base. Вызывает­ся та версия конструктора, список параметров которой соответствует списку аргументов, указанных после слова base.

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

Элементы базового класса, определенные как public, в производном классе доступны.

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

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

  • объект, в который во время выполнения программы заносятся ссылки на объ­екты разных классов иерархии;
  • контейнер, в котором хранятся объекты разных классов, относящиеся к од­ной иерархии;
  • метод, в который могут передаваться объекты разных классов иерархии;
  • метод, из которого в зависимости от типа вызвавшего его объекта вызывают­ся соответствующие методы.

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

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

UML-диаграммы классов

UML – унифицированный язык моделирования (Unified Modeling Language) – это система обозначений, которую можно применять для объектно-ориентированного анализа и проектирования.
Его можно использовать для визуализации, спецификации, конструирования и документирования программных систем.
Словарь UML включает три вида строительных блоков:

  • Диаграммы.
  • Сущности.
  • Связи.

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

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

Сущности

Диаграммы классов оперируют тремя видами сущностей UML:

  • Структурные.
  • Поведенческие.
  • Аннотирующие.

Структурные сущности – это «имена существительные» в модели UML. В основном, статические части модели, представляющие либо концептуальные, либо физические элементы. Основным видом структурной сущности в диаграммах классов является класс .

Поведенческие сущности – динамические части моделей UML. Это «глаголы» моделей, представляющие поведение модели во времени и пространстве. Основной из них является взаимодействие – поведение, которое заключается в обмене сообщениями между наборами объектов или ролей в определенном контексте для достижения некоторой цели. Сообщение изображается в виде линии со стрелкой, почти всегда сопровождаемой именем операции.

Поведенческие сущности
Аннотирующие сущности – это поясняющие части UML-моделей, иными словами, комментарии, которые можно применить для описания, выделения и пояснения любого элемента модели. Главная из аннотирующих сущностей – примечание . Это символ, служащий для описания ограничений и комментариев, относящихся к элементу либо набору элементов. Графически представлен прямоугольником с загнутым углом; внутри помещается текстовый или графический комментарий.

Аннотирующие сущности

Структурные сущности — классы

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

Графически класс изображается в виде прямоугольника, разделенного на 3 блока горизонтальными линиями:

  • имя класса
  • атрибуты (свойства) класса
  • операции (методы) класса.

Для атрибутов и операций может быть указан один из трех типов видимости:

  • — private (частный)
  • # — protected (защищенный)
  • + — public (общий)

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

Каждый класс должен обладать именем, отличающим его от других классов. Имя – это текстовая строка. Имя класса может состоять из любого числа букв, цифр и знаков препинания (за исключением двоеточия и точки) и может записываться в несколько строк.
На практике обычно используются краткие имена классов, взятые из словаря моделируемой системы. Каждое слово в имени класса традиционно пишут с заглавной буквы (верблюжья конвенция), например Sensor (Датчик) или TemperatureSensor (ДатчикТемпературы).
Обозначение класса
Для абстрактного класса имя класса записывается курсивом.

Атрибут (свойство) – это именованное свойство класса, описывающее диапазон значений, которые может принимать экземпляр атрибута. Класс может иметь любое число атрибутов или не иметь ни одного. В последнем случае блок атрибутов оставляют пустым.
Атрибут представляет некоторое свойство моделируемой сущности, которым обладают все объекты данного класса. Имя атрибута, как и имя класса, может представлять собой текст. На практике для именования атрибута используются одно или несколько коротких существительных, выражающих некое свойство класса, к которому относится атрибут.

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

Статические атрибуты класса обозначаются подчеркиванием.

Операция (метод) – это реализация метода класса. Класс может иметь любое число операций либо не иметь ни одной. Часто вызов операции объекта изменяет его атрибуты.
Графически операции представлены в нижнем блоке описания класса.
Допускается указание только имен операций. Имя операции, как и имя класса, должно представлять собой текст. На практике для именования операции используются короткие глагольные конструкции, описывающие некое поведение класса, которому принадлежит операция. Обычно каждое слово в имени операции пишется с заглавной буквы, за исключением первого, например move (переместить) или isEmpty (проверка на пустоту).
Можно специфицировать операцию, устанавливая ее сигнатуру, включающую имя, тип и значение по умолчанию всех параметров, а применительно к функциям – тип возвращаемого значения.

Абстрактные методы класса обозначаются курсивным шрифтом.
Статические методы класса обозначаются подчеркиванием.

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

Отношения между классами

Существует четыре типа связей в UML:

  • Зависимость
  • Ассоциация
  • Обобщение
  • Реализация

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

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

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

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

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

Множественность ассоциации представляет собой диапазон целых чисел, указывающий возможное количество связанных объектов. Он записывается в виде выражения с минимальным и максимальным значением; для их разделения используются две точки. Устанавливая множественность дальнего конца ассоциации, вы указываете, сколько объектов может существовать на дальнем конце ассоциации для каждого объекта класса, находящегося на ближнем ее конце. Количество объектов должно находиться в пределах заданного диапазона. Множественность может быть определена как единица 1 , ноль или один 0..1 , любое значение 0..* или * , один или несколько 1..* . Можно также задавать диапазон целых значений, например 2..5 , или устанавливать точное число, например 3 .
Множественность ассоциации
Агрегация – особая разновидность ассоциации, представляющая структурную связь целого с его частями. Как тип ассоциации, агрегация может быть именованной. Одно отношение агрегации не может включать более двух классов (контейнер и содержимое).
Агрегация встречается, когда один класс является коллекцией или контейнером других. Причём, по умолчанию агрегацией называют агрегацию по ссылке, то есть когда время существования содержащихся классов не зависит от времени существования содержащего их класса. Если контейнер будет уничтожен, то его содержимое — нет.
Графически агрегация представляется пустым ромбом на блоке класса «целое», и линией, идущей от этого ромба к классу «часть».
Агрегация
Композиция — более строгий вариант агрегации. Известна также как агрегация по значению.
Композиция – это форма агрегации с четко выраженными отношениями владения и совпадением времени жизни частей и целого. Композиция имеет жёсткую зависимость времени существования экземпляров класса контейнера и экземпляров содержащихся классов. Если контейнер будет уничтожен, то всё его содержимое будет также уничтожено.
Графически представляется как и агрегация, но с закрашенным ромбиком.
Композиция

Третья связь – обобщение – выражает специализацию или наследование , в котором специализированный элемент (потомок) строится по спецификациям обобщенного элемента (родителя). Потомок разделяет структуру и поведение родителя. Графически обобщение представлено в виде сплошной линии с пустой стрелкой, указывающей на родителя.
Обобщение
Четвертая – реализация – это семантическая связь между классами, когда один из них (поставщик) определяет соглашение, которого второй (клиент) обязан придерживаться. Это связи между интерфейсами и классами, которые реализуют эти интерфейсы. Это, своего рода, отношение «целое-часть». Поставщик, как правило, представлен абстрактным классом. В графическом исполнении связь реализации – это гибрид связей обобщения и зависимости: треугольник указывает на поставщика, а второй конец пунктирной линии – на клиента.
Реализация

Пример кода и диаграммы классов для него

Программа получает данные с датчика температуры (вводятся с консоли) — по 5 измерений для каждого из двух объектов класса TemperatureMeasure и усредняет их. Также предусмотрен класс ShowMeasure для вывода измеренных значений.

#include <iostream>
using namespace std;
class Sensor <
int value;
public :
Sensor() < value = 0; >
void setValue( int value) < this ->value += value; >
int getValue() < return value; >
>;
class MeasureCount
<
int number;
static int total;
public :
MeasureCount() < number = 0; >
void increment() < number++; total++; >
int getNumber() < return number; >
static int getTotal() < return total; >
>;
int MeasureCount::total = 0;
class ITemperatureMeasure
<
public :
virtual void setValue() = 0;
virtual double getValue() = 0;
>;
class TemperatureMeasure : public virtual ITemperatureMeasure
<
private :
Sensor *h; // агрегация
MeasureCount *measure; // композиция
public :
TemperatureMeasure(Sensor *h);
int getNumber() < return measure->getNumber(); >
double getValue() < return ( double )h->getValue() / measure->getNumber(); >
void setValue()
<
int value;
measure->increment();
cout << "t[" << measure->getNumber() << "]= " ;
cin >> value;
h->setValue(value);
>
static int getTotal() < return MeasureCount::getTotal(); >
>;
TemperatureMeasure::TemperatureMeasure(Sensor *h)
<
measure = new MeasureCount();
this ->h = h;
>
class ShowTemperature // зависимость
<
public :
static void Show(TemperatureMeasure t)
<
cout << t.getNumber() << ": " ;
cout << t.getValue() << " oC" << endl;
>
>;

int main()
<
Sensor *h1 = new Sensor();
TemperatureMeasure tc1(h1);
for ( int i=0; i<5; i++)
tc1.setValue();
ShowTemperature::Show(tc1);
cout << endl;
Sensor *h2 = new Sensor();
TemperatureMeasure tc2(h2);
for ( int i = 0; i<5; i++)
tc2.setValue();
ShowTemperature::Show(tc2);
cout << endl;
cout << "Total: " << TemperatureMeasure::getTotal() << endl;
cin.get(); cin.get();
return 0;
>

Результат выполнения
Тестирование отношений классов
UML-диаграмма классов для приведенного выше кода будет выглядеть следующим образом:
UML-диаграмма классов
На диаграмме классов основным классом является класс TemperatureMeasure , который и является измерителем температуры. В качестве измеренного значения формируется среднее арифметическое всех измерений — сумма всех измерений, деленная на их количество.
Для получения измерений и их суммирования используется класс Sensor (в качестве датчика температуры). В консольной задаче сами измерения передаются в этот класс для суммирования. Класс состоит в отношении агрегации с основным классом TemperatureMeasure : мы сначала создаем объект класса Sensor , а потом передаем его в качестве параметра конструктора классу TemperatureMeasure , чтобы использовать его в качестве части класса.
Количество измерений формируется классом MeasureCount , который содержит статическое свойство total для подсчета общего измерений, а также свойство count для подсчета количества измерителей конкретного объекта TemperatureMeasure . Класс MeasureCount находится в отношении композиции с классом TemperatureMeasure : объект MeasureCount создается непосредственно при создании объекта TemperatureMeasure (в его конструкторе).
Класс ITemperatureMeasure представляет собой интерфейс класса TemperatureMeasure и является своего рода поставщиком в отношении реализации.
Наконец, класс ShowTemperature находится в отношении зависимости с классом TemperatureMeasure , поскольку реализация единственного метода Show класса ShowTemperature зависит от структуры класса TemperatureMeasure .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *