Friday, November 15, 2019

Абстрактные классы (интерфейсы) в си++

Любой объект или сущность в природе имеет уровень абстракции. Это удобно и позволяет переходить от общего к деталям и наоборот. Например, “стул” — это конкретное описание предмета, но мы также можем говорить о нем, как о “мебели” или к примеру, как об “изделии из дерева”. Исходя из этого мы можем сказать, что стул — это также и элемент мебели, еще раз – стул является элементом мебели. В свою очередь стулья могут делиться на офисные, кухонные, и т.д., что в свою очередь вырастает в довольно запутанную иерархию отношений.  В нашем случае нам помогают с этим справиться классы и наследование, мы говорили об этом, когда рассматривали полиморфизм. Животные как общий уровень абстракции и кот с собакой, как конкретный уровень абстракции. К примеру общий уровень нам нужен как тип контейнера, в котором мы храним конкретные типы.

Как правило, когда мы работаем с иерархией классов нам нужны объекты конкретных типов, а не общих. В рассмотренном примере нам были нужны объекты конкретно собаки и кота. Но если бы мы рассматривали животных, рыб и людей, как конкретные типы формы жизни на нашей планете, то животные были бы конкретным типом, и мы бы создавали такие объекты.  Более того, если мы создаем в базовом классе чисто виртуальные функции, которые обязуемся реализовать в наследниках, то физически не можем создавать объекты такого класса. Вот и получается, что базовый класс для таких наследников является абстрактным, и служит лишь для доступа к наследующим его конкретным классам, и мы называем его интерфейсом.
 
Как пульт дистанционного управления является интерфейсом между человеком и телевизором. Пультов много разных по форме, способу ввода информации и т.д. но все они должны предоставлять для пользователя кнопки с цифрами, для ввода номеров каналов. Этими кнопками служат наши чисто виртуальные методы. В современных языках программирования (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

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

No comments: