Connexus观察者模式(第8部分):添加一个观察者请求·进阶篇
🔭

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

(2/3)·客户端层已就位,但多请求联动仍靠手工胶水代码?观察者模式让状态变更自动广播

进阶 第 2/3 篇

接上篇,我们继续深挖 Connexus 的客户端层之后如何优雅管理多个并发请求。很多交易者自己写的 EA 把请求发送和界面刷新死绑在一起,改一处逻辑就牵一发动全身。本篇把观察者模式接进库里,让状态变更自己通知订阅者。

「先定义观察者接口再谈请求封装」

做 MT5 的 HTTP 客户端抽象,第一步不是急着写发送逻辑,而是把「通知」这件事用接口钉死。我们在 Connexus/Interface 路径下新建 IHttpClient 相关头文件,实际先落了一个 IHttpObserver 接口,专门负责在请求发出前和响应回来后做回调。 接口只定义了两个纯虚函数:OnSend 接收 CHttpRequest 指针,OnRecv 接收 CHttpResponse 指针。这意味着任何实现了该接口的类,都能在不动底层传输代码的前提下,插进日志、计时或 AIGC 预处理逻辑。 头文件通过 ../Core/HttpRequest.mqh 与 ../Core/HttpResponse.mqh 把请求和响应结构体拉进来,依赖路径相对固定,拷代码时目录层级不能乱。外汇与贵金属 EA 接外部信号源属高风险操作,接口层做好解耦,才能在实盘断网或时延异常时快速替换实现。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 IHttpObserver.mqh |
class=class="str">"cmt">//|                                         Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "../Core/HttpRequest.mqh"
class="macro">#include "../Core/HttpResponse.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
interface IHttpObserver
  {
   class="type">void OnSend(CHttpRequest *request);
   class="type">void OnRecv(CHttpResponse *response);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+

在HTTP客户端里挂观察者数组

给 CHttpClient 加观察者,第一步是把 IHttpObserver 的接口头文件引进来,否则编译器不认指针类型。类的私有段里要放一个观察者数组,元素必须是指针,所以变量名前得带「*」,不然存的是对象副本而非引用。 公开方法只需三个:AddObserver 往数组塞指针、RemoveObserver 按指针移除、NotifyObservers 遍历数组逐个回调。实际触发点放在发送请求的函数尾端,请求一发出就顺手通知所有观察者,逻辑比画类图时想的轻。 目前库内部的观察者机制已经闭环,但还没有任何具体类实现 IHttpObserver。下一步写测试时再去落地这些具体观察者,那时才能看到通知链真正跑起来。

MQL5 / C++
class="macro">#include "../Interface/IHttpObserver.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                    HttpClient.mqh |
class=class="str">"cmt">//|                                                     Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                                  [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "HttpRequest.mqh"
class="macro">#include "HttpResponse.mqh"
class="macro">#include "../Constants/HttpMethod.mqh"
class="macro">#include "../Interface/IHttpTransport.mqh"
class="macro">#include "../Interface/IHttpObserver.mqh"
class="macro">#include "HttpTransport.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class : CHttpClient                                                          |
class=class="str">"cmt">//|                                                                            |
class=class="str">"cmt">//| [PROPERTY]                                                                  |
class=class="str">"cmt">//| Name       : CHttpClient                                                    |
class=class="str">"cmt">//| Heritage   : No heritage                                                    |
class=class="str">"cmt">//| Description : Class responsible for linking the request and                 |
class=class="str">"cmt">//|               response object with the transport layer.                     |
class=class="str">"cmt">//|                                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CHttpClient
  {
class="kw">private:

◍ 给 HTTP 客户端挂观察者

在 MT5 里做行情外挂或 AIGC 信号桥接时,CHttpClient 用观察者数组把『发请求 / 收响应』的动作广播出去,主类只管调度,不耦合具体日志或告警逻辑。 注册观察者就是往 m_observers 数组末尾塞指针:先取当前 size,ArrayResize 扩 1,再赋值。注销时遍历数组,用 GetPointer 比地址,命中就 ArrayRemove 删掉那一个并 break,避免多次误删。 真正广播发生在 OnSendNotifyObservers 和 OnRecvNotifyObservers:两者都先算 size,再 for 循环调每个观察者的 OnSend / OnRecv。这意味着你每挂一个观察者,一次请求或响应就会多一次虚函数调用——挂 10 个观察者,单次收发就多 10 次调用,高频刷新的外汇 EA 里要掂量开销。 下面这段是观察者的注册与通知实现,逐行拆完你就能直接抄进自己的 http 封装类。

MQL5 / C++
  IHttpObserver      *m_observers[];                 class=class="str">"cmt">// Array of observers

class="kw">public:
  class=class="str">"cmt">//--- Observers
  class="type">void                RegisterObserver(IHttpObserver *observer);
  class="type">void                UnregisterObserver(IHttpObserver *observer);
  class="type">void                OnSendNotifyObservers(CHttpRequest *request);
  class="type">void                OnRecvNotifyObservers(CHttpResponse *response);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add observer pointer to observer list                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CHttpClient::RegisterObserver(IHttpObserver *observer)
  {
   class="type">int size = ArraySize(m_observers);
   ArrayResize(m_observers,size+class="num">1);
   m_observers[size] = observer;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Remove observer pointer to observer list                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CHttpClient::UnregisterObserver(IHttpObserver *observer)
  {
   class="type">int size = ArraySize(m_observers);
   for(class="type">int i=class="num">0;i<size;i++)
     {
      if(GetPointer(m_observers[i]) == GetPointer(observer))
        {
         ArrayRemove(m_observers,i,class="num">1);
         class="kw">break;
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Notifies observers that a request has been made                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CHttpClient::OnSendNotifyObservers(CHttpRequest *request)
  {
   class="type">int size = ArraySize(m_observers);
   for(class="type">int i=class="num">0;i<size;i++)
     {
      m_observers[i].OnSend(request);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Notifies observers that a response has been received             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CHttpClient::OnRecvNotifyObservers(CHttpResponse *response)
  {
   class="type">int size = ArraySize(m_observers);
   for(class="type">int i=class="num">0;i<size;i++)
     {
      m_observers[i].OnRecv(response);
     }
  }

「HTTP 客户端怎么把请求和传输层缝起来」

在 MT5 里做行情接口或信号中转,最怕把传输细节散落在策略代码里。下面这个类把请求/响应对象和真正的网络传输解耦,只留一个 Send 方法对外。 class CHttpClient 内部挂了一个 IHttpObserver 指针数组 m_observers[],用于在发送前和接收后通知观察者,方便你插桩记日志或做延迟统计,而不动主流程。 Send 函数的核心动作很直白:先把 request 序列化成二进制 body,调 m_transport.Request 发走,用 GetMicrosecondCount() 掐头尾算耗时;返回后把状态码、响应体、响应头填进 response 对象。注意 response.Duration((end-start)/1000) 把微秒转成了毫秒,这是你排查外汇 API 延迟时最直接的一个数据点。 最后一行 return(response.StatusCode().IsSuccess()) 只认状态码是否成功,不抛异常。实盘接外部报价源时,建议你在观察者里把每次 Duration 落盘,连续超 200ms 的响应可能预示桥接不稳定,贵金属和外汇品种在高波动时段尤甚,杠杆品种风险被放大。

MQL5 / C++
class=class="str">"cmt">//| [PROPERTY]                                                                 |//| Name     : CHttpClient                                                      |//| Heritage : No heritage                                                      |//| Description : Class responsible for linking the request and     |//|              response object with the transport layer.                     |//|                                                                            |//+------------------------------------------------------------------+
class CHttpClient
  {
class="kw">private:
  IHttpObserver      *m_observers[];                     class=class="str">"cmt">// Array of observers
class="kw">public:
  class=class="str">"cmt">//--- Basis function
  class="type">bool               Send(CHttpRequest &request, CHttpResponse &response);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Basis function                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CHttpClient::Send(CHttpRequest &request, CHttpResponse &response)
  {
  class=class="str">"cmt">//--- Request
  class="type">uchar body_request[];
  request.Body().GetAsBinary(body_request);
  class=class="str">"cmt">//--- Response
  class="type">uchar body_response[];
  class="type">class="kw">string headers_response;
  class=class="str">"cmt">//--- Notify observer of request
  this.OnSendNotifyObservers(GetPointer(request));
  class=class="str">"cmt">//--- Send
  class="type">class="kw">ulong start = GetMicrosecondCount();
  class="type">int status_code = m_transport.Request(request.Method().GetMethodDescription(),request.Url().FullUrl(),request.Header().Serialize(),request.Timeout(),body_request,body_response,headers_response);
  class="type">class="kw">ulong end = GetMicrosecondCount();
  class=class="str">"cmt">//--- Notify observer of response
  this.OnRecvNotifyObservers(GetPointer(response));
  class=class="str">"cmt">//--- Add data in Response
  response.Clear();
  response.Duration((end-start)/class="num">1000);
  response.StatusCode() = status_code;
  response.Body().AddBinary(body_response);
  response.Header().Parse(headers_response);
  class=class="str">"cmt">//--- Return is success
  class="kw">return(response.StatusCode().IsSuccess());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

用观察者把 HTTP 调用摊在控制台上

想验证这套 HTTP 客户端库是否真按预期工作,最直接的方法是挂一个观察者把收发数据全打印出来。新建 Experts/Connexus/Tests/TestObserver.mq5,只留 OnInit() 事件,导入 HttpClient.mqh 后实现一个 IHttpClient 的具体类,把库内发送和接收的字节流原样打到终端。 在图表上挂这个测试 EA 跑一次,控制台会显示出 CMyObserver 的函数确实在库内部被触发——这说明库的主要设计目标达成了:对外保持灵活,对内可插拔。 更有用的是,观察者可以多处并存。若一个 EA 被拆成多个类,每个类都能各自实现一个 IHttpObserver,只要任意请求发出或响应回来,相关类都会收到通知,调试分布式逻辑时不用再满屏找 Print。 下面这段是测试文件的骨架,注意 include 路径必须和库安装目录一致,否则编译期就报找不到文件: //+------------------------------------------------------------------+

//TestObserver.mq5
//Copyright 2024, MetaQuotes Ltd.
//[MQL5官方文档]

//+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "[MQL5官方文档] #property version "1.00" //+------------------------------------------------------------------+

//Expert initialization function

//+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+

//TestObserver.mq5
//Copyright 2024, MetaQuotes Ltd.
//[MQL5官方文档]

//+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "[MQL5官方文档] #property version "1.00" //+------------------------------------------------------------------+

//

//+------------------------------------------------------------------+ #include <Connexus/Core/HttpClient.mqh> //+------------------------------------------------------------------+

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestObserver.mq5 |
class=class="str">"cmt">//|                                                  Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Connexus/Core/HttpClient.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---

class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TestObserver.mq5 |
class=class="str">"cmt">//|                                                  Copyright class="num">2024, MetaQuotes Ltd. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Connexus/Core/HttpClient.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用观察者类接管 HTTP 收发日志

在 MT5 里做外部行情或信号接口调用,光靠主流程打印不够用。把收发动作抽成一个继承 IHttpObserver 的观察者类,能在不改动请求逻辑的前提下,单独截获每一次发出与返回。 下面这段类定义只声明了构造、析构和 OnSend / OnRecv 两个回调。OnSend 里打印一条分隔线和 'Order sent notification received in CMyObserver',OnRecv 同理打印响应到达提示,实际跑起来你会在专家日志里看到每次请求前后各多两段固定标记。 初始化函数里先 new 一个 CMyObserver,给 request 设成 HTTP_METHOD_GET 并解析 'https://httpbin.org/get',随后 client.RegisterObserver(my_observer) 把观察者挂上 HttpClient。这样后续所有经该 client 的流量都会触发观察者的打印,方便你确认外汇或贵金属接口是否真把单子/查询送出去了——这类跨网络调用延迟和失败率都不低,属高风险环节,日志留痕是排查的第一步。

MQL5 / C++
class CMyObserver : class="kw">public IHttpObserver
  {
class="kw">public:
                      CMyObserver(class="type">void);
                     ~CMyObserver(class="type">void);
  
   class="type">void               OnSend(CHttpRequest *request);
   class="type">void               OnRecv(CHttpResponse *response);
  };

class="type">void CMyObserver::CMyObserver(class="type">void)
  {
  }

class="type">void CMyObserver::~CMyObserver(class="type">void)
  {
  }

class="type">void CMyObserver::OnSend(CHttpRequest *request)
  {
   Print("-----------------------------------------------");
   Print("Order sent notification received in CMyObserver");
  }

class="type">void CMyObserver::OnRecv(CHttpResponse *response)
  {
   Print("-----------------------------------------------");
   Print("Response notification received in CMyObserver");
  }

class="type">int OnInit()
  {
   class=class="str">"cmt">//--- Create objects
   CHttpRequest request;
   CHttpResponse response;
   CHttpClient client;
   CMyObserver *my_observer = new CMyObserver();

   class=class="str">"cmt">//--- Configure request
   request.Method() = HTTP_METHOD_GET;
   request.Url().Parse("https:class=class="str">"cmt">//httpbin.org/get");

   class=class="str">"cmt">//--- Adding observer
   client.RegisterObserver(my_observer);
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到请求状态与观察者联动的实时视图,把重复劳动交给小布,你专注决策。

常见问题

事件驱动多依赖终端内置事件,观察者模式是你自己在类层维护订阅列表,不依赖图表事件,更适合库内部解耦。
通知本身是同步循环调用,观察者逻辑应保持轻量;重活建议抛给异步或缓存,否则可能拉高延迟。
可以,小布盯盘的品种页能展示外部库请求状态与订阅变化,你不必自己写日志面板。
通过接口隔离,主体只调用 Update,新增观察者不影响原有发送逻辑,扩展时不用改客户端层。
实时行情下通知链异常可能导致漏单或重复发单,概率上建议先在模拟盘验证,外汇贵金属本身波动大、风险高。