Las bases de la programación orientada a objetos

Dmitry Fedoseev | 26 diciembre, 2013

 

Introducción

Suponemos que cualquiera que empieza a aprender programación orientada a objetos (OOP), se encuentre por primera vez con palabras como poliformismo, encapsulación, sobrecarga y herencia. Quizás alguien vio algunas clases ya preparadas e intentó averiguar dónde estaban esos poliformismos o encapsulación... Lo más probable es que ese sea el final del proceso de aprendizaje de OOP.

De hecho, todo es más sencillo de lo que parece. Para utilizar OOP no es necesario saber lo que significan estas palabras, simplemente puede utilizar las características de OOP sin saber siquiera cómo se llaman. Aún así espero que todos los que lean este artículo no sólo aprendan a utilizar OOP, sino también sepan los significados de estas palabras.

 

Crear bibliotecas de funciones

La primera y la más simple aplicación de OOP es la creación de sus propias bibliotecas de funciones utilizadas con frecuencia. Por supuesto, sólo tiene que almacenar estas funciones en un archivo de inclusión (mqh). Si necesita una función, basta con incluir un archivo y activar a esta función. Sin embargo, si usted programa lo suficiente puede recopilar una gran cantidad de funciones, por lo que sería difícil de recordar sus nombres y propósitos.

Puede recopilar las funciones en diferentes archivos, dividiendo en categorías basadas en propósito. Por ejemplo, funciones que trabajan con arrays, funciones que trabajan con secuencias, funciones de órdenes de cálculo, etc. En la última frase, la palabra "categoría" puede sustituirse por la palabra "clases". El significado sigue siendo el mismo pero se acercará más al tema de programación orientada a objetos.

Así pues, las funciones se pueden dividir en varias clases: clase de funciones que trabajan con arrays, clase de funciones que trabajan con secuencias, clase de funciones que cuentan órdenes, etc. La palabra "clase" nos acerca al tema de OOP puesto que es su concepto fundamental. Puede buscar varios libros de referencia, diccionarios y enciclopedias (por ejemplo Wikipedia) qué es "clase en programación".

En la programación orientada a objetos, una clase es una construcción que se usa como un modelo para crear instancias de la misma.

Quizás, la primera impresión sería la misma que con las palabras "poliformismo", "encapsulación", etc. Por el momento, con "clase" nos referiremos a un conjunto de funciones y variables. En el caso de usar una clase para crear una biblioteca - un conjunto de funciones y variables agrupadas por tipo de datos procesados o por tipo de objetos procesados: arrays, secuencias, órdenes.

 

Un programa en programa

Hubo (y habrá) muchas cuestiones similares de el foro - ¿Cómo activar un script desde un Expert Advisor? Mientras no use herramientas de terceros, esta tarea se realiza colocando el código del script en el código del Expert Advisor. De hecho, no es una tarea difícil, pero un script puede usar los mismos nombres de variables y funciones que EA, por lo que necesitará ajustar el código del script. Los cambios no son complejos, pero probablemente serán de un volumen considerable.

Sería genial simplemente activar este script como un programa independiente. Es posible si programa el script como una clase y a continuación, usa esta clase. La cantidad de trabajo se incrementará sólo en unas líneas de código. En este caso, una clase combinará funciones no por el tipo de datos procesados, sino de acuerdo a su propósito. Por ejemplo: una clase para eliminar órdenes pendientes, una clase para abrir una posición, una clase para trabajar con objetos gráficos, etc.

Una característica importante de la clase es que se distingue del espacio en que se encuentra. La clase es como un programa que se ejecuta en un sistema operativo: múltiples programas se pueden ejecutar al mismo tiempo, pero por sí solos, de forma independiente el uno del otro. Por lo tanto, la clase puede llamarse "un programa en programa" puesto que se distingue del espacio en que se encuentra.

 

Aspecto de una clase

La creación de una clase comienza escribiendo la palabra clase, seguido por el nombre de la clase y a continuación se coloca el código completo entre corchetes:

clase CName 
  {
   // Aquí va el código completo de la clase
  };
Atención No olvide poner un punto y coma tras cerrar los corchetes.

 

Visible y oculto (Encapsulación)

Si toma un programa, sabemos que incluye una variedad de funciones. Estas funciones se pueden dividir en dos tipos: principal y auxiliar. Las funciones principales son funciones de las que un programa se compone realmente. Estas funciones podrían requerir muchas otras funciones de las que el usuario no tiene que saber. Por ejemplo, en el terminal del cliente para abrir una posición el agente necesita abrir el diálogo Nueva Orden, introducir volumen, valores de Stop Loss y Take Profit y a continuación hacer clic en "Comprar" o "Vender".

Pero lo que realmente sucede entre hacer clic en el botón y abrir una posición sólo los desarrolladores de terminales pueden decirlo a ciencia cierta. Podemos suponer que el terminal hace un montón de acciones: comprueba la posición del volumen, comprueba los valores de Stop Loss y Take Profit, comprueba la conexión de la red, etc. Muchos otros procedimientos están ocultos o, en otras palabras, encapsulados. De manera similar, en una clase puede dividir un código en piezas (funciones y variables) algunas de ellas estarán disponibles al usar una clase y otras estarán ocultas.

Los niveles de encapsulación se definen al usar las siguientes palabras clave: private, protected y public. Las diferencias entre protected y private se considerarán más tarde, pero primeros trataremos las palabras clave private y public. Por lo tanto, una plantilla de clase simple tiene la siguiente forma:

class CName 
  {
private:
   // Variables y funciones disponibles sólo dentro de la clase
public:
   // Variables y funciones disponibles fuera de la clase
  };
