
Observador de Connexus (Parte 8): Cómo agregar un observador de solicitudes
Introducción
Este artículo es la continuación de una serie de artículos donde construiremos una biblioteca llamada Connexus. En el primer artículo, comprendimos el funcionamiento básico de la función WebRequest, entendimos cada uno de sus parámetros y también creamos un código de ejemplo que muestra el uso de esta función y sus dificultades. En el último artículo, creamos la capa cliente, una clase sencilla e intuitiva encargada de enviar solicitudes, recibir un objeto de solicitud (CHttpRequest) y devolver una respuesta (CHttpResponse) que contiene información sobre la solicitud, como el código de estado, la duración, el cuerpo y el encabezado de la respuesta. También hemos creado una separación de la clase con la función WebRequest, lo que hace que la biblioteca sea más flexible, creando una nueva capa llamada CHttpTransport.
En este octavo artículo de la serie, comprenderemos e implementaremos un observador en la biblioteca para facilitar la gestión de múltiples solicitudes por parte del cliente. ¡Vamos!
Sólo para recordarle el estado actual de la biblioteca, aquí está el diagrama actual:
¿Qué es un observador?
Imaginemos al Observador como ese amigo que, medio escondido, simplemente observa desde lejos, prestando atención a todo, pero sin interferir. En el mundo de la programación, el patrón Observador hace algo muy similar: Permite "notificar" a ciertos objetos cuando algo cambia, pero sin necesidad de saber exactamente quién causó el cambio. Es casi como un toque mágico: alguien mueve una pieza y quien la necesita, en ese momento, ya lo sabe. Este patrón es uno de los clásicos para mantener los engranajes de la lógica de negocio y la interfaz trabajando uno al lado del otro. De esta manera, el sistema gana ese aire de fluidez, con varias partes ajustándose automáticamente a los eventos.
La idea surgió para resolver uno de esos molestos problemas que se dan en sistemas muy rígidos, en los que un objeto depende de otro y queda «atascado» a él, sin mucho margen de maniobra. ¿La solución? Desacoplar, hacerlo más ligero. Antes de que el Observador adquiriera nombre y apellidos, los programadores ya buscaban formas de aligerar los sistemas. Fue entonces, allá por 1994, cuando Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides, en su obra Design Patterns: Elements of Reusable Object-Oriented Software (1994) propusieron el Observador como la forma ideal de mantener múltiples objetos actualizados con los cambios de un solo objeto, sin las cadenas de una vinculación fuerte.
¿Por qué utilizar el patrón del Observador?
El Observador es perfecto para cuando necesitamos desacoplamiento, cuando queremos que las cosas sean más independientes entre sí. El sujeto no necesita saber quién lo está mirando, sólo necesita gritar "¡cambio a la vista!" y seguir adelante. También puede ser útil para actualizaciones en tiempo real, los sistemas que necesitan actualizarse instantáneamente, como interfaces interactivas o notificaciones automáticas, son mucho más ágiles con el Observador.
Componentes del patrón del Observador
- Subject: Este es el «propietario de la pieza» cuyo estado cambia y que debe notificar a los observadores dichos cambios. Mantiene una lista de observadores y dispone de métodos para añadir o eliminar a alguien de la lista.
- Observer: Cada observador es como un «oyente», siempre dispuesto a reaccionar ante los cambios del sujeto. Implementa un método de actualización que el sujeto llama cada vez que se produce un cambio.
A continuación, añadiré un diagrama que muestra cómo funciona el patrón observador:
- Clases principales
- Subject: Esta clase mantiene una colección de observadores (observerCollection) y proporciona métodos para gestionar dichos observadores. Su función es notificar a los observadores cada vez que se produce un cambio de estado.
- Métodos:
- registerObserver(observer) : Añade un observador a la colección.
- unregisterObserver(observer) : Elimina un observador de la colección.
- notifyObservers() : Notifica a todos los observadores llamando al método update() de cada observador en la colección de observadores observerCollection.
- Métodos:
- Observer: Esta es una interfaz o clase abstracta que define el método update(). Todas las clases observadoras concretas (concretaciones de Observer) deben implementar este método, que se invoca cuando el sujeto notifica cambios.
- Subject: Esta clase mantiene una colección de observadores (observerCollection) y proporciona métodos para gestionar dichos observadores. Su función es notificar a los observadores cada vez que se produce un cambio de estado.
- Clases concretas
- ConcreteObserverA y ConcreteObserverB: Son implementaciones concretas de la interfaz Observer. Cada uno implementa el método update(), que define la respuesta específica a un cambio en el Subject.
- Relación entre sujeto y observador
- El sujeto mantiene una lista de observadores y les notifica llamando a observer.update() para cada observador de la colección.
- Los observadores concretos reaccionan a los cambios que se producen en el sujeto según su implementación específica en el método update().
¿Cómo será útil esto en la biblioteca Connexus? Utilizaremos este patrón para informar al código cliente cuando se haya enviado una solicitud, cuando se haya recibido una respuesta o incluso cuando se haya generado un error inesperado. Siguiendo este patrón, se informará al cliente de que esto ha ocurrido. Esto facilita el uso de la biblioteca, ya que evita crear condiciones en el código como, por ejemplo, «si se generó un error, haz esto», «si se realizó una solicitud, entonces haz esto», «si se recibió una respuesta, entonces haz esto».
Manos a la obra con el código
Primero mostraré un diagrama que muestra cómo vamos a añadir este patrón en el contexto de la biblioteca:
Entendamos mejor cómo se llevará a cabo la implementación.
- Tenga en cuenta que el diagrama tiene la misma estructura que el diagrama de referencia.
- He añadido dos métodos a los que tendrán acceso los observadores:
- OnSend() → Cuando se envía una solicitud.
- OnRecv() → Cuando se obtiene una respuesta.
- IHttpObserver no será una clase abstracta, sino una interfaz.
Creación de la interfaz IHttpClient
Primero creamos la interfaz IHttpClient en la ruta <Connexus/Interface/IHttpClient.mqh>. Y definimos las dos funciones de notificación.
//+------------------------------------------------------------------+ //| IHttpObserver.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include "../Core/HttpRequest.mqh" #include "../Core/HttpResponse.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ interface IHttpObserver { void OnSend(CHttpRequest *request); void OnRecv(CHttpResponse *response); }; //+------------------------------------------------------------------+
Creación de la lista de observadores en CHttpClient
Añadamos la importación de la interfaz.
#include "../Interface/IHttpObserver.mqh"Ahora creemos una matriz de observadores en el campo privado de la clase. Recuerda que esta matriz debe almacenar punteros, por lo que debemos añadir el símbolo «*» delante del nombre de la variable. También crearemos métodos públicos para añadir, eliminar y notificar a todos los observadores que están almacenados en la matriz.
//+------------------------------------------------------------------+ //| HttpClient.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "HttpRequest.mqh" #include "HttpResponse.mqh" #include "../Constants/HttpMethod.mqh" #include "../Interface/IHttpTransport.mqh" #include "../Interface/IHttpObserver.mqh" #include "HttpTransport.mqh" //+------------------------------------------------------------------+ //| class : CHttpClient | //| | //| [PROPERTY] | //| Name : CHttpClient | //| Heritage : No heritage | //| Description : Class responsible for linking the request and | //| response object with the transport layer. | //| | //+------------------------------------------------------------------+ class CHttpClient { private: IHttpObserver *m_observers[]; // Array of observers public: //--- Observers void RegisterObserver(IHttpObserver *observer); void UnregisterObserver(IHttpObserver *observer); void OnSendNotifyObservers(CHttpRequest *request); void OnRecvNotifyObservers(CHttpResponse *response); }; //+------------------------------------------------------------------+ //| Add observer pointer to observer list | //+------------------------------------------------------------------+ void CHttpClient::RegisterObserver(IHttpObserver *observer) { int size = ArraySize(m_observers); ArrayResize(m_observers,size+1); m_observers[size] = observer; } //+------------------------------------------------------------------+ //| Remove observer pointer to observer list | //+------------------------------------------------------------------+ void CHttpClient::UnregisterObserver(IHttpObserver *observer) { int size = ArraySize(m_observers); for(int i=0;i<size;i++) { if(GetPointer(m_observers[i]) == GetPointer(observer)) { ArrayRemove(m_observers,i,1); break; } } } //+------------------------------------------------------------------+ //| Notifies observers that a request has been made | //+------------------------------------------------------------------+ void CHttpClient::OnSendNotifyObservers(CHttpRequest *request) { int size = ArraySize(m_observers); for(int i=0;i<size;i++) { m_observers[i].OnSend(request); } } //+------------------------------------------------------------------+ //| Notifies observers that a response has been received | //+------------------------------------------------------------------+ void CHttpClient::OnRecvNotifyObservers(CHttpResponse *response) { int size = ArraySize(m_observers); for(int i=0;i<size;i++) { m_observers[i].OnRecv(response); } } //+------------------------------------------------------------------+Por último, llamamos a la función de notificación dentro de la función que envía la solicitud para que los observadores sean realmente informados:
//+------------------------------------------------------------------+ //| class : CHttpClient | //| | //| [PROPERTY] | //| Name : CHttpClient | //| Heritage : No heritage | //| Description : Class responsible for linking the request and | //| response object with the transport layer. | //| | //+------------------------------------------------------------------+ class CHttpClient { private: IHttpObserver *m_observers[]; // Array of observers public: //--- Basis function bool Send(CHttpRequest &request, CHttpResponse &response); }; //+------------------------------------------------------------------+ //| Basis function | //+------------------------------------------------------------------+ bool CHttpClient::Send(CHttpRequest &request, CHttpResponse &response) { //--- Request uchar body_request[]; request.Body().GetAsBinary(body_request); //--- Response uchar body_response[]; string headers_response; //--- Notify observer of request this.OnSendNotifyObservers(GetPointer(request)); //--- Send ulong start = GetMicrosecondCount(); int status_code = m_transport.Request(request.Method().GetMethodDescription(),request.Url().FullUrl(),request.Header().Serialize(),request.Timeout(),body_request,body_response,headers_response); ulong end = GetMicrosecondCount(); //--- Notify observer of response this.OnRecvNotifyObservers(GetPointer(response)); //--- Add data in Response response.Clear(); response.Duration((end-start)/1000); response.StatusCode() = status_code; response.Body().AddBinary(body_response); response.Header().Parse(headers_response); //--- Return is success return(response.StatusCode().IsSuccess()); } //+------------------------------------------------------------------+
El trabajo está hecho, y es más sencillo de lo que parece cuando escribimos el código, ¿no? Con esto, hemos completado toda la implementación dentro de la biblioteca. Necesitamos crear los observadores, es decir, las clases concretas que implementan IHttpObserver. Lo haremos en el siguiente tema, las pruebas.
Pruebas
Ahora todo lo que necesitamos hacer es usar la biblioteca. Para ello, crearé un nuevo archivo de prueba llamado TestObserver.mq5, en la ruta <Experts/Connexus/Tests/TestObserver.mq5>. Importaremos la biblioteca y dejaremos solo el evento OnInit().
//+------------------------------------------------------------------+ //| TestObserver.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include <Connexus/Core/HttpClient.mqh> //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Justo debajo de la importación, crearé una clase concreta que implemente la interfaz IHttpClient, que simplemente imprimirá en la consola del terminal los datos que se enviaron y recibieron utilizando la biblioteca:
//+------------------------------------------------------------------+ //| TestObserver.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include <Connexus/Core/HttpClient.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CMyObserver : public IHttpObserver { public: CMyObserver(void); ~CMyObserver(void); void OnSend(CHttpRequest *request); void OnRecv(CHttpResponse *response); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::CMyObserver(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::~CMyObserver(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::OnSend(CHttpRequest *request) { Print("-----------------------------------------------"); Print("Order sent notification received in CMyObserver"); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::OnRecv(CHttpResponse *response) { Print("-----------------------------------------------"); Print("Response notification received in CMyObserver"); } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create objects CHttpRequest request; CHttpResponse response; CHttpClient client; CMyObserver *my_observer = new CMyObserver(); //--- Configure request request.Method() = HTTP_METHOD_GET; request.Url().Parse("https://httpbin.org/get"); //--- Adding observer client.RegisterObserver(my_observer); //--- Send client.Send(request,response); //--- Delete pointer delete my_observer; return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Al ejecutar esto en el gráfico, obtenemos este resultado:
Esto demuestra que las funciones de la clase CMyObserver se invocaron dentro de la biblioteca, lo que cambia todo. Completamos la biblioteca con el objetivo principal cumplido: que sea flexible.
Lo más interesante es que podemos tener varios observadores en diferentes partes del código. Si tenemos un EA dividido en varias clases, podemos hacer que cada una de estas clases cree una implementación de IHttpObserver, ¡y listo! Se nos notificará tan pronto como se envíe la solicitud o se reciba una respuesta.
Ahora, con estas inclusiones de observadores, este es el diagrama actual de la biblioteca:
Refactorización de las carpetas
Actualmente el directorio de todos los archivos de la biblioteca se ve así:
|--- Connexus |--- |--- Constants |--- |--- |--- HttpMethod.mqh |--- |--- |--- HttpStatusCode.mqh |--- |--- Core |--- |--- |--- HttpClient.mqh |--- |--- |--- HttpRequest.mqh |--- |--- |--- HttpResponse.mqh |--- |--- |--- HttpTransport.mqh |--- |--- Data |--- |--- |--- Json.mqh |--- |--- Header |--- |--- |--- HttpBody.mqh |--- |--- |--- HttpHeader.mqh |--- |--- Interface |--- |--- |--- IHttpObserver.mqh |--- |--- |--- IHttpTransport.mqh |--- |--- URL |--- |--- |--- QueryParam.mqh |--- |--- |--- URL.mqh
Haremos dos ajustes: Incluiremos los archivos de la carpeta URL en la carpeta Data y los renombraremos a Utils, simplificando así ambas carpetas que contienen archivos con propósitos similares. También agregaremos la carpeta de interfaces dentro de la carpeta Core, ya que las interfaces son parte del núcleo de la biblioteca. Al final, la estructura de carpetas de la biblioteca se ve así:
|--- Connexus |--- |--- Constants |--- |--- |--- HttpMethod.mqh |--- |--- |--- HttpStatusCode.mqh |--- |--- Core |--- |--- |--- Interface |--- |--- |--- |--- IHttpObserver.mqh |--- |--- |--- |--- IHttpTransport.mqh |--- |--- |--- HttpClient.mqh |--- |--- |--- HttpRequest.mqh |--- |--- |--- HttpResponse.mqh |--- |--- |--- HttpTransport.mqh |--- |--- Utils |--- |--- |--- Json.mqh |--- |--- |--- QueryParam.mqh |--- |--- |--- URL.mqh |--- |--- Header |--- |--- |--- HttpBody.mqh |--- |--- |--- HttpHeader.mqh
Cambiar el nombre de algunos métodos
Cuando se trata de escribir código que sea fácil de entender, mantener y mejorar, adoptar un estilo de codificación estándar marca la diferencia. Tener un estándar coherente a la hora de crear bibliotecas va mucho más allá de la simple estética; aporta claridad, previsibilidad y una base sólida para cualquiera que vaya a utilizar o colaborar con el código, hoy o en el futuro. Este estilo uniforme no es solo una cuestión de organización, sino una inversión en calidad, solidez y crecimiento saludable de la biblioteca a lo largo del tiempo. Y aunque al principio pueda parecer solo un detalle, acaba siendo el hilo conductor que hace que el código sea seguro y esté más preparado para evolucionar.
¿Por qué es esencial tener un estilo estándar?
- Consistencia y legibilidad: Un código bien estructurado con un estilo uniforme hace que la lectura sea más fluida y comprensible para cualquier desarrollador. Con un estándar bien definido, las personas no necesitan perder tiempo descifrando variaciones o inconsistencias; en cambio, pueden centrarse en lo que realmente importa: la lógica del código. Aspectos como el espaciado, la sangría y la nomenclatura son detalles que, en conjunto, crean una experiencia más intuitiva y sencilla. Todo está alineado, lo que facilita la navegación y reduce los obstáculos causados por estilos variados y desconectados.
- Facilidad de mantenimiento y ampliación: Las bibliotecas rara vez se quedan estancadas en el tiempo; surgen nuevos retos y es natural que necesiten ajustes. Con un estilo de codificación estandarizado, el mantenimiento se vuelve más sencillo y menos propenso a errores. Esto no solo ahorra tiempo a la hora de solucionar problemas, sino que también facilita que los nuevos desarrolladores comprendan rápidamente el código y colaboren de manera eficiente. Y, por supuesto, una biblioteca que está bien estructurada desde el principio es mucho más fácil de escalar, ya que cada nueva característica encuentra un entorno predecible y organizado en el que integrarse.
Dicho esto, definamos algunas normas en el código, principalmente en lo que respecta a la denominación de las funciones. Ya se han aplicado otras normas, como por ejemplo:
- Todas las clases utilizan el prefijo «C» delante del nombre.
- Todas las interfaces utilizan el prefijo «I» antes del nombre.
- Las variables privadas utilizan el prefijo «m_».
- Los métodos siempre deben comenzar con mayúscula.
- Los valores ENUM deben escribirse en mayúsculas.
Todas estas normas ya se han aplicado en la biblioteca durante el desarrollo, pero añadiremos otras, como por ejemplo:
- Los métodos para establecer/obtener atributos de una clase deben utilizar el prefijo Get o Set.
- Los métodos que devuelven el tamaño de la matriz deben llamarse «Size».
- Los métodos que restablecen los atributos de clase deben llamarse «Clear».
- Los métodos para convertir a cadena deben llamarse «ToString».
- Evite la redundancia de nombres en las clases de contexto. Por ejemplo, la clase CQueryParam tiene el método AddParam(), que no tiene sentido. Lo ideal sería simplemente Add(), ya que ya estamos en el contexto de los parámetros.
Dicho esto, no voy a enumerar todos los métodos de la biblioteca que voy a renombrar, ni voy a proporcionar el código fuente, ya que no voy a cambiar la implementación del método, solo el nombre. Pero con los cambios, dejaré un diagrama a continuación que muestra todas las clases de la biblioteca con los nombres de los métodos actualizados y sus relaciones.
Conclusión
Con este último artículo, concluimos la serie sobre la creación de la biblioteca Connexus, diseñada para simplificar la comunicación HTTP. Fue todo un viaje: repasamos los conceptos básicos, profundizamos en técnicas más avanzadas de diseño y perfeccionamiento del código, y exploramos el patrón Observer para dotar a Connexus de la reactividad que es esencial en sistemas dinámicos y flexibles. Hemos implementado este patrón en la práctica, de modo que las distintas partes de la aplicación puedan reaccionar automáticamente a los cambios, creando una estructura robusta y adaptable.
Además del Observador, organizamos toda la arquitectura de archivos y carpetas, haciendo que el código fuera modular e intuitivo. También hemos renombrado los métodos para aumentar la claridad y hacer que el uso de la biblioteca sea más directo y coherente, detalles que marcan la diferencia cuando se trata de código limpio y mantenimiento a largo plazo.
Connexus se diseñó para que la integración HTTP fuera lo más sencilla e intuitiva posible, y esperamos que esta serie haya mostrado cada punto importante del proceso, revelando las decisiones de diseño que lo hicieron posible. Con este último artículo, espero que Connexus no solo simplifique sus integraciones HTTP, sino que también inspire mejoras continuas. Gracias por acompañarme en este viaje. ¡Que Connexus sea un aliado en tus proyectos!
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16377





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso