Любой объект или сущность в природе имеет уровень абстракции. Это удобно и позволяет переходить от общего к деталям и наоборот. Например, “стул” — это конкретное описание предмета, но мы также можем говорить о нем, как о “мебели” или к примеру, как об “изделии из дерева”. Исходя из этого мы можем сказать, что стул — это также и элемент мебели, еще раз – стул
является элементом мебели. В свою очередь стулья могут делиться на офисные, кухонные, и т.д., что в свою очередь вырастает в довольно запутанную иерархию отношений. В нашем случае нам помогают с этим справиться классы и наследование, мы говорили об этом, когда рассматривали полиморфизм. Животные как общий уровень абстракции и кот с собакой, как конкретный уровень абстракции. К примеру общий уровень нам нужен как тип контейнера, в котором мы храним конкретные типы.
Как правило, когда мы работаем с иерархией классов нам нужны объекты конкретных типов, а не общих. В рассмотренном примере нам были нужны объекты конкретно собаки и кота. Но если бы мы рассматривали животных, рыб и людей, как конкретные типы формы жизни на нашей планете, то животные были бы конкретным типом, и мы бы создавали такие объекты. Более того, если мы создаем в базовом классе чисто виртуальные функции, которые обязуемся реализовать в наследниках, то физически не можем создавать объекты такого класса. Вот и получается, что базовый класс для таких наследников является
абстрактным, и служит лишь для доступа к наследующим его
конкретным классам, и мы называем его
интерфейсом.
Как пульт дистанционного управления является интерфейсом между человеком и телевизором. Пультов много разных по форме, способу ввода информации и т.д. но все они должны предоставлять для пользователя кнопки с цифрами, для ввода номеров каналов. Этими кнопками служат наши чисто виртуальные методы. В современных языках программирования (Java), есть специальное ключевое слово “Interface“, служащее специально для этих целей, что облегчает узнавание таких классов в коде. Давайте рассмотрим пример с такой иерархией.
[__Shape__] [_IDrawable_] [_IMovable_]
^ ^ ^ ^ ^
/ \ / / /
[_Triangle_] [_Circle_] / /
\_____________________/________/
У нас есть базовый класс Shape, от которого мы наследуем классы Triangle и Circle. У Фигуры есть приватные поля и публичные методы, которые наследуются. У Круга также есть поле и метод. Также у нас есть два интерфейса. IDrawable, который наследуется Треугольником и Кругом, и IMovable, который наследуется только Треугольником. Таким образом мы хотим гарантировать то что оба наследника рисуются, т.е. переопределяют метод draw(). И Треугольник может двигаться, переопределяя метод move().
#include <iostream>
#include <vector>
#include <string>
class IDrawable {
public:
virtual void draw() = 0;
};
class IMovable {
public:
virtual void move() = 0;
};
class Shape {
public:
Shape(int _x, int _y) : x{_x}, y{_y} {}
virtual ~Shape() = default;
int getX() const {return x;}
int getY() const {return y;}
private:
int x;
int y;
};
class Triangle : public Shape, public IDrawable, public IMovable{
public:
Triangle(int _x, int _y) : Shape(_x, _y) {}
~Triangle() override = default;
void
draw() override {std::cout << "Triangle draw\n";}
void
move() override {std::cout << "Triangle move\n";}
private:
};
class Circle : public Shape, public IDrawable {
public:
Circle(int _x, int _y, int _r) : Shape(_x, _y), r{_r} {}
~Circle() override = default;
void
draw() override {std::cout << "Circle draw\n";}
int getR() const {return r;}
private:
int r;
};
Теперь для того чтобы не создавать каждый раз новый контейнер, определенного типа (например, одного из интерфейсов или базового класса), а работать с одним, нам нужен инструмент для перехода по иерархии типов, для вызова методов этих типов или доступа к данным. Дело в том, что имея например, контейнер Фигур, мы можем иметь доступ только к членам этого типа. И хотя в контейнере хранятся Круги, нам не доступен Радиус (int r) этих объектов. Но так как Круг является Фигурой (по нашей иерархии), мы можем изменить тип объекта из Фигуры в Круг, тем самым получив доступ к его членам. В этом нам поможет динамическое приведение типов
dynamic_cast<T>(obj), принимающий в качестве параметра объект obj, который мы хотим
привести к типу T.
int main() {
Triangle tri1(0, 0);
Circle cir1(1, 1, 1);
std::vector<IDrawable *> shapes = {&tri1, &cir1};
for (auto
i_asIDrawable : shapes) {
i_asIDrawable->draw();
if (auto
i_asShape = dynamic_cast<Shape *>(i_asIDrawable)) {
std::cout << "x: " << i_asShape->getX() << std::endl;
std::cout << "y: " << i_asShape->getY() << std::endl;
}
if (auto
i_asCircle = dynamic_cast<Circle *>(i_asIDrawable)) {
std::cout << "r: " << i_asCircle->getR() << std::endl;
}
if (auto
i_asIMovable = dynamic_cast<IMovable *>(i_asIDrawable)) {
i_asIMovable ->move();
}
}
return 0;
}
Output:
>>Triangle draw
>>x: 0
>>y: 0
>>Triangle move
>>Circle draw
>>x: 1
>>y: 1
>>r: 1
К сожалению, использовать динамическое приведение очень дорого, т.к. создается временная
копия объекта с новым типом. И считается плохим архитектурным решением. Поэтому часто, в угоду производительности, интерфейсы объединяют в базовые классы, что усложняет понимание кода.
Перегуд В.