Esto es suficiente para aprovechar las ventajas de la OOP. En lugar de escribir su código directamente en el Expert Advisor (Script o Indicador), primero cree una clase y después escriba todo en esta clase. A continuación consideraremos la diferencia entre las secciones private y public con un ejemplo práctico.

 

Ejemplo de creación de biblioteca

La plantilla de clase presentada anteriormente se puede usar para crear una biblioteca de funciones. Creemos una clase para trabajar con arrays. Las tareas más comunes pueden surgir al usar un array (añadirán un nuevo elemento al array y añadirán un nuevo elemento, siempre que el elemento con el valor dado no exista en el array).

Llamemos a la función que añade un elemento AddToEnd() y a la función que añade un único elemento AddToEndIfNotExists(). En la función AddToEndIfNotExists() primero necesitaremos comprobar si el elemento añadido ya existe en el array y si no, usar la función AddToEnd(). La función que comprueba si un elemento ya existe será considerada como auxiliar, por lo tanto, se colocará en la sección private y todas las demás en la sección public. En consecuencia, obtendremos la siguiente clase:

class CLibArray 
  {
private:
   // Compruebe si un elemento con el valor requerido existe en el array
   int Find(int &aArray[],int aValue) 
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // Elemento existente, devolver índice del elemento
           }
        }
      return(-1);  // Elemento inexistente, devolver -1
     }
public:
   // Añadir al final del array
   void AddToEnd(int &aArray[],int aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // Añadir al final del array si aún no hay tal valor en el array
   void AddToEndIfNotExistss(int &aArray[],int aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
  };
 

Cargar clase

Para usar una clase, se debe cargar. Si una clase se encuentra en un archivo separado, debe incluir este archivo

#include <OOP_CLibArray_1.mqh>

y después cargar esta clase. La carga de clase es similar a la declaración de variable:

CLibArray ar;

Primero viene el nombre de la clase y luego el nombre de un puntero para referirse a esta instancia. Después la carga la clase se convierte en un objeto. Para usar cualquier función de un objeto escriba el nombre del indicador, el punto y luego el nombre de la función. Después de escribir el punto, se abrirá una lista desplegable de funciones de clase (Ilustración 1).

Ilustración 1 Lista de funciones
Ilustración 1. Lista de funciones

Gracias a la lista desplegable no es necesario recordar los nombres de las funciones - puede navegar por la lista de nombres y recordar el propósito de la función. Esta es la mayor ventaja del uso de clases para crear bibliotecas en lugar de simplemente recopilar funciones en archivos.

En el caso de recopilar funciones, al escribir algunas letras iniciales del nombre de la función, la lista desplegable mostrará todas las funciones de todas las bibliotecas incluidas, y cuando se utilizan clases - solo las funciones relacionadas con la clase especificada. También tenga en cuenta que la función Find() no se muestra - esta es la diferencia entre las secciones private y public. La función se escribe en la sección private y por lo tanto no está disponible.

 

Hacer una biblioteca universal para diferentes tipos de datos (sobrecarga)

En este punto, nuestra biblioteca incluye funciones que funcionan sólo con arrays del tipo int. Además del tipo de arrays int podríamos necesitar aplicar funciones de biblioteca a los arrays de los siguientes tipos: uint, long, ulong etc. Para arrays de otros tipo de datos, tenemos que escribir sus propias funciones. Sin embargo, no necesita darle otros nombres a esas funciones - la función correcta se seleccionará automáticamente dependiendo del tipo de parámetro cambiado o conjunto de parámetros (en este ejemplo, dependiendo del tipo de parámetros). Vamos a complementar la clase con funciones que trabajan con arrays del tipo long:

class CLibArray 
  {
private:
   // Compruebe si un elemento con valor requerido existe en el array
   int Find(int &aArray[],int aValue)
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // El elemento existe, volver a índice del elemento
           }
        }
      return(-1); // No está ese elemento, volver -1
     }
   // For long. Compruebe si un elemento con valor requerido existe en el array
   int Find(long &aArray[],long aValue) 
     {
      for(int i=0; i<ArraySize(aArray); i++) 
        {
         if(aArray[i]==aValue) 
           {
            return(i); // El elemento existe, volver a índice del elemento
           }
        }
      return(-1); // No está ese elemento, volver -1
     }
