English Русский Español Deutsch 日本語
preview
Connexus观察者模式(第8部分):添加一个观察者请求

Connexus观察者模式(第8部分):添加一个观察者请求

MetaTrader 5示例 |
176 0
joaopedrodev
joaopedrodev

概述

本文是该系列文章的延续,我们将构建一个名为Connexus的库。在第一篇文章中,我们深入理解了WebRequest函数的基础操作,剖析了其每个参数的用途,并编写了示例代码来演示该函数的使用方法及其潜在难点。在上一篇文章中,我们构建了客户端层——一个简洁直观的类,负责发送请求、接收请求对象(CHttpRequest),并返回包含请求信息的响应对象(CHttpResponse),如状态码、耗时、响应体及响应头等。同时,我们将该类与WebRequest函数解耦,通过引入名为CHttpTransport的新层,显著提升了库的灵活性。

在本系列第八篇文章中,我们将探讨并实现库中的观察者模式,以优化客户端对多请求的管理能力。让我们开始吧!

为帮助您回顾库的当前架构,以下是最新类图:

示意图1


什么是观察者模式?

可将观察者想象为一位默默观察的朋友:他隐于暗处,洞悉一切,却从不干预。在编程领域,观察者模式实现了类似的功能:当对象状态发生变化时,系统能自动通知所有订阅者,而无需通知方知晓具体接收者是谁。这就犹如魔法般高效:某组件触发变更后,所有依赖方会即时感知并响应。作为经典设计模式之一,它完美协调了业务逻辑与界面层的同步更新。使系统各组件能基于事件自动调整,从而呈现出流畅的运行体验。

这一设计理念的诞生,旨在解决僵化系统中一个令人头疼的顽疾:当某个对象深度依赖另一对象时,二者会形成强耦合关系,导致系统缺乏灵活性与扩展空间。有什么解决方案?解耦,让架构更轻盈。早在观察者模式被正式命名之前,开发者们就已探索系统轻量化的实现路径。1994年,Erich GammaRichard HelmRalph JohnsonJohn Vlissides在著作《Design Patterns: Elements of Reusable Object-Oriented Software (1994)》中,首次将观察者模式系统化,提出其可作为打破强绑定桎梏、实现多对象同步更新的理想方案。


为什么使用观察者模式?

观察者模式非常适合在我们需要解耦时使用,当我们希望让对象更加独立时。主体(Subject)不需要知道谁在观察,只需广播变更通知“状态已更新!”然后继续执行。对于实时更新,观察者模式也非常有用。需要即时更新的系统,例如交互式界面或自动通知,使用观察者模式会更加敏捷。


观察者模式的组成部分

  1. 主体(Subject):这是“所有者”,其状态发生变化,并需要通知观察者这些变化。其维护一个观察者列表,并提供添加或删除观察者的方法。
  2. 观察者(Observer):每个观察者就像一个“监听者”,随时准备对主体的变化做出反应。其实现了一个更新方法,主体在每次发生变化时都会调用这个方法。

我将在下面添加一个图表,展示观察者模式的工作原理:

示意图 2

  1. 主类
    • 主体(Subject):该类维护一个观察者集合(observerCollection),并提供管理这些观察者的方法。其功能是在状态发生变化时通知观察者。
      • 方法:
        • registerObserver(observer):将一个观察者添加到集合中。
        • unregisterObserver(observer):从集合中移除一个观察者。
        • notifyObservers():通过调用observerCollection中每个观察者的update()方法来通知所有观察者。
    • 观察者(Observer):这是一个定义了update()方法的接口或抽象类。所有具体的观察者类(Observer的具体化)都必须实现这个方法,当Subject通知变化时会调用该方法。
  2. 具体类
    • ConcreteObserverAConcreteObserverB:这些是Observer接口的具体实现。每个类都实现了update()方法,该方法定义了对Subject变化的具体响应。
  3. Subject与Observer之间的关系
    • Subject维护一个Observer列表,并通过调用集合中每个观察者的observer.update()来通知他们。
    • 具体的Observers根据其在update()方法中的具体实现,对Subject中发生的变化做出反应。

这在Connexus库中将如何发挥作用?我们将使用这种模式来通知客户端代码何时发送了请求、何时接收到了响应,甚至是何时生成了意外错误。使用这种模式,客户端将被告知这些事件的发生。这使得使用库变得更加容易,因为它避免了在代码中创建诸如“如果生成了错误,则执行此操作”、“如果发送了请求,则执行此操作”、“如果接收到了响应,则执行此操作”等条件语句。


动手编码

首先,我将展示一张图表,呈现我们如何在库中添加这种模式:

示意图 3

让我们更好地理解实现方式。

  1. 请注意,该图表的结构与参考图表相同。
  2. 我添加了两个观察者可以访问的方法:
  3. OnSend() → 当发送请求时。
  4. OnRecv() → 当获得响应时。
  5. IHttpObserver不会是一个抽象类,而是一个接口。


