Dividir definición y declaración de clase

En los grandes proyectos de software es conveniente separar las clases en una breve descripción (declaración) y una definición, que incluye los principales detalles de implementación. En algunos casos, dicha separación se hace necesaria si las clases se refieren de alguna manera entre sí, es decir, ninguna puede definirse completamente sin declaraciones previas.

Hemos visto un ejemplo de declaración forward en la sección Indicadores (véase el archivo ThisCallback.mq5), donde las clases Manager y Element contienen punteros recíprocos. Allí, la clase se predeclaró de forma abreviada; es decir, forma de un encabezado con la palabra clave class y un nombre:

class Manager;

No obstante, esta es la declaración más breve posible. Registra sólo el nombre y permite posponer la descripción de la interfaz de programación hasta un momento dado, pero esta descripción debe encontrarse en algún punto posterior del código.

La mayoría de las veces, la declaración incluye una descripción completa de la interfaz: especifica todas las variables y cabeceras de métodos de la clase, pero sin sus cuerpos (bloques de código).

Las definiciones de métodos se escriben por separado: con cabeceras que utilizan nombres totalmente cualificados que incluyen el nombre de la clase (o varias clases y espacios de nombres si el contexto del método está muy anidado). Los nombres de todas las clases y el nombre del método se concatenan utilizando el operador de selección contextual '::'.

type class_name [:: nested_class_name...] :: method_name([parameters...])
{
}

En teoría, puede definir parte de los métodos directamente en el bloque de descripción de la clase (normalmente lo hacen con las funciones pequeñas), y algunos pueden sacarse por separado (por regla general, las funciones grandes). Pero un método debe tener una sola definición (es decir, no se puede definir un método en un bloque de clase y luego otra vez por separado) y una declaración (una definición en un bloque de clase es también una declaración).

La lista de parámetros, el tipo de devolución y los modificadores const (si existen) deben coincidir exactamente en la definición y declaración del método.

Veamos cómo podemos separar la descripción y definición de clases del script ThisCallback.mq5 (ejemplo de la sección Punteros): vamos a crear su análogo con el nombre ThisCallback2.mq5.

La predeclaración Manager seguirá estando al principio. Además, ambas clases Element y Manager se declaran sin implementación: en lugar de un bloque de código con un cuerpo de método, hay un punto y coma.

class Manager// preliminary announcement
  
class Element
{
   Manager *owner// pointer
public:
   Element(Manager &t);
   void doMath();
   string getMyName() const;
};
  
class Manager
{
   Element *elements[1]; // array of pointers (replace with dynamic)
public:
   ~Manager();
   Element *addElement();
   void progressNotify(Element *econst float percent);
};

La segunda parte del código fuente contiene las implementaciones de todos los métodos (las implementaciones en sí no se modifican).

Element::Element(Manager &t) : owner(&t)
{
}
 
void Element::doMath()
{
   ...
}
 
string Element::getMyName() const
{
   return typename(this);
}
 
Manager::~Manager()
{
   ...
}
 
Element *Manager::addElement()
{
   ...
}
 
void Manager::progressNotify(Element *econst float percent)
{
   ...
}

Las estructuras también admiten definiciones y declaraciones de métodos independientes.

Tenga en cuenta que la lista de inicialización del constructor (después del nombre y ':') forma parte de la definición y, por tanto, debe preceder al cuerpo de la función (en otras palabras, la lista de inicialización no está permitida en una declaración de constructor en la que sólo esté presente la cabecera).

La redacción por separado de la declaración y la definición permite el desarrollo de bibliotecas cuyo código fuente debe estar cerrado. En este caso, las declaraciones se colocan en un archivo de cabecera independiente con la extensión mqh, mientras que las definiciones se colocan en un archivo del mismo nombre con la extensión mq5. El programa se compila y distribuye como un archivo ex5 con un archivo de cabecera que describe la interfaz externa.

En este caso puede plantearse la pregunta de por qué parte de la implementación interna, en concreto la organización de los datos (variables), es visible en la interfaz externa. En sentido estricto, esto indica un nivel insuficiente de abstracción en la jerarquía de clases. Todas las clases que proporcionan una interfaz externa no deben exponer ningún detalle de implementación.

En otras palabras: si nos marcamos el objetivo de exportar las clases anteriores de una determinada biblioteca, entonces tendríamos que separar sus métodos en clases base que proporcionaran una descripción de la API (sin campos de datos), y Manager y Element heredarían de ellas. Al mismo tiempo, en los métodos de las clases base, no podemos utilizar ningún dato de las clases derivadas y, en general, no pueden tener ninguna implementación. ¿Cómo es esto posible?

Para ello, existe una tecnología de métodos abstractos, clases abstractas e interfaces.