public:
   // For int. Añadir al final del array
   void AddToEnd(int &aArray[],int aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // For long. Añadir al final del array
   void AddToEnd(long &aArray[],long aValue) 
     {
      int m_size=ArraySize(aArray);
      ArrayResize(aArray,m_size+1);
      aArray[m_size]=aValue;
     }
   // For int. Añadir al final del array si no está ese valor en el array
   void AddToEndIfNotExistss(int &aArray[],int aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
   // For long. Añadir al final del array si no está ese valor en el array
   void AddToEndIfNotExistss(long &aArray[],long aValue) 
     {
      if(Find(aArray,aValue)==-1) 
        {
         AddToEnd(aArray,aValue);
        }
     }
  };
Ahora, utilizando el mismo nombre tenemos una funcionalidad diferente. Estas funciones se denominan sobrecargadas, ya que un nombre está cargado con más de una funcionalidad, es decir, sobrecargados.

Puede encontrar este ejemplo en el archivo OOP_CLibArray_1.mqh adjunto a este artículo.

 

Otro modo de notación de clase

En los ejemplos anteriores todas las funciones se escribieron dentro de la clase. Si tiene un montón de funciones y cada uno de ellas es bastante grande, tal notación puede no ser muy cómoda. En tales casos, puede colocar las funciones fuera de la clase. Dentro de la clase se escribe sólo los nombres de funciones con parámetros y las funciones se describen completamente fuera de la clase. Además, hay que indicar que la función pertenece a una clase específica: primero se escribe el nombre de la clase, luego se ponen dos signos de dos puntos y el nombre de la función:

class CLibArray 
  {
private:
   int               Find(int  &aArray[],int  aValue);
   int               Find(long &aArray[],long aValue);
public:
   void              AddToEnd(int  &aArray[],int  aValue);
   void              AddToEnd(long &aArray[],long aValue);
   void              AddToEndIfNotExistss(int  &aArray[],int  aValue);
   void              AddToEndIfNotExistss(long &aArray[],long aValue);
  };
//---
int CLibArray::Find(int &aArray[],int aValue) 
  {
   for(int i=0; i<ArraySize(aArray); i++) 
     {
      if(aArray[i]==aValue) 
        {
         return(i);
        }
     }
   return(-1);
  }
//---
int CLibArray::Find(long &aArray[],long aValue) 
  {
   for(int i=0; i<ArraySize(aArray); i++) 
     {
      if(aArray[i]==aValue) 
        {
         return(i);
        }
     }
   return(-1);
  }
//---
void CLibArray::AddToEnd(int &aArray[],int aValue) 
  {
   int m_size=ArraySize(aArray);
   ArrayResize(aArray,m_size+1);
   aArray[m_size]=aValue;
  }
//---
void CLibArray::AddToEnd(long &aArray[],long aValue) 
  {
   int m_size=ArraySize(aArray);
   ArrayResize(aArray,m_size+1);
   aArray[m_size]=aValue;
  }
//---
void CLibArray::AddToEndIfNotExistss(int &aArray[],int aValue) 
  {
   if(Find(aArray,aValue)==-1) 
     {
      AddToEnd(aArray,aValue);
     }
  }
//---
void CLibArray::AddToEndIfNotExistss(long &aArray[],long aValue) 
  {
   if(Find(aArray,aValue)==-1) 
     {
      AddToEnd(aArray,aValue);
     }
  }

Con tal notación se puede obtener una imagen completa de la composición de clase y echar un vistazo más de cerca a las funciones individuales si es necesario.

Puede encontrar este ejemplo en el archivo OOP_CLibArray_2.mqh adjunto a este artículo.

 

Declarar variables en clase

Sigamos para considerar el ejemplo mencionado anteriormente. Hay una diferencia entre codificar directamente en el archivo y dentro de la clase. Directamente en el archivo puede asignar variables con valores como las declara:

int Var = 123;

Si declara una variable en una clase no puede hacer esto; los valores deben ser asignados al ejecutar alguna función de una clase. Por lo que, antes que nada necesita transferir parámetros a la clase (es decir, preparar la clase para trabajar). Llamemos a esta función Init().

Considerémosla en un ejemplo práctico.

 

Ejemplo de conversión de Script en clase

Suponga que hay un script que elimina órdenes pendientes (véase el archivo OOP_sDeleteOrders_1.mq5 adjunto).

// Include file to use the CTrade class from standard delivery
#include <Trade/Trade.mqh>

// External parameters

// Select symbol. true  - delete orders for all symbols, 
//                false - only for symbol of chart, where the script is running
input bool AllSymbol=false;

// Select types of orders to delete
input bool BuyStop       = false;
input bool SellStop      = false;
input bool BuyLimit      = false;
input bool SellLimit     = false;
input bool BuyStopLimit  = false;
input bool SellStopLimit = false;

// Load the CTrade class
CTrade Trade;
//---
void OnStart()
  {
// Variable to check function result
   bool Ret=true;
// Loop by all orders in terminal
   for(int i=0; i<OrdersTotal(); i++)
     {
      ulong Ticket=OrderGetTicket(i); // Select order and get its ticket
                                      // Successfully selected
      if(Ticket>0)
        {
         long Type=OrderGetInteger(ORDER_TYPE);
         // Check order type
         if(Type == ORDER_TYPE_BUY_STOP && !BuyStop) continue;
         if(Type == ORDER_TYPE_SELL_STOP && !SellStop) continue;
         if(Type == ORDER_TYPE_BUY_LIMIT && !BuyLimit) continue;
         if(Type == ORDER_TYPE_SELL_LIMIT && !SellLimit) continue;
         if(Type == ORDER_TYPE_BUY_STOP_LIMIT && !BuyStopLimit) continue;
         if(Type == ORDER_TYPE_SELL_STOP_LIMIT && !SellStopLimit) continue;
         // Check symbol
         if(!AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue;
         // Delete
         if(!Trade.OrderDelete(Ticket))
           {
            Ret=false; // Failed to delete
           }
        }
      // Failed to select order, unknown result,
      // function ended up with error
      else
        {
         Ret=false;
         Print("Error selecting order");
        }
     }

   if(Ret)
     {
      Alert("Script ended successfully");
     }
   else    
     {
      Alert("Script ended up with error, see details. in Journal");
     }
  }

El script tiene parámetros externos que permiten habilitar varios tipos de órdenes y seleccionar el símbolo, para lo cual se eliminarán las órdenes (todos los símbolos o símbolo de la tabla en la que se ejecuta el script).

Convierta este script en una clase llamada COrderDelete. En la sección private vamos a declarar las mismas variables que se declaran en la secuencia de comandos como parámetros externos, pero con el prefijo "m_" en los nombres de variables (de la palabra "miembro", es decir, miembro de la clase). No es necesario añadir un prefijo, pero es muy conveniente puesto que permite distinguir fácilmente las variables. Así pues, podemos saber con certeza que se trata de variables limitadas por el espacio de clase. Además, no recibirá advertencias del compilador sobre que la declaración de variable oculta variable declarada en el ámbito global.

Usar los mismos nombres de variables en el ámbito global, en la definición de la clase, en el cuerpo de la función no es un error, sino que hace que el programa sea difícil de entender, así que es por eso que en estos casos el compilador hace una advertencia. Para asignar variables con valores escriba la función Init() con los parámetros correspondientes a estas variables (y los parámetros externos de la secuencia de comandos). Si usa esta función, primero tiene que activar la función Init() y transferir los parámetros externos a ésta. El resto del código del script no cambia. La única excepción - en lugar de usar directamente los parámetros externos, debe usar variables declaradas dentro de la clase.

Así que tenemos la siguiente clase:

#include <Trade/Trade.mqh> 

class COrderDelete 
  {

private:
   // Variables for parameters
   bool              m_AllSymbol;
   bool              m_BuyStop;
   bool              m_SellStop;
   bool              m_BuyLimit;
   bool              m_SellLimit;
   bool              m_BuyStopLimit;
   bool              m_SellStopLimit;
   // Load the CTrade class
   CTrade            m_Trade;
public:
   // Function to set parameters
   void Init(bool aAllSymbol,bool aBuyStop,bool aSellStop,bool aBuyLimit,bool aSellLimit,bool aBuyStopLimit,bool aSellStopLimit) 
     {
      // Set parameters
      m_AllSymbol    =aAllSymbol;
      m_BuyStop      =aBuyStop;
      m_SellStop     =aSellStop;
      m_BuyLimit     =aBuyLimit;
      m_SellLimit    =aSellLimit;
      m_BuyStopLimit =aBuyStopLimit;
      m_SellStopLimit=aSellStopLimit;
     }
   Main function to delete orders
   bool Delete() 
     {
      // Variable to check function result
      bool m_Ret=true;
      // Loop by all orders in terminal
      for(int i=0; i<OrdersTotal(); i++) 
        {
         // Select order and get its ticket
         ulong m_Ticket=OrderGetTicket(i);
         // Successfully selected
         if(m_Ticket>0) 
           {
            long m_Type=OrderGetInteger(ORDER_TYPE);
            // Check order type
            if(m_Type == ORDER_TYPE_BUY_STOP && !m_BuyStop) continue;
            if(m_Type == ORDER_TYPE_SELL_STOP && !m_SellStop) continue;
            if(m_Type == ORDER_TYPE_BUY_LIMIT && !m_BuyLimit) continue;
            if(m_Type == ORDER_TYPE_SELL_LIMIT && !m_SellLimit) continue;
            if(m_Type == ORDER_TYPE_BUY_STOP_LIMIT && !m_BuyStopLimit) continue;
            if(m_Type == ORDER_TYPE_SELL_STOP_LIMIT && !m_SellStopLimit) continue;
            // Check symbol/s61>
            if(!m_AllSymbol && Symbol()!=OrderGetString(ORDER_SYMBOL)) continue;
            // Delete
            if(!m_Trade.OrderDelete(m_Ticket)) 
              {
               m_Ret=false; // Failed to delete
              }
           }
         // Failed to select order, unknown result,
         // function ended up with error
         else 
           {
            m_Ret=false;
            Print("Error selecting order");
           }
        }
      // Return function result
      return(m_Ret);
     }
  };
Puede encontrar un ejemplo de esta clase en el archivo OOP_CDeleteOrder_1.mqh adjunto a este artículo. El script que usa esta clase se reduce al un mínimo (parámetros externos, clase de carga, métodos de activación de Init() y Delete()):
// External parameters

// Select symbol. true  - delete orders for all symbols, 
//                false - only for symbol of chart, where the script is running
input bool AllSymbol=false;

// Select types of orders to delete
input bool BuyStop       = false;
input bool SellStop      = false;
input bool BuyLimit      = false;
input bool SellLimit     = false;
input bool BuyStopLimit  = false;
input bool SellStopLimit = false;

// Include file with class
#include <OOP_CDeleteOrder_1.mqh> 

// Load class
COrderDelete od;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart() 
  {
// Pass external parameters to the class
   od.Init(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);
// Delete orders
   bool Ret=od.Delete();
// Process result of deleting
   if(Ret) 
     { 
       Alert("Script ended successfully"); 
     }
   else    
     { 
       Alert("Script ended up with error, see details in Journal"); 
     }
  }

Puede encontrar un ejemplo de este script en el archivo OOP_sDeleteOrders_2.mq5 adjunto a este artículo. La mayoría del script está procesando los resultados de la función Delete(), notificando por tanto los resultados del script.

Ahora todas las funciones básicas del script se designan como una clase ubicada en un archivo separado, por lo que puede usar esta clase desde cualquier otro programa (Expert Advisor o Script), es decir, activar este script desde Expert Advisor.

 

Un poco de automáticos (Constructor y Destructor)

El funcionamiento del programa se puede dividir en tres fases: lanzamiento del programa, proceso de trabajo y conclusión de sus trabajos. La importancia de esta separación es obvia: cuando el programa se inicia se prepara (por ejemplo, carga y establece los parámetros con los que trabajar), cuando el programa finaliza debe realizar una "limpieza" (por ejemplo, eliminar objetos gráficos de la tabla).

Para separar estas fases Expert Advisors e indicadores tienen funciones especiales: OnInit() (ejecuta e inicia) y OnDeinit() (ejecución en el apagado). Las clases tienen las características similares: puede agregar funciones que se ejecutarán automáticamente cuando la clase se carga y cuando se descarga. Estas funciones se llaman Constructor y Destructor. Añadir una constructor a la clase significa añadir una función con exactamente el mismo nombre que el nombre de la clase. Para añadir un destructor - haga lo mismo que para crear un constructor, pero el nombre de la función empieza con "~".

Un script que demuestra el constructor y destructor:

// Class
class CName 
  {
public:
   // Constructor
                     CName() { Alert("Constructor"); }
   // Destructor
                    ~CName() { Alert("Destructor"); }

   void Sleep() { Sleep(3000); }
  };

// Load class
CName cname;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart() 
  {
// Pause
   cname.Sleep();
  }

Esta clase tiene sólo una función Sleep() que pausa durante 3 segundos. Cuando ejecuta el script, aparece una ventana de alerta con el mensaje "Constructor", después de una pausa de tres segundos, se muestra una ventana de alerta con el mensaje "Destructor". Esto es a pesar del hecho de que las funciones CName() y ~CName() nunca se activaron de manera explícita.

Puede encontrar este ejemplo en el archivo OOP_sConstDestr_1.mq5 adjunto a este artículo.

 

Transferir parámetros a Constructor

En el ejemplo en que convertimos el script en clase, aún puede reducir la cantidad de código por una línea: deshacerse de activar la función Init(). Los parámetros se pueden transferir al constructor al cargar una clase. Añada el constructor a la clase:

COrderDelete(bool aAllSymbol     = false,
             bool aBuyStop       = false,
             bool aSellStop      = false,
             bool aBuyLimit      = false,
             bool aSellLimit     = false,
             bool aBuyStopLimit  = false,
             bool aSellStopLimit=false) 
  {
   Init(aAllSymbol,aBuyStop,aSellStop,aBuyLimit,aSellLimit,aBuyStopLimit,aSellStopLimit);
  }

La función Init() permanece tal cal, pero se activa desde el constructor. Todos los parámetros del constructor son opcionales, por lo que la clase se puede utilizar como antes:cargar la clase sin parámetros y activar la función Init().

Después de crear un constructor, hay otra manera de usar esta clase. Cuando se carga la clase, puede transferir parámetros a ésta sin necesitad de activar la función Init():

COrderDelete od(AllSymbol,BuyStop,SellStop,BuyLimit,SellLimit,BuyStopLimit,SellStopLimit);

La función Init() se dejó en la sección public para permitir el reinicio de la clase. Cuando usa el programa (Expert Advisor), en una casilla podría necesitar eliminar las órdenes Stop, en otras sólo las órdenes Limit. Para hacer esto, puede activar la función Init() con diferentes parámetros de manera que la función Delete() eliminará un diferente grupo de órdenes.

Puede encontrar este ejemplo en los archivos OOP_CDeleteOrder_2.mqh y OOP_sDeleteOrders_3.mq5 adjuntos a este artículo.

 

Usar múltiples instancias de una clase

Como se mencionó en la sección anterior, la misma clase puede realizar diferentes acciones en función de los parámetros que se establecen durante la inicialización. Si sabe para qué propósito se va a usar la clase, puede omitir el reinicio de la clase. Para hacer esto, debe cargar unas instancias de una clase con diferentes parámetros.

Por ejemplo, se sabe que cuando nuestro EA se está ejecutando, en algunos casos necesitamos eliminar las órdenes BuyStop y BuyLimit, mientras que en otras casos, las órdenes SellStop y SellLimit. En este caso, puede cargar dos instancias de la clase.

Para eliminar las órdenes BuyStop y BuyLimit:

COrderDelete DeleteBuy(false,true,false,true,false,false,false);

Para eliminar las órdenes SellStop y SellLimit:

COrderDelete DeleteSell(false,false,true,false,true,false,false);

Ahora, cuando quiera eliminar las órdenes pendientes Buy, use una instancia de una clase:

DeleteBuy.Delete();

Cuando quiera eliminar las órdenes pendientes Sell, otra instancia:

DeleteSell.Delete();

 

Array de objetos

No siempre se puede saber a ciencia cierta cuántas instancias de clase necesitará cuando su programa se está ejecutando. En esta casilla puede crear un array de instancias de clase (objetos). Echemos un vistazo a esto en el ejemplo de clase con constructor y destructor. Alterando ligeramente la clase, vamos a transferir el parámetro al constructor, para que podamos monitorear cada instancia de clase:

// Class
class CName 
  {
private:
   int               m_arg; // Variable para la instancia

public:
   // Constructor
   CName(int aArg) 
     {
      m_arg=aArg;
      Alert("Constructor "+IntegerToString(m_arg));
     }
   // Destructor
  ~CName() 
     { 
      Alert("Destructor "+IntegerToString(m_arg)); 
     }
   //---
   void Sleep() 
     { 
      Sleep(3000); 
     }
  };
Usemos esta clase. Puede declarar un array de cierto tamaño, por ejemplo, diez elementos:
CName* cname[10];

Observa una diferencia con respecto al array habitual de declaración de variables, un asterisco "*". Un asterisco avisa que se usa el puntero dinámico en contraste con el puntero automático usado previamente.

Puede usar un array dinámico (sin tamaño preasignado, no confunda array dinámico con puntero dinámico):

CName* cname[];

En este caso se requerirá aumento (realizado en la función, en scripts en la función OnStart()):

ArrayResize(cname,10);

Ahora ejecutemos un bucle a través de todos los elementos del array y carguemos la instancia de clase en cada una de ellas. Para hacerlo, use la nueva clave:

ArrayResize(cname,10);
for(int i=0; i<10; i++) 
  {
   cname[i]=new CName(i);
  }
Pausa:
cname[0].Sleep();

Compruebe el script. Ejecútelo y compruebe que tenga diez constructores, no destructores. Si usa punteros dinámicos, las clases no se cargan automáticamente cuando el programa se termina. Además de esto, en la pestaña "Experts", puede ver mensajes sobre pérdidas de memoria. Debe eliminar objetos manualmente:

for(int i=0; i<10; i++) 
  {
   delete(cname[i]);
  }

Ahora, al final del script, hay diez destructores ejecutándose y ningún mensaje de error.

Puede encontrar este ejemplo en el archivo OOP_sConstDestr_2.mq5 adjunto a este artículo.

 

Usar OOP para cambiar la lógica del programa (funciones virtuales, poliformismo)

Poliformismo, quizá es la función de OOP más interesante y significativa, la cual le permite controlar la lógica de su programa. Hace uso de una clase base con funciones virtuales y múltiples clases secundarias. Hace uso de una clase base con funciones virtuales y múltiples clases secundarias.

Un simple ejemplo, comparación de dos valores. Puede haber cinco versiones de comparación: mayor que (>), menor que (<), mayor o igual que (>=), menor o igual que (<=), igual que (==).

Crear una clase base con función virtual. Función virtual: es exactamente la misma función normal pero su enunciación empieza con la palabra virtual:

class CCheckVariant 
  {
public:
   virtual bool CheckVariant(int Var1,int Var2) 
     {
      return(false);
     }
  };

La función virtual no tiene código. Es un tipo de conector que acepta varios dispositivos. Dependiendo del tipo de dispositivo realizará diferentes acciones.

Crear cinco clases secundarias:

//+------------------------------------------------------------------+
//|   >                                                              |
//+------------------------------------------------------------------+
class CVariant1: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1>Var2);
     }
  };
