
Наблюдатель Connexus (Часть 8): Добавление Request Observer (Наблюдатель запросов)
Введение
Настоящая статья является продолжением серии статей, в которых мы будем создавать библиотеку под названием Connexus. В первой статье мы разобрались с основами работы функции WebRequest, разобрались с каждым из ее параметров, а также создали пример кода, который демонстрирует использование данной функции и связанные с этим трудности. В прошлой статье мы создали клиентский уровень, простой и интуитивно понятный класс, отвечающий за отправку запросов, получение объекта запроса (CHttpRequest) и возврат ответа (CHttpResponse), который содержит информацию о запросе, такую как код состояния, продолжительность, тело и заголовок ответа. Мы также создали разделение класса с помощью функции WebRequest, что сделало библиотеку более гибкой, создав новый уровень под названием CHttpTransport.
В этой восьмой статье серии мы рассмотрим и реализуем Observer в библиотеке, чтобы облегчить управление многочисленными запросами клиента. Поехали!
Просто чтобы напомнить вам о текущем состоянии библиотеки, вот текущая схема:
Что такое Наблюдатель?
Представьте себе Наблюдателя в роли того друга, который, наполовину спрятавшись, просто наблюдает издалека, обращая на все внимание, но не вмешиваясь. В мире программирования паттерн Observer (Наблюдатель) делает нечто очень похожее: он позволяет определенным объектам получать "уведомления", когда что-то меняется, но без необходимости точно знать, кто вызвал это изменение. Это почти как волшебное прикосновение: кто-то перемещает часть, и тот, кому это нужно в данный момент, уже знает об этом. Этот паттерн является одним из классических для обеспечения того, чтобы механизмы бизнес-логики и интерфейс работали бок о бок. Таким образом, система приобретает плавность хода, где некоторые компоненты автоматически подстраиваются под события.
Идея родилась, чтобы решить одну из тех досадных проблем, которые возникают в очень жестких системах, где один объект зависит от другого и "прилипает" к нему, не давая возможности дышать. Решение? Отделить, сделать легче. Еще до того, как Наблюдатель получил имя и фамилию, программисты уже стремились сделать системы легче. Это было еще в 1994 году в работе авторов **Эриха Гаммы (Erich Gamma), Ричарда Хелма (Richard Helm), Ральфа Джонсона (Ralph Johnson) и Джона Влиссидеса (John Vlissides)** - «Приёмы объектно-ориентированного проектирования. Па́ттерны проектирования» (Design Patterns: Elements of Reusable Object-Oriented Software) (1994), где они предложили Наблюдатель как идеальный способ держать несколько объектов в курсе изменений, вносимых в один объект, без цепочек строгой привязки.
Зачем использовать паттерн Наблюдатель?
Наблюдатель идеально подходит для тех случаев, когда нам нужна развязка, когда надо сделать вещи более независимыми друг от друга. Субъекту не обязательно знать, кто за ним наблюдает, ему просто нужно крикнуть: "Перемена в поле зрения!" - и двигаться дальше. Это также может быть полезно для обновлений в режиме реального времени **,** системы, требующие мгновенное обновление, такие как интерактивные интерфейсы или автоматические уведомления, гораздо более проворные в работе с Наблюдателем.
Компоненты паттерна Наблюдатель
- Субъект (Subject): Это "владелец части", состояние которого меняется и который должен уведомлять наблюдателей об этих изменениях. Он ведет список наблюдателей и имеет методы для добавления или удаления кого-либо из этого списка.
- Наблюдатель (Observer): Каждый Наблюдатель подобен "слушателю", всегда готовому отреагировать на изменения в Субъекте. Он реализует метод обновления, который Субъект вызывает каждый раз при возникновении изменения.
Ниже я добавлю схему, которая показывает, как работает шаблон наблюдателя:
- Основные классы
- Субъект (Subject): Этот класс поддерживает коллекцию наблюдателей ( observerCollection ) и предоставляет методы для управления этими наблюдателями. Его функция заключается в уведомлении наблюдателей всякий раз при изменении состояния.
- Методы:
- registerObserver(observer) : Добавляет наблюдателя в коллекцию.
- unregisterObserver(observer) : Удаляет наблюдателя из коллекции.
- notifyObservers() : Уведомляет всех наблюдателей, вызывая метод update() для каждого наблюдателя в observerCollection
- Методы:
- Наблюдатель (Observer): Это интерфейс или абстрактный класс, определяющий метод update(). Все конкретные классы наблюдателей (конкреции Наблюдателя) должны реализовывать этот метод, который вызывается, когда Субъект уведомляет об изменениях.
- Субъект (Subject): Этот класс поддерживает коллекцию наблюдателей ( observerCollection ) и предоставляет методы для управления этими наблюдателями. Его функция заключается в уведомлении наблюдателей всякий раз при изменении состояния.
- Конкретные классы
- ConcreteObserverA и ConcreteObserverB: Это конкретные реализации интерфейса Наблюдатель. В каждом из них реализован метод update(), который определяет конкретный ответ на изменение в Субъекте.
- Отношения между Субъектом и Наблюдателем
- Субъект ведет список Наблюдателей и уведомляет их, вызывая observer.update() для каждого наблюдателя в коллекции.
- Конкретные Наблюдатели реагируют на изменения, происходящие в Субъекте, в соответствии с их конкретной реализацией в методе update().
Как это будет полезно в библиотеке Connexus? Мы будем использовать этот паттерн для информирования клиентского кода о том, когда отправлен запрос, когда получен ответ или даже когда была сгенерирована непредвиденная ошибка. Используя этот паттерн, клиент будет проинформирован о том, что это произошло. Это упрощает использование библиотеки, поскольку позволяет избежать создания в коде таких условий, как, например, “если сгенерирована ошибка, сделайте это”, “если сделан запрос, то сделайте это”, “если получен ответ, то сделайте это”.
Рука на коде
Сначала я продемонстрирую схему, которая показывает, как мы собираемся добавить этот паттерн в контекст библиотеки:
Лучше разберемся, как будет проходить реализация
- Обратите внимание, что схема имеет ту же структуру, что и референсная схема
- Я добавил два метода, к которым будут иметь доступ наблюдатели:
- OnSend() → Когда отправлен запрос.
- OnRecv() → Когда получен ответ.
- IHttpObserver будет не абстрактным классом, а скорее интерфейсом.
Создание интерфейса IHttpClient
Сначала создадим интерфейс IHttpClient и его путь будет <Connexus/Interface/IHttpClient.mqh> . И мы определяем две функции уведомления
//+------------------------------------------------------------------+ //| 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); }; //+------------------------------------------------------------------+
Создание списка наблюдателей в CHttpClient
Добавим импорт интерфейса.
#include "../Interface/IHttpObserver.mqh"Теперь создадим массив наблюдателей в частном поле класса. Помните, что в этом массиве должны храниться указатели, поэтому нам нужно добавить "*" перед именем переменной. Также создадим публичные методы для добавления, удаления и уведомления всех наблюдателей, которые хранятся в массиве.
//+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+Наконец, вызываем функцию уведомления внутри функции, которая отправляет запрос, чтобы наблюдатели действительно были проинформированы:
//+------------------------------------------------------------------+ //| 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()); } //+------------------------------------------------------------------+
Работа выполнена, и она проще, чем кажется, когда мы пишем код, не так ли? На этом мы завершили всю реализацию в библиотеке. Нам нужно создать наблюдателей, то есть конкретные классы, которые реализуют IHttpObserver. Мы сделаем это в следующем разделе, посвященном тестам.
Тесты
Всё, что нам нужно сделать, - это использовать библиотеку. Для этого я создам новый тестовый файл под названием TestObserver.mq5, его путь: <Experts/Connexus/Tests/TestObserver.mq5>. Мы импортируем библиотеку и оставим только событие 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); } //+------------------------------------------------------------------+
Сразу после импорта я создам конкретный класс, который реализует интерфейс IHttpClient, он просто выведет на консоли терминала данные, которые были отправлены и получены с использованием библиотеки:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
При отображении этого на графике мы получаем следующий результат:
Это показывает, что функции класса CMyObserver были вызваны внутри библиотеки, это все меняет, мы дополняем библиотеку, достигнув основной цели - сделать ее гибкой.
Самое интересное, что у нас может быть несколько наблюдателей в разных частях кода, если у нас есть советник, который разделен на несколько классов, мы можем заставить каждый из этих классов создать реализацию IHttpObserver и все! Мы будем уведомлены, как только запрос будет отправлен или будет получен ответ.
Теперь, с учетом этих включений наблюдателя, это текущая схема библиотеки:
Рефакторинг папок
В настоящее время каталог всех библиотечных файлов выглядит следующим образом:
|--- 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
Внесем две корректировки: добавим файлы из папки URL в папку Data и переименуем их в Utils, таким образом упростив обе папки, содержащие файлы аналогичного назначения. Мы также добавим папку interfaces в папку Core, поскольку интерфейсы являются частью ядра библиотеки. В итоге структура папок библиотеки выглядит следующим образом:
|--- 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
Переименование некоторых методов
Когда дело доходит до написания кода, который легко понять, поддерживать и улучшать, принятие стандартного стиля кодирования имеет решающее значение. Наличие единого стандарта при создании библиотек выходит далеко за рамки простой эстетики; он обеспечивает ясность, предсказуемость и прочную основу для всех, кто будет использовать код или сотрудничать с ним сегодня или в будущем. Этот единый стиль - не просто вопрос организации; это инвестиции в качество, надежность и здоровый рост библиотеки с течением времени. И хотя на первый взгляд это может показаться простой деталью, в конечном итоге это становится путеводной нитью, которая делает код безопасным и более подготовленным к развитию.
Почему так важно придерживаться стандартного стиля?
- Согласованность и удобочитаемость: Хорошо структурированный код с единым стилем делает чтение более плавным и понятным для любого разработчика. При наличии четко определенного стандарта людям не нужно тратить время на расшифровку вариаций или несоответствий; вместо этого они могут сосредоточиться на том, что действительно важно: на логике кода. Такие аспекты, как интервалы, отступы и названия - это детали, в совокупности создающие более интуитивный и понятный практический опыт. Все согласовано, что облегчает навигацию и уменьшает количество препятствий, возникающих из-за разнообразия и несвязанности стилей.
- Простота обслуживания и расширения: Библиотеки редко стоят на месте; возникают новые задачи, и естественно, что они требуют корректировки. Благодаря стандартизированному стилю кодирования обслуживание становится проще и менее подвержено ошибкам. Это не только экономит время при устранении неполадок, но и облегчает начинающим разработчикам быстрое понимание кода и эффективную совместную работу. И, конечно же, библиотеку, которая с самого начала была хорошо структурирована, гораздо проще масштабировать, поскольку каждая новая функция находит предсказуемую и организованную среду для интеграции.
Тем не менее, давайте определим некоторые стандарты в коде, главным образом в области именования функций. Некоторые другие стандарты уже применены, например:
- Все классы используют префикс “ C ” перед названием
- Все интерфейсы используют префикс “ I ” перед названием
- Частные переменные используют префикс “ m_ ”
- Методы всегда должны начинаться с заглавных букв
- Значения ENUM должны быть записаны в верхнем регистре
Все эти стандарты уже были применены в библиотеке во время разработки, но мы добавим другие, такие как:
- Методы для установки/получения атрибутов класса должны использовать префикс Get или Set
- Методы, возвращающие размер массива, должны иметь имя “Size”
- Методы, которые сбрасывают атрибуты класса, должны иметь имя “Clear”.
- Методы для преобразования в строку должны иметь имя “ ToString ”
- Избегайте избыточности имен в контекстных классах. Например, класс CQueryParam имеет метод AddParam(), который не имеет смысла. Идеальным вариантом было бы просто Add(), поскольку мы уже находимся в контексте параметров.
Тем не менее, я не буду перечислять все методы в библиотеке, которые я переименую, и не буду предоставлять исходный код, поскольку я не меняю реализацию метода, только название. Но с учетом изменений я оставлю ниже схему, на которой показаны все классы в библиотеке с названиями обновленных методов и их взаимосвязями.
Заключение
Этой последней статьей мы завершаем серию статей о создании библиотеки Connexus, предназначенной для упрощения HTTP-взаимодействия. Это было непростое путешествие: мы изучили основы, углубились в более продвинутые методы проектирования и доработки кода, а также изучили паттерн Наблюдатель (Observer), чтобы придать Connexus реактивность, которая необходима в динамичных и гибких системах. Мы реализовали этот паттерн на практике, чтобы различные части приложения могли автоматически реагировать на изменения, создавая надежную и адаптируемую структуру.
В дополнение к Наблюдателю мы организовали всю архитектуру файлов и папок, сделав код модульным и интуитивно понятным. Мы также переименовали методы, чтобы повысить ясность и сделать использование библиотеки более простым и последовательным - детали, которые имеют решающее значение, когда речь заходит о чистоте кода и долгосрочном обслуживании.
Connexus был разработан для того, чтобы сделать интеграцию HTTP максимально простой и интуитивно понятной и надеемся, что в этой серии показаны все важные моменты процесса, раскрывая варианты дизайна, которые сделали это возможным. Я надеюсь, что в этой заключительной статье Connexus не только упростит вашу интеграцию с HTTP, но и вдохновит на постоянные улучшения. Спасибо вам за то, что отправились в это путешествие вместе со мной, пусть Connexus станет союзником в ваших проектах!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/16377





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования