Наследование, полиморфизм и отношения между классами в ООП

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

Использование наследования и полиморфизма в ООП

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

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

Наследования как способ задания интерфейса. Механизм абстрактных классов и полиморфизма в ООП позволяет решать многие задачи в терминах базовых классов, не заботясь о том, какие конкретно классы участвуют в той или иной операции. Представим себе класс фигур, которые можно изображать на экране терминала  в графическом редакторе. Из этого класса выделены конкретные классы квадрата, эллипса, треугольника, трехмерного шара и т.д. Базовый класс задает действия, которые можно производить с фигурами: сдвинуть, повернуть, масштабировать, закрасить и т.д. Большинство действий графического редактора можно запрограммировать в терминах действий на базовым классом (Например, при нажатии определенной клавиши сдвинуть фигуру к левому краю страницы). Полиморфизм в ООП обеспечит правильное выполнение конкретных действий, а добавление новых типов фигур путем введения нового конкретного класса из класса «Фигура» не нарушит алгоритмов работы редактора.

Приведем фрагмент кода программы на языке C++, который иллюстрирует использование базового класса «Фигура» для задания интерфейса. Ключевое слово class отмечает начало определения класса. В части, отмеченной меткой public, задается внешняя часть класса или его интерфейс. Ключевое слово virtual перед методом определяет то, что этот метод полиморфен (виртуален в терминах C++), т.е. может быть переопределен в порожденных классах. Все методы возвращают логический результат (bool), сообщающий об успешном или неуспешном завершении операции.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Базовый класс Фигура
// для него заданы методы, реализация которых
// отложена для выведенных классов
class Shape
{
public:
// Внешняя часть класса
    virtual bool Draw() = 0; // Перерисовать фигуру
    virtual bool Move(double _x, double _y) = 0; // Сдвинуть фигуру
    virtual bool Zoom(double scale) = 0; // Масштабировать
protected:
    // Защищенная часть класса, доступная только
    // выведенным из него классам
    double coordX; // Атрибут — координата X
    double coordY; // Атрибут — координата Y
};

// Класс Круг выведенный из класса Фигура
class Circle : public Shape
{
public:
    virtual bool Draw() {…}; // Реализация перерисовки
    virtual bool Move(double _x, double _y) {…}; // Реализация сдвига
    virtual bool Zoom(double scale) {…}; // Реализация операции масштабирования
private:
    // Внутренняя часть доступная только самому классу
    double radius; // Атрибут — длина радиуса
};

// Класс Квадрат выведенный из класса Фигура
class Square : public Shape
{
public:
    virtual bool Draw() {…}; // Реализация перерисовки
    virtual bool Move(double _x, double _y) {…}; // Реализация сдвига
    virtual bool Zoom(double scale) {…}; // Реализация операции масштабирования
private:
    // Внутренняя часть доступная только самому классу
    double side; // Атрибут — длина стороны квадрата
};

// Функция вызываемая в ответ на нажатие кнопки
// перерисовать весь экран
void click_ButtonRedraw()
{
    // Итерация по всем имеющимся фигурам
    while (Iterator it = listShapes.begin();
            it++;
            it != listShapes.end() ) {
                  // Используя указатель на базовый класс
                  // вызвать метод перерисовки конкретной фигуры

                  Shape* shapePtr = it.GetShape();
                  shapePtr->Draw();
    }
}

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

Включение

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Carburetor    // Класс карбюраторов
{…}

class Engine    // Класс моторов
{
private:
    Carburetor engineCarburetor;  // Атрибут мотора — карбюратор
};

class Cars    // Класс автомобилей
{
private:
    Engine carEngine;  // Атрибут машина — мотор
};

Ключевое слово private означает тот факт, что атрибуты включены во внутреннюю часть класса, т.е. непосредственный доступ к ним извне класса (из объектов других классов) запрещен.

Отношение включение может использовать ссылку на другой класс, т.е. использовать вместо объекта указатель ссылку на объект в языке C++:Engine* carEngine;

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

Ассоциация

Отношение ассоциации устанавливает двустороннюю связь между объектами разных классов. Предположим, что существует класс, описывающий преподавателей в университете, и класс, описывающий студентов. Студент может выбрать руководителя своего курсового проекта среди преподавателей. Между студентами и преподавателями устанавливается ассоциация «руководит — имеет руководителя курсового проекта», иными словами, преподаватель может руководить несколькими студентами а у студента есть руководитель. Если преподаватель отказывается от руководства каким-либо студентом (например, из-за хронического невыполнения заданий), он разрывает ассоциацию. При этом происходит следующее. Во-первых, данный студент удаляется из списка студентов, которыми руководит преподаватель (изменяется атрибут объекта преподаватель), а во-вторых, студент лишается преподавателя (изменяется атрибут объекта студент). Ситуация пояснена на рисунке:

Ассоциация "Руководитель - руководит" преподавателя и студентов

Ассоциация "Руководитель — руководит" преподавателя и студентов

Ассоциация — двусторонняя ссылка, при каждом изменении ассоциации изменяются атрибуты обоих объектов. Говорят о множественности ассоциации, т.е. о количестве объектов, участвующих в ней. В примере преподаватели-студенты множественность ассоциации 1-n (один преподаватель может руководить несколькими студентами, у студента может быть только один руководитель). Легко представить себе ассоциации с множественностью 1-1, n-m и т.д.

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

Отношение наследования между классами было рассмотрено в статье Классы в ООП. Иногда об отношении наследования говорят как об отношении «является». Объект класса-наследника «является» объектом наследуемого класса, поскольку он обладает всеми атрибутами своего родителя и всеми методами. Однако поведение объекта может быт изменено путем использования полиморфизма.

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

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

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

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

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

Использование

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

Вызов методов другого класса также является отношением использования. Фактически при этом отношении один класс «знает» о существовании второго и «знает» по крайней мере его интерфейс.

Метаклассы и метаданные в ООП

Сказав, что классы в ООП создают шаблон для создания объектов, не было сказано, как же именно новые объекты создаются.

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

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

В языке C++ понятие метаклассов и метаобъектов отсутствует, однако фактически конструкторы классов и статические элементы классов относятся к функциональности метаклассов. Метаклассы реализованы в таких языках программирования, как Smalltalk, CLOS и других. Имея метаклассы, можно динамически, т.е в ходе выполнения программ, создавать новые классы, изменять существующие, добавляя или модифицируя элементы классов.

Метаданные — это данные о данных.  Обычно этот термин в языках программирования применяют к даным, которые описывают структуру программных данных, в случае объектно-ориентированного языка программирования — структуру классов и объектов. Наличие метаданных позволяет динамически опрашивать объект, какие у него есть методы и атрибуты. Если объект во время работы системы может сообщить, к какому классу он принадлежит, эта информация используется другими частями системы для определения того, какие операции данный объект может выполнить. На опросе классов, во время выполнения программы, в частности, основан механизм динамического вызова интерфейсов в объектной модели CORBA, используемой для взаимодействия распределенных объектов и систем.

Объект в ООП (Объектно-ориентированном программировании)
Объектная модель в ООП
ITandLife.ru