//+------------------------------------------------------------------+
//|   <                                                              |
//+------------------------------------------------------------------+
class CVariant2: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1<Var2);
     }
  };
//+------------------------------------------------------------------+
//|   >=                                                             |
//+------------------------------------------------------------------+
class CVariant3: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1>=Var2);
     }
  };
//+------------------------------------------------------------------+
//|   <=                                                             |
//+------------------------------------------------------------------+
class CVariant4: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1<=Var2);
     }
  };
//+------------------------------------------------------------------+
//|   ==                                                             |
//+------------------------------------------------------------------+
class CVariant5: public CCheckVariant
  {
   bool CheckVariant(int Var1,int Var2)
     {
      return(Var1==Var2);
     }
  };

Antes de usar esta clase, se debe cargar. Si usted sabe qué clase secundaria se debe utilizar, puede declarar un puntero con el tipo de esta secundaria. Por ejemplo, si quiere comprobar la condición ">":

CVariant1 var; // Cargar clase para comprobar la condición">"

Si, como en nuestro caso, no conocemos de antemano el tipo de secundaria, se declara un puntero a una clase con el tipo de clase base. Pero en este caso se usa el puntero dinámico.

CCheckVariant* var;

En esta secundaria se debe cargar la palabra clave nueva. Cargar secundaria dependiendo de la variante seleccionada:

// Número de variante
int Variant=5; 
// Dependiendo del número de variante se usarán una de cinco clases secundarias
switch(Variant) 
  {
    case 1: 
       var = new CVariant1;
       break;
    case 2: 
       var = nueva CVariant2;
       break;
    case 3: 
       var = nueva CVariant3;
       break;
    case 4: 
       var = nueva CVariant4;
       break; 
    case 5: 
       var = nueva CVariant5;
       break; 
 }

Comprobar condiciones:

bool rv = var.CheckVariant(1,2);

El resultado de comparar dos valores dependerá de las clases secundarias, incluso si el código que comprueba las condiciones es idéntico.

Puede encontrar este ejemplo en el archivo OOP_sVariant_1.mq5 adjunto a este artículo.

 

Más sobre encapsulación (privada, protegida, pública)

Por el momento está bastante claro con respecto a la sección public. Contiene funciones y variables que deben ser visibles al usuario de la clase (por usuario, nos referimos a un programador, escribiendo programas que usan una clase preparada.) Desde la perspectiva de un usuario de clase no hay diferencia entre las secciones protegida y privada. Las funciones en estas secciones no están disponibles para el usuario:

//+------------------------------------------------------------------+
//|   Class with the protected keyword                               |
//+------------------------------------------------------------------+
class CName1
  {
protected:
   int ProtectedFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(ProtectedFunc(aArg));
     }
  };
