Абстрактные классы и интерфейсы

Для изучения абстрактных классов и интерфейсов вернемся к нашему сквозному примеру программы рисования. Её программный интерфейс для простоты состоит из единственного виртуального метода draw. До сих пор он был пустым, но вместе с тем даже такая пустая реализация является конкретной реализацией. Однако объекты класса Shape не могут изображаться — их фигура не определена. Поэтому метод draw имеет смысл сделать абстрактным или, как иначе это называют, чисто виртуальным.

Для этого блок с пустой реализацией следует убрать, а в заголовке метода добавить "= 0":

class Shape
{
public:
   virtual void draw() = 0;
   ...

Класс, у которого есть хотя бы один абстрактный метод, также становится абстрактным, потому что создать его объект не получится: нет реализации. В частности, у нас конструктор Shape был доступен производным классам (благодаря модификатору protected), и их разработчики могли, гипотетически, создать объект Shape. Но так было раньше, а после объявления абстрактного метода мы такое поведение пресекли, как запрещенное нами — авторами интерфейса рисования. Компилятор будет выдавать ошибку:

'Shape' - cannot instantiate abstract class
      'void Shape::draw()' is abstract

Наиболее правильным подходом для описания интерфейса является создание под него абстрактного класса, содержащего только абстрактные методы. В нашем случае, метод draw следует вынести в новый класс Drawable, а класс Shape унаследовать от него (Shapes.mq5).

class Drawable
{
public:
   virtual void draw() = 0;
};
 
class Shape : public Drawable
{
public:
   ...
   // virtual void draw() = 0; // вынесен в базовый класс
   ...
};

Разумеется, интерфейсные методы должны находиться в секции public.

MQL5 предоставляет другой удобный способ описания интерфейсов — с помощью ключевого слова interface. Все методы в интерфейсе объявляются без реализации и считаются публичными и виртуальными. Описание интерфейса Drawable, эквивалентное вышеприведенному классу, выглядит так:

interface Drawable
{
   void draw();
};

В классах наследниках при этом ничего не приходится менять, если в абстрактном классе не было никаких полей (что было бы нарушением принципа абстракции).

Теперь пришло время расширить интерфейс и сделать тройку методов setColor, moveX, moveY также его частью.

interface Drawable
{
   void draw();
   Drawable *setColor(const color c);
   Drawable *moveX(const int x);
   Drawable *moveY(const int y);
};

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

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

class Text : public Drawable
{
public:
   Text(const string label)
   {
      ...
   }
   
   void draw()
   {
      ...
   }
   
   Text *setColor(const color c)
   {
      ...
      return &this;
   }
   ...
};

Если бы классы фигур распространялись в виде двоичной ex5-библиотеки (без исходных кодов), мы бы поставляли для неё заголовочный файл, содержащий только описание интерфейса, и никаких намеков о внутренних структурах данных.

Поскольку для виртуальных функций выполняется динамическое (позднее) связывание с объектом во время выполнения программы, есть вероятность получить критическую ошибку "вызов чистой виртуальной функции" ("Pure virtual function call"): программа при этом завершает работу. Это происходит, если программист по недосмотру "забыл" предоставить реализацию. Компилятор не всегда способен выявить такие упущения на стадии компиляции.