创建IHttpClient接口

首先,我们在路径<Connexus/Interface/IHttpClient.mqh>处创建IHttpClient接口。并且定义两个通知函数。

//+------------------------------------------------------------------+
//|                                                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类的函数在库内部被调用,这样改变了整个局面。我们完成了库的主要目标,使其变得灵活。

最有趣的部分是,我们可以在代码的不同部分拥有多个观察者。如果有一个被分成多个类的EA,那么我们可以让每个类都创建一个IHttpObserver的实现,就是这样!只要请求被发送或接收响应,我们就会得到通知。

在引入这些观察者模式后,当前库的示意图如下:

示意图4



重构文件夹

目前,所有库文件的目录结构如下:

|--- 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文件夹中的文件移动到数据文件夹中,并将它们重命名为Utils,从而简化了包含类似目的文件的两个文件夹。我们还会在核心文件夹(Core)中添加接口文件夹(interfaces folder),因为接口是库的核心部分。最终,库的文件夹结构如下所示:

|--- 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_ ”
  • 方法命名:首字母必须大写
  • 枚举值:必须全部大写

以上规范已在库中全面应用,现补充以下规则:

  • 属性访问方法:必须使用Get/Set前缀
  • 数组长度方法:统一命名为“ Size ”
  • 属性重置方法:统一命名为“ Clear ”
  • 字符串转换方法:统一命名为“ ToString ”
  • 避免命名冗余:CQueryParam类中的AddParam()方法存在语义重复。应当直接命名为Add(),因当前内容已明确为参数操作。

鉴于此,我不会列出我将重命名的库中的所有方法,也不会提供源代码,因为我会不改变方法的实现,而只是改变名称。但随着这些更改,我将在下方提供一张图表,展示库中所有类以及更新后的方法名称及其关系。

示意图5


结论

随着本文的发布,我们完成了关于创建Connexus库的系列文章。这个库旨在简化HTTP通信。这是一段相当长的旅程:我们从基础开始,深入探讨了更先进的设计和代码优化技巧,并研究了观察者模式,为Connexus赋予了动态和灵活系统中必不可少的响应性。我们还编码实现了这种模式,使得应用程序的各个部分能够自动对变化做出反应,从而创建了一个强大且适应性强的结构。

除了引入观察者模式外,我们还对整体文件与文件夹的结构进行了重构,使代码实现模块化设计且逻辑清晰直观。我们还对方法命名进行了优化,以提升代码的可读性,使库的使用方式更直观统一,这些细节对于实现代码整洁和长期维护而言至关重要。

Connexus的设计目标是让HTTP集成尽可能简单和直观,我们希望该系列文章展示过程中的每一个重要环节,并揭示实现这一目标的设计选择。通过本文,我们希望Connexus不仅能够简化您的HTTP集成,还能激发持续改进的灵感。感谢您与我一起踏上这段旅程,愿Connexus成为您项目中的得力助手!

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16377

从基础到中级:数组和字符串(二) 从基础到中级:数组和字符串(二)
在本文中,我将展示,尽管我们仍处于编程的一个非常基本的阶段,但我们已经可以实现一些有趣的应用程序。在这种情况下,我们将创建一个相当简单的密码生成器。通过这种方式,我们将能够应用到目前为止已经解释过的一些概念。此外,我们将研究如何为一些具体问题制定解决方案。
原子轨道搜索(AOS)算法:改进与拓展 原子轨道搜索(AOS)算法:改进与拓展
在本文的第二部分,我们将继续开发一种改进版的原子轨道搜索(AOS)算法,重点聚焦于特定操作符的优化设计,以提升算法的效率和适应性。在分析了该算法的基本原理和运行机制之后,我们将探讨提升其性能以及分析复杂解空间能力的方法,并提出新的思路以扩展其作为优化工具的功能。
基于MQL5的自动化交易策略(第一部分):Profitunity系统(比尔·威廉姆斯的《交易混沌》) 基于MQL5的自动化交易策略(第一部分):Profitunity系统(比尔·威廉姆斯的《交易混沌》)
在本文中,我们研究了比尔·威廉姆斯(Bill Williams)的Profitunity系统,深入剖析其核心组成部分以及在混沌市场中独特的交易方法。我们指导读者在MQL5中实现该系统,专注于自动化关键指标和入场/出场信号。最后,我们对策略进行测试和优化,提供其在不同市场环境下的表现。
Connexus客户端(第七部分):添加客户端层 Connexus客户端(第七部分):添加客户端层
在本文中,我们将继续开发connexus库。在本章节中,我们将构建CHttpClient类,该类负责发送请求并接收指令。我们还将介绍模拟对象(mocks)的概念,让该库与WebRequest函数解耦,从而为用户提供更强大的灵活性。