Clases abstractas e interfaces

Para explorar las clases abstractas y las interfaces vamos a volver a nuestro ejemplo de programa de dibujo de extremo a extremo. Para simplificar, su API consiste en un único método draw virtual. Hasta ahora, ha estado vacía, pero al mismo tiempo, incluso una implementación tan vacía es una implementación concreta. Sin embargo, los objetos de la clase Shape no se pueden dibujar: su forma no está definida. Por lo tanto, tiene sentido hacer que el método draw sea abstracto o, como también se denomina, puramente virtual.

Para ello, hay que eliminar el bloque con una implementación vacía y añadir «= 0» a la cabecera del método:

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

Una clase que tiene al menos un método abstracto también se convierte en abstracta, ya que su objeto no se puede crear: no hay implementación. En concreto, nuestro constructor Shape estaba disponible para las clases derivadas (gracias al modificador protected), y sus desarrolladores podían, hipotéticamente, crear un objeto Shape. Pero era así antes, y después de la declaración del método abstracto detuvimos este comportamiento, ya que lo habíamos prohibido nosotros, los autores de la interfaz de dibujo. El compilador arrojará un error:

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

El mejor enfoque para describir una interfaz es crear una clase abstracta para ella que contenga sólo métodos abstractos. En nuestro caso, el método draw debería trasladarse a la nueva clase Drawable, y la clase Shape debería heredarse de ella (Shapes.mq5).

class Drawable
{
public:
   virtual void draw() = 0;
};
 
class Shape : public Drawable
{
public:
   ...
   // virtual void draw() = 0; // moved to base class
   ...
};

Por supuesto, los métodos de interfaz deben estar en la sección public.

MQL5 proporciona otra manera conveniente de describir las interfaces mediante el uso de la palabra clave interface. Todos los métodos de una interfaz se declaran sin implementación y se consideran públicos y virtuales. La descripción de la interfaz Drawable que equivale a la clase anterior tiene el siguiente aspecto:

interface Drawable
{
   void draw();
};

En este caso no es necesario cambiar nada en las clases descendientes si no hubo campos en la clase abstracta (lo que supondría una violación del principio de abstracción).

Ahora es el momento de ampliar la interfaz y hacer que el trío de métodos setColor, moveX, moveYtambién formen parte de ella.

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

Tenga en cuenta que los métodos devuelven un objeto Drawable porque no sé nada de Shape. En la clase Shape tenemos ya implementaciones que son adecuadas para sobrescribir (override) estos métodos, porque Shape hereda de Drawable (Shape «son una especie de» objetos Drawable).

Ahora, los desarrolladores externos pueden añadir otras familias de clases Drawable al programa de dibujo, en concreto, no sólo formas, sino también texto, mapas de bits y también, sorprendentemente, recopilaciones de otros Drawables, lo que permite anidar objetos unos dentro de otros y realizar composiciones complejas. Basta con heredar de la interfaz e implementar sus métodos.

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

Si las clases de forma se distribuyeran como una biblioteca binaria ex5 (sin códigos fuente), proporcionaríamos un archivo de cabecera que sólo contuviera la descripción de la interfaz y ninguna pista sobre las estructuras de datos internas.

Dado que las funciones virtuales se vinculan dinámicamente (más tarde) a un objeto durante la ejecución del programa, es posible obtener un error fatal «Llamada a función virtual pura»: el programa termina. Esto ocurre si el programador «olvidó» sin darse cuenta proporcionar una implementación. El compilador no siempre es capaz de detectar tales omisiones en tiempo de compilación.