//+------------------------------------------------------------------+
//|   Class with the private keyword                                 |
//+------------------------------------------------------------------+
class CName2
  {
private:
   int PrivateFunc(int aArg)
     {
      return(aArg);
     }
public:
   int PublicFunction(int aArg)
     {
      return(PrivateFunc(aArg));
     }
  };

CName1 c1; // Cargar clase con la clave privada
CName2 c2; // Cargar clase con la clave privada
En este ejemplo hay dos clases: CName1 y CName2. Cada clase tiene dos funciones: una está ubicada en la sección public y otra en la sección protected (para la clase CName1) o en la sección private (para la clase CName2). Ambas clases tienen sólo una función de la sección public en la lista de funciones desplegable (imágenes 2 y 3).

Ilustración 2 Funciones de la clase CName1
Ilustración 2. Funciones de la clase CName1

Ilustración 3 Funciones de la clase CName2
Ilustración 3. Funciones de la clase CName2

Puede encontrar este ejemplo en el archivo OOP_sProtPriv_1.mq5 adjunto a este artículo.

Las secciones private y protected determinan la visibilidad de la función de clase base con respecto a sus clases secundarias:

//+------------------------------------------------------------------+
//|   Base class                                                     |
//+------------------------------------------------------------------+
class CBase
  {
protected:
   string ProtectedFunc()
     {
      return("CBase ProtectedFunc");
     }
private:
   string PrivateFunc()
     {
      return("CBase PrivateFunc");
     }
public:
   virtual string PublicFunction()
     {
      return("");
     }
  };
