Connexus观察者模式(第8部分):添加一个观察者请求
概述
本文是该系列文章的延续,我们将构建一个名为Connexus的库。在第一篇文章中,我们深入理解了WebRequest函数的基础操作,剖析了其每个参数的用途,并编写了示例代码来演示该函数的使用方法及其潜在难点。在上一篇文章中,我们构建了客户端层——一个简洁直观的类,负责发送请求、接收请求对象(CHttpRequest),并返回包含请求信息的响应对象(CHttpResponse),如状态码、耗时、响应体及响应头等。同时,我们将该类与WebRequest函数解耦,通过引入名为CHttpTransport的新层,显著提升了库的灵活性。
在本系列第八篇文章中,我们将探讨并实现库中的观察者模式,以优化客户端对多请求的管理能力。让我们开始吧!
为帮助您回顾库的当前架构,以下是最新类图:

什么是观察者模式?
可将观察者想象为一位默默观察的朋友:他隐于暗处,洞悉一切,却从不干预。在编程领域,观察者模式实现了类似的功能:当对象状态发生变化时,系统能自动通知所有订阅者,而无需通知方知晓具体接收者是谁。这就犹如魔法般高效:某组件触发变更后,所有依赖方会即时感知并响应。作为经典设计模式之一,它完美协调了业务逻辑与界面层的同步更新。使系统各组件能基于事件自动调整,从而呈现出流畅的运行体验。
这一设计理念的诞生,旨在解决僵化系统中一个令人头疼的顽疾:当某个对象深度依赖另一对象时,二者会形成强耦合关系,导致系统缺乏灵活性与扩展空间。有什么解决方案?解耦,让架构更轻盈。早在观察者模式被正式命名之前,开发者们就已探索系统轻量化的实现路径。1994年,Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides在著作《Design Patterns: Elements of Reusable Object-Oriented Software (1994)》中,首次将观察者模式系统化,提出其可作为打破强绑定桎梏、实现多对象同步更新的理想方案。
为什么使用观察者模式?
观察者模式非常适合在我们需要解耦时使用,当我们希望让对象更加独立时。主体(Subject)不需要知道谁在观察,只需广播变更通知“状态已更新!”然后继续执行。对于实时更新,观察者模式也非常有用。需要即时更新的系统,例如交互式界面或自动通知,使用观察者模式会更加敏捷。
观察者模式的组成部分
- 主体(Subject):这是“所有者”,其状态发生变化,并需要通知观察者这些变化。其维护一个观察者列表,并提供添加或删除观察者的方法。
- 观察者(Observer):每个观察者就像一个“监听者”,随时准备对主体的变化做出反应。其实现了一个更新方法,主体在每次发生变化时都会调用这个方法。
我将在下面添加一个图表,展示观察者模式的工作原理:

- 主类
- 主体(Subject):该类维护一个观察者集合(observerCollection),并提供管理这些观察者的方法。其功能是在状态发生变化时通知观察者。
- 方法:
- registerObserver(observer):将一个观察者添加到集合中。
- unregisterObserver(observer):从集合中移除一个观察者。
- notifyObservers():通过调用observerCollection中每个观察者的update()方法来通知所有观察者。
- 方法:
- 观察者(Observer):这是一个定义了update()方法的接口或抽象类。所有具体的观察者类(Observer的具体化)都必须实现这个方法,当Subject通知变化时会调用该方法。
- 主体(Subject):该类维护一个观察者集合(observerCollection),并提供管理这些观察者的方法。其功能是在状态发生变化时通知观察者。
- 具体类
- ConcreteObserverA和ConcreteObserverB:这些是Observer接口的具体实现。每个类都实现了update()方法,该方法定义了对Subject变化的具体响应。
- Subject与Observer之间的关系
- Subject维护一个Observer列表,并通过调用集合中每个观察者的observer.update()来通知他们。
- 具体的Observers根据其在update()方法中的具体实现,对Subject中发生的变化做出反应。
这在Connexus库中将如何发挥作用?我们将使用这种模式来通知客户端代码何时发送了请求、何时接收到了响应,甚至是何时生成了意外错误。使用这种模式,客户端将被告知这些事件的发生。这使得使用库变得更加容易,因为它避免了在代码中创建诸如“如果生成了错误,则执行此操作”、“如果发送了请求,则执行此操作”、“如果接收到了响应,则执行此操作”等条件语句。
动手编码
首先,我将展示一张图表,呈现我们如何在库中添加这种模式:

让我们更好地理解实现方式。
- 请注意,该图表的结构与参考图表相同。
- 我添加了两个观察者可以访问的方法:
- OnSend() → 当发送请求时。
- OnRecv() → 当获得响应时。
- 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的实现,就是这样!只要请求被发送或接收响应,我们就会得到通知。
在引入这些观察者模式后,当前库的示意图如下:

重构文件夹
目前,所有库文件的目录结构如下:
|--- 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(),因当前内容已明确为参数操作。
鉴于此,我不会列出我将重命名的库中的所有方法,也不会提供源代码,因为我会不改变方法的实现,而只是改变名称。但随着这些更改,我将在下方提供一张图表,展示库中所有类以及更新后的方法名称及其关系。

结论
随着本文的发布,我们完成了关于创建Connexus库的系列文章。这个库旨在简化HTTP通信。这是一段相当长的旅程:我们从基础开始,深入探讨了更先进的设计和代码优化技巧,并研究了观察者模式,为Connexus赋予了动态和灵活系统中必不可少的响应性。我们还编码实现了这种模式,使得应用程序的各个部分能够自动对变化做出反应,从而创建了一个强大且适应性强的结构。
除了引入观察者模式外,我们还对整体文件与文件夹的结构进行了重构,使代码实现模块化设计且逻辑清晰直观。我们还对方法命名进行了优化,以提升代码的可读性,使库的使用方式更直观统一,这些细节对于实现代码整洁和长期维护而言至关重要。
Connexus的设计目标是让HTTP集成尽可能简单和直观,我们希望该系列文章展示过程中的每一个重要环节,并揭示实现这一目标的设计选择。通过本文,我们希望Connexus不仅能够简化您的HTTP集成,还能激发持续改进的灵感。感谢您与我一起踏上这段旅程,愿Connexus成为您项目中的得力助手!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16377
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
从基础到中级:数组和字符串(二)
原子轨道搜索(AOS)算法:改进与拓展
基于MQL5的自动化交易策略(第一部分):Profitunity系统(比尔·威廉姆斯的《交易混沌》)
Connexus客户端(第七部分):添加客户端层
您好!我已经将这篇文章中的所有文件 + 上一篇文章中的附加文件复制到 MQL5 文件夹中。下面是我尝试编译 Connexus\Test\TestRequest.mq5 时得到的结果:
你好!我将这篇文章中的所有文件 + 上一篇文章中的附加文件复制到了 MQL5 文件夹。下面是我尝试编译 Connexus\Test\TestRequest.mq5 时得到的结果:
简而言之,不再允许隐式有符号/无符号数组类型转换。
需要对代码进行一些修改。