
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.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