//+------------------------------------------------------------------+
//|   Child class                                                    |
//+------------------------------------------------------------------+
class Class: public CBase
  {
public:
   string PublicFunction()
     {
      // With this line everything compiles correctly
      return(ProtectedFunc());
      // If you will uncomment this line and comment the previous one, there will be a compiler error
      // return(PrivateFunc()); 
     }
  };

En este ejemplo, tenemos clase base llamada CBase y clase secundaria llamada Class. Intente activar la función de clase base ubicada en las secciones protected y private de su clase secundaria. Si activa la función de la sección protected, todo se compila y se ejecuta. Si activa la función de la sección private, se muestra un error de compilador (no se puede activar la función de miembro privado). Esto es, la función de la sección private no es visible a clases secundarias.

La sección protected protege las funciones solo de usuarios de clase y la sección private también las protege de las clases secundarias. En la ilustración 4 se muestra la visibilidad de las funciones de clase (ubicadas en diferentes secciones) de clases secundarias.

Ilustración 4 Visibilidad de las funciones de clase base desde clases secundaria
Ilustración 4. Visibilidad de las funciones de clase base desde clases secundarias
Flechas azules: funciones disponibles; gris: no disponible.

Puede encontrar este ejemplo en el archivo OOP_sProtPriv_2.mq5 adjunto a este artículo.

 

Función virtual por defecto y legado

No todas las funciones virtuales en la clase base deben tener las funciones correspondientes en clases secundarias. Si una clase secundaria tiene la misma función nombre, usará esa misma función, si no, ejecutará el código desde la función virtual de la clase base. Considérelo en el ejemplo.

//+------------------------------------------------------------------+
//|   Base Class                                                     |
//+------------------------------------------------------------------+
class CBase
  {
public:
   virtual string Function()
     {
      string str="";
      str="Function ";
      str=str+"of base ";
      str=str+"class";
      return(str);
     }
  };
//+------------------------------------------------------------------+
//|   Child class 1                                                  |
//+------------------------------------------------------------------+
class Class1: public CBase
  {
public:
   string Function()
     {
      string str="";
      str="Function ";
      str=str+"of child ";
      return(str);
     }
  };
//+------------------------------------------------------------------+
//|   Child class 2                                                  |
//+------------------------------------------------------------------+
class Class2: public CBase
  {

  };
Class1 c1; // Load class 1
Class2 c2; // Load class 2
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   Alert("1: "+c1.Function()); // Running function from Class1
   Alert("2: "+c2.Function()); // Running function from CBase
  }

Aunque la clase Class2 no tiene funciones, es posible activar la función Function() desde la misma. Ejecutará la función desde la clase CBase. La clase Class1 ejecutará su propia función:

void OnStart() 
   {
    Alert("1: " + c1.Function()); // Running function from Class1
    Alert("2: " + c2.Function()); // Running function from CBase
   }

Desde el punto de vista del usuario, al usar una clase secundaria, estarán disponibles todas las funciones de clase base de la sección public. A esto se le llama legado. Si la función de la clase base se declara como virtual, será sustituida por la función de clase secundaria si una clase secundaria tiene una función con este nombre (ilustración 5).

Ilustración 5 Acceso a las funciones por parte de los usuarios de clase
Ilustración 5. Acceso a las funciones por parte de los usuarios de clase

Salvo el caso en que la clase secundaria no tenga funciones que correspondan a las funciones virtuales de la clase base, la clase secundaria puede tener funciones "extra" (funciones sin funciones virtuales del mismo nombre dentro de la clase base). Si carga la clase utilizando puntero al tipo de clase secundaria, estas funciones estarán disponibles. Si carga la clase utilizando puntero al tipo de clase del niño, estas funciones no estarán disponibles (ilustración 6).

Ilustración 6 Visibilidad de las funciones "extra"
Ilustración 6. La visibilidad de la función "extra" (flecha roja) se determina
por el tipo de puntero usado para cargar la clase.

Puede encontrar este ejemplo en el archivo OOP_sDefaultVirtual_1.mq5 adjunto a este artículo.

 

Un poco más sobre cargar clases

Al utilizar las funciones virtuales y, en consecuencia, clases base y clases secundarias, si sabe qué clase secundaria se debe utilizar, puede utilizar un puntero que se corresponda con la clase secundaria:

Class1 c1; // Load class 1
Class2 c2; // Load class 2

Si no se conoce qué clase secundaria se debe usar, use un puntero dinámico al tipo de clase base y cargue la clase usando la clave nueva:

