Connexus观察者模式(第8部分):添加一个观察者请求·进阶篇
(2/3)·客户端层已就位,但多请求联动仍靠手工胶水代码?观察者模式让状态变更自动广播
接上篇,我们继续深挖 Connexus 的客户端层之后如何优雅管理多个并发请求。很多交易者自己写的 EA 把请求发送和界面刷新死绑在一起,改一处逻辑就牵一发动全身。本篇把观察者模式接进库里,让状态变更自己通知订阅者。
「先定义观察者接口再谈请求封装」
做 MT5 的 HTTP 客户端抽象,第一步不是急着写发送逻辑,而是把「通知」这件事用接口钉死。我们在 Connexus/Interface 路径下新建 IHttpClient 相关头文件,实际先落了一个 IHttpObserver 接口,专门负责在请求发出前和响应回来后做回调。 接口只定义了两个纯虚函数:OnSend 接收 CHttpRequest 指针,OnRecv 接收 CHttpResponse 指针。这意味着任何实现了该接口的类,都能在不动底层传输代码的前提下,插进日志、计时或 AIGC 预处理逻辑。 头文件通过 ../Core/HttpRequest.mqh 与 ../Core/HttpResponse.mqh 把请求和响应结构体拉进来,依赖路径相对固定,拷代码时目录层级不能乱。外汇与贵金属 EA 接外部信号源属高风险操作,接口层做好解耦,才能在实盘断网或时延异常时快速替换实现。
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。下一步写测试时再去落地这些具体观察者,那时才能看到通知链真正跑起来。
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 封装类。
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 的响应可能预示桥接不稳定,贵金属和外汇品种在高波动时段尤甚,杠杆品种风险被放大。
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> //+------------------------------------------------------------------+
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 的流量都会触发观察者的打印,方便你确认外汇或贵金属接口是否真把单子/查询送出去了——这类跨网络调用延迟和失败率都不低,属高风险环节,日志留痕是排查的第一步。
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);