CBase *c; // Dynamic pointer 
void OnStart() 
   {
      c=new Class1; // Load class
      ...

Si usa puntero automático a la clase base

CBase c; // Automatic pointer

la clase base se usará tal cual. Cuando activa sus funciones virtuales, ejecutará el código ubicado dentro de estas funciones. Las funciones virtuales se convierten en funciones ordinarias.  

 

Procesar objetos en funciones

El título de esta sección es autosuficiente. Los punteros a objetos pueden ser transferidos a funciones y luego dentro de la función que puede activar las funciones de objetos. El parámetro de la función se puede declarar con el tipo de clase base. Esto hace la función universal. Un puntero a una clase se puede transferir a la función solo por referencia (indicado por la marca &):

//+------------------------------------------------------------------+
//|   Base Class                                                     |
//+------------------------------------------------------------------+
class CBase
  {
public:
   virtual string Function()
     {
      return("");
     }
  };
//+------------------------------------------------------------------+
//|   Child class 1                                                  |
//+------------------------------------------------------------------+
class Class1: public CBase
  {
public:
   string Function()
     {
      return("Class 1");
     }
  };
//+------------------------------------------------------------------+
//|   Child class 2                                                  |
//+------------------------------------------------------------------+
class Class2: public CBase
  {
public:
   string Function()
     {
      return("Class 2");
     }
  };

Class1 c1; // Load class 1
Class2 c2; // Load class 2
//+------------------------------------------------------------------+
//|   Function to process objects                                    |
//+------------------------------------------------------------------+
void Function(CBase  &c)
  {
   Alert(c.Function());
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
// Process objects using one function.
   Function(c1);
   Function(c2);
  }
Puede encontrar este ejemplo en el archivo OOP_sFunc_1.mq5 adjunto a este artículo.

 

Funciones y métodos, variables y propiedades

Antes, en este artículo, usamos la palabra "función". Pero en OOP, en lugar de la palabra "función", los programadores a menudo usan la palabra "método". Si mira a la clase desde la perspectiva de un programador escribiendo una clase, todas las funciones siguen siendo funciones. Si mira a la clase desde la perspectiva de un programador usando una clase preparada, entonces las funciones de la interfaz de la clase ubicadas en la sección public (disponibles en la lista desplegable tras escribir un punto) son llamadas métodos.

Además de los métodos, la interfaz de clase puede incluir propiedades de clase. La sección public puede incluir no solo funciones, sino también variables (incluyendo arrays).

class CMethodsAndProperties 
   {
    public:
        int               Property1; // Property 1
        int               Property2; // Property 2
        void Function1() 
           {

            //...
            return;
           }
        void Function2() 
           {
            //...
            return;
           }
   };

Estas variables serán llamadas propiedades de clase y también estarán disponibles en la lista desplegable (ilustración 7).

Ilustración 7 Métodos y propiedades de clase en una lista
Ilustración 7. Métodos y propiedades de clase en una lista

Puede usar estas propiedades de la misma manera que las variables:

void OnStart() 
   {
    c.Property1 = 1; // Set property 1
    c.Property2 = 2; // Set property 2

    // Read properties
    Alert("Property1 = " + IntegerToString(c.Property1) + ", Property2 = " + IntegerToString(c.Property2));
   }

Puede encontrar este ejemplo en el archivo OOP_sMethodsAndProperties.mq5 adjunto a este artículo.

 

Estructuras de datos

Las estructuras de datos son similares a las clases, sólo que algo más sencillas. Aunque puedes expresarlo así: las clases son como estructuras de datos, pero bastante más complicadas. La diferencia es que las estructuras de datos pueden incluir sólo variables. Con respecto a esto, no hay necesidad de dividirlas en las secciones public, private y protected. Todos los contenidos de la estructura ya están ubicados en la sección public. La estructura de datos empieza con la palabra struct, seguida por el nombre de la estructura y dentro de los corchetes declara las variables

struct Str1 
   {
    int    IntVar;
    int    IntArr[];
    double DblVar[];
    double DblArr[];
   };

Para utilizar una estructura, debe ser declarada como una variable, pero en lugar del tipo de variable utilice el nombre de la estructura.

Str1 s1;

También puede declarar un array de estructuras:

Str1 sar1[];

Las estructuras puede incluir no sólo variables y arrays, sino también otras estructuras:

struct Str2 
   {
    int    IntVar;
    int    IntArr[];
    double DblVar[];
    double DblArr[];
    Str1   Str;
   };

En este caso, para activar variables desde la estructura 1 que es parte de la estructura 2, debe usar dos puntos:

s2.Str.IntVar=1;

Puede encontrar este ejemplo en el archivo OOP_Struct.mq5 adjunto a este artículo.

Las clases pueden incluir no sólo variables, sino también estructuras.

 

Conclusión

Revisemos los puntos principales de la programación orientada a objetos y momentos importantes a tener en cuenta:

1. La clase se crea usando la palabra clave class, seguida del nombre de la clase y a continuación entre corchetes el código de clase escrito en tres secciones.

class CName 
  {
private:

protected:

public:
  };

2. Las funciones y variables de clase se pueden ubicar en una de las tres secciones: private, protected y public. Las funciones y variables de la sección private están disponibles sólo dentro de la clase. Las funciones y variables de la sección protected están disponibles dentro de las clases secundarias. Las funciones y variables de la sección public están disponibles para todos.

3. Las funciones de la clase pueden ubicarse dentro o fuera de la clase. Si coloca las funciones fuera de la clase debe identificar a qué clase pertenecen al colocar el nombre de la clase y los dos puntos antes del nombre de cada función:

void ClassName::FunctionName() { ... }

4. Las clases se pueden cargar usando tanto el puntero automático como dinámico. Al usar el puntero dinámico, la clase se debe cargar usando la palabra clave nueva. En este caso, tiene que eliminar un objeto usando la palabra clave eliminar al terminar su programa.

5. Para decir que la clase secundaria pertenece a la clase base, tiene que añadir el nombre de la clase base después del nombre de una clase secundaria.

class Class : public CBase { ... }

6. No puede asignar variables con valores durante la inicialización de la clase. Puede asignar valores mientras ejecuta alguna función (más comunmente, el constructor).

7. Las funciones virtuales se declaran usando la palabra clave virtual. Si la función secundaria tiene una función con el mismo nombre, ejecuta esta misma función, si no, ejecuta una función virtual de la clase base.

8. Los punteros a clases se pueden transferir a funciones. Puede declarar los parámetros de la función con el tipo de clase base, por lo que puede transferir un puntero a cualquier clase secundaria en la función.

9. La sección public no tiene sólo funciones (métodos) sino también variables (propiedades).

10. Las estructuras pueden incluir arrays y otras estructuras.

 

Lista de archivos adjuntos

Después de experimentar con estos archivos, puede eliminarlos todos excepto OOP_CDeleteOrder_2.mqh y OOP_sDeleteOrders_3.mq5. Los archivos OOP_CDeleteOrder_2.mqh y OOP_sDeleteOrders_3.mq5 pueden ser útiles en la programación práctica.