English Русский Deutsch 日本語
preview
精通日志记录(第九部分):实现构造器模式并添加默认配置

精通日志记录(第九部分):实现构造器模式并添加默认配置

MetaTrader 5示例 |
199 7
joaopedrodev
joaopedrodev

引言

我在各类个人与专业项目中使用 Logify 后很快发现,该库的核心问题并非在于自身的稳定性或功能完整性,而是配置环节过于繁琐。Logify 是一款用于管理 EA 日志的高效工具,内置多类处理器、多级别日志控制、自定义格式、多语言支持等丰富功能。但这些功能的使用,要求用户为每一个处理器、格式化器和参数逐一手动配置。对于小型项目而言,这种方式或许尚能接受;但随着 EA 及项目数量的增加,该操作会迅速沦为重复、繁琐且极易出错的工作。

试想,你需要在每个 EA 中重复复制一套套复杂的配置:为注释、控制台、文件、数据库创建专属处理器;为每个处理器设定最低日志级别;为错误消息、调试信息、告警信息等定义专属格式。这些步骤虽必不可少,却会产生冗长、冗余且可读性差的代码,严重影响开发效率和节奏。这种上手阶段的复杂性会形成使用门槛,甚至可能导致开发者放弃使用 Logify—— 即便该库在运行阶段的日志管理能力堪称完美。

这一问题促使我思考:如何在保留库的灵活性与可定制性的前提下,简化配置流程,降低用户使用成本?正是在此刻,为 Logify 创建一个构造器的想法应运而生。该构造器是一个类,支持以流畅的链式调用方式完成全量配置的组装;它能通过简洁直观的方法创建自带合理默认配置的处理器,还支持对配置进行快速局部调整。我们的目标是把原本数十行的配置代码,简化为寥寥数行方法调用 —— 就像编写一份清晰的 “需求清单”,而非手动完成繁琐的组装工作。

在本文中,我将展示我是如何实现这些改进的。我会介绍构造器,解释其设计思路与使用方法,并通过实际示例演示如何配置 Logify。


理解构造器模式:简化复杂对象的构建

在深入讲解 CLogifyBuilder 的具体实现前,先理解我们所采用的构造器模式及其核心设计思想,这一点至关重要。

构造器模式本质上是一种设计模式,它的核心目标只有一个:简化复杂对象的创建流程 —— 尤其是当这类对象需要经过多步配置、或包含大量可配置项时。该模式的核心设计思路是将对象的构造过程与其最终形态解耦,从而能基于相同的组装架构,创建出不同配置形态的成品对象。

举个简单的例子:假设你要组装一辆汽车,需要确定车型、颜色、发动机类型、变速箱、选装配置、轮毂尺寸、内饰等数十项配置。若在代码中直接实现这一逻辑,需要向构造函数传入数百个参数,这种方式既不实用,也难以维护。

而这正是构造器模式的优势所在。它将这种构造方式分解为一系列链式方法(也称为流式接口),每个方法负责配置对象的特定部分,直到最后调用 .Build() 或类似方法,从而获得一个完全准备就绪、可直接使用的最终对象。

这种方式具备三大核心优势:

  1. 可读性强:对象的构建逻辑读起来就像一份清晰的 “配置脚本”,直观描述出所需的对象形态。
  2. 降低出错率:每个步骤的职责独立,便于检测和修正配置错误。
  3. 灵活且可复用:同一个构造器可重复使用,只需微调即可创建不同配置的对象实例。


将构造器应用于 Logify

对于 Logify 来说,创建一个日志记录器实例需要完成一系列操作 :创建控制台、注释、文件等多类处理器,配置日志最低级别,为每个处理器定义专属格式化规则,甚至还需设置面板尺寸、边框样式等参数。 手动逐行重复完成这些操作,会让整个流程变得极其繁琐且冗余。

而构造器模式恰好能解决这一问题。我们无需再让用户手动组装每个功能组件,而是为其提供一套更直观的配置接口:

CLogify *logify = logify
   .Create()
   .AddHandlerComment()
      .SetTitle("My Logger")
      .SetSize(5)
   .Done()
   .AddHandlerConsole()
   .Done()
   .Build();

可以看到,这种写法的可读性极强,且表达直观。.Create () 用于启动构建流程,每个 .AddHandlerXXX () 方法用于开启对应处理器的配置,SetX () 方法用于调整处理器参数,.Done () 用于结束该处理器的配置,最终调用 .Build () 即可生成就绪的 CLogify 实例。

这正是构造器模式的价值所在:开发者只需描述所需的配置效果,无需关注底层实现逻辑。

现在我们已经理解了为什么要使用这种模式以及它如何帮助我们将 Logify 变得更实用、更具可扩展性,接下来让我们实际看看这个类是如何构建的,以及实践中如何使用它。


针对每个处理器的专用构造器

我们新建了一个文件 <Include/Logify/LogifyBuilder.mqh>,该文件中定义了 CLogifyBuilder 类;该类内置一个 CLogify 实例作为私有字段,后续会对这个实例进行操作,并最终将其返回给用户。

//+------------------------------------------------------------------+
//|                                                LogifyBuilder.mqh |
//|                                                     joaopedrodev |
//|                       https://www.mql5.com/en/users/joaopedrodev |
//+------------------------------------------------------------------+
#property copyright "joaopedrodev"
#property link      "https://www.mql5.com/en/users/joaopedrodev"
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#include "Logify.mqh"
//+------------------------------------------------------------------+
//| class : CLogifyBuilder                                           |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyBuilder                                      |
//| Heritage    : No heritage                                        |
//| Description : Build CLogify objects, following the Builder design|
//|               pattern.                                           |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyBuilder
  {
private:
   CLogify           *m_logify;
   
public:
                     CLogifyBuilder(void)
                    ~CLogifyBuilder(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyBuilder::CLogifyBuilder(void)
  {
   m_logify = new CLogify();
  }
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyBuilder::~CLogifyBuilder(void)
  {
  }
//+------------------------------------------------------------------+

CLogifyBuilder 类虽然是构建 CLogify 对象的核心枢纽,但它将每个处理器的具体配置工作委托给了专用的构造器:CLogifyHandlerCommentBuilder、CLogifyHandlerConsoleBuilder、CLogifyHandlerDatabaseBuilder 以及 CLogifyHandlerFileBuilder。

每个构造器封装了单一类型日志输出的细节。这种做法避免了职责混淆,确保了构建核心的简洁与模块化。让我们以 ConsoleBuilder 为例,来看看它们的结构。


CLogifyHandlerConsoleBuilder

该类首先定义了支持流式 API 所需的基础结构。它在构造函数中接收主构造器(CLogifyBuilder*)的指针,从而保持对构建上下文的引用。这使得在设置完处理器后,可以通过 Done() 方法返回:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerConsoleBuilder                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyHandlerConsoleBuilder                        |
//| Heritage    : No heritage                                        |
//| Description : Console handler constructor.                       |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerConsoleBuilder
  {
private:

   CLogifyBuilder    *m_parent;

public:
                     CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify);
                    ~CLogifyHandlerConsoleBuilder(void);

   CLogifyBuilder    *Done(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify)
  {
   m_parent = logify;
  };
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::~CLogifyHandlerConsoleBuilder(void)
  {
  }
//+------------------------------------------------------------------+
//| Finalizes the handler configuration.                             |
//+------------------------------------------------------------------+
CLogifyBuilder    *CLogifyHandlerConsoleBuilder::Done(void)
  {
   m_parent.AddHandler(GetPointer(m_handler));
   delete GetPointer(this);
   return(m_parent);
  }
//+------------------------------------------------------------------+

Done () 方法是返回主构造器的出口,其作用是将处理器添加至 CLogify 实例,同时销毁中间构造器。这样既保证了配置流程的连贯性,又能避免不必要的内存开销。

所有专用构造器均采用统一架构,包含以下核心成员:

  • CLogifyBuilder *m_parent:指向父构造器的引用,供 Done () 方法返回时调用。
  • CLogifyFormatter *m_formatter:与当前处理器关联的格式化器实例。
  • CLogifyHandlerX *m_handler:当前正在配置的具体处理器实例。

对于文件处理器、数据库处理器等更复杂的构造器,还会借助内部配置结构体(MqlLogifyHandleXConfig),在处理器注册生效前临时存储各项配置值。

这种将配置数据与处理器业务逻辑解耦的设计,能让我们在不增加处理器自身逻辑复杂度的前提下,完成配置验证、预设值应用和选项组合等操作。


完整的构造器

接下来是已经实现了配置方法的完整构造器:

//+------------------------------------------------------------------+
//| class : CLogifyHandlerConsoleBuilder                             |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyHandlerConsoleBuilder                        |
//| Heritage    : No heritage                                        |
//| Description : Console handler constructor.                       |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyHandlerConsoleBuilder
  {
private:

   CLogifyBuilder    *m_parent;
   CLogifyFormatter  *m_formatter;
   CLogifyHandlerConsole *m_handler;

public:
                     CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify);
                    ~CLogifyHandlerConsoleBuilder(void);

   CLogifyHandlerConsoleBuilder *SetLevel(ENUM_LOG_LEVEL level);
   CLogifyHandlerConsoleBuilder *SetFormatter(string format);
   CLogifyHandlerConsoleBuilder *SetFormatter(ENUM_LOG_LEVEL level, string format);
   CLogifyBuilder    *Done(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::CLogifyHandlerConsoleBuilder(CLogifyBuilder *logify)
  {
   m_parent = logify;
   m_formatter = new CLogifyFormatter();
   m_handler = new CLogifyHandlerConsole();

   m_handler.SetFormatter(GetPointer(m_formatter));
  };
//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder::~CLogifyHandlerConsoleBuilder(void)
  {
  }
//+------------------------------------------------------------------+
//| Sets the log level for the handler.                              |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetLevel(ENUM_LOG_LEVEL level)
  {
   m_handler.SetLevel(level);
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Sets the default format string for the formatter.                |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetFormatter(string format)
  {
   m_formatter.SetFormat(format);
   m_handler.SetFormatter(GetPointer(m_formatter));
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Sets a log-level-specific format for the formatter.              |
//+------------------------------------------------------------------+
CLogifyHandlerConsoleBuilder *CLogifyHandlerConsoleBuilder::SetFormatter(ENUM_LOG_LEVEL level, string format)
  {
   m_formatter.SetFormat(level,format);
   m_handler.SetFormatter(GetPointer(m_formatter));
   return(GetPointer(this));
  }
//+------------------------------------------------------------------+
//| Finalizes the handler configuration.                             |
//+------------------------------------------------------------------+
CLogifyBuilder    *CLogifyHandlerConsoleBuilder::Done(void)
  {
   m_parent.AddHandler(GetPointer(m_handler));
   delete GetPointer(this);
   return(m_parent);
  }
//+------------------------------------------------------------------+

至此,我们可以继续来创建剩余的专用构造器


其他专用构造器

CLogifyHandlerCommentBuilder

负责配置将消息直接写入图表(即调用 Comment() 函数)的处理器,该过程使用了 MqlLogifyHandleCommentConfig 结构体。

允许你定义:

  • SetSize(int):要显示的消息数量。
  • SetFrameStyle(ENUM_LOG_FRAME_STYLE): 为日志区域添加边框。
  • SetDirection(ENUM_LOG_DIRECTION): 设置垂直或水平方向。
  • SetTitle(string): 在顶部设置固定的标题。

同时也接受 SetLevel() 和 SetFormatter() 方法,可全局设置,也可按级别设置。配置程序以Done()结束。

CLogifyHandlerDatabaseBuilder

配置数据库持久化处理器(目前采用二进制结构)。它使用了 MqlLogifyHandleDatabaseConfig,并提供以下功能:

  • SetDirectory(string)
  • SetBaseFileName(string)
  • SetMessagesPerFlush(int)

该结构与其它构造器相同,保持了一致性。

CLogifyHandlerFileBuilder

这是最为完整的构造器。通过 MqlLogifyHandleFileConfig 配置写入 .log、.txt 等文件。

可用选项:

  • SetDirectory(), SetFilename(), SetFileExtension()
  • SetRotationMode(),按日期、大小或手动方式
  • SetMessagesPerFlush()
  • SetCodepage(),例如 CP_UTF8
  • SetFileSizeMB(), SetMaxFileCount()

还有三个带有现成预设的实用方法:

  • ConfigNoRotation()
  • ConfigDateRotation()
  • ConfigSizeRotation()

这些快捷方式通过单次调用封装完整的配置,适用于重复出现的模式。其他处理器的构建器遵循相同的结构,具有特定的配置变体。您可以在附件中查阅完整的代码。


核心类:CLogifyBuilder

现在我们已经探讨了专用构造器的工作方式,那么现在是时候看看它们的协调组件CLogifyBuilder 类了。

它负责创建和维护主 CLogify 实例,所有处理器都将添加到该实例中。但是,它并不直接配置所有内容,而是将此职责委托给专用构造器,每个构造器负责一种特定的处理器类型。因此,CLogifyBuilder 充当了调度指挥的角色,主导日志器的模块化构建。

以下是该类的完整实现:

//+------------------------------------------------------------------+
//| class : CLogifyBuilder                                           |
//|                                                                  |
//| [PROPERTY]                                                       |
//| Name        : LogifyBuilder                                      |
//| Heritage    : No heritage                                        |
//| Description : Build CLogify objects, following the Builder design|
//|               pattern.                                           |
//|                                                                  |
//+------------------------------------------------------------------+
class CLogifyBuilder
  {
private:

   CLogify           *m_logify;

public:
                     CLogifyBuilder(void);
                    ~CLogifyBuilder(void);

   CLogifyBuilder    *UseLanguage(ENUM_LANGUAGE language);

   //--- Starts configuration handlers
   CLogifyHandlerCommentBuilder *AddHandlerComment(void);
   CLogifyHandlerConsoleBuilder *AddHandlerConsole(void);
   CLogifyHandlerDatabaseBuilder *AddHandlerDatabase(void);
   CLogifyHandlerFileBuilder *AddHandlerFile(void);

   void              AddHandler(CLogifyHandler *handler);
   CLogify           *Build(void);
  };
//+------------------------------------------------------------------+

该类集中了一些重要功能:

  • UseLanguage(ENUM_LANGUAGE language):允许设置日志系统的主要语言。这会影响内部错误消息(通过CLogifyError)以及依赖本地化的格式化。
  • AddHandlerX():这些是配置处理器的入口点。每个方法(如AddHandlerConsole()、AddHandlerFile()等)会实例化一个专用构造器,并将自身作为指针(this)传递,以便在配置完成后通过Done()返回。
  • AddHandler(CLogifyHandler *handler):该方法在配置结束时(Done())由专用构造器在内部调用。它将准备就绪的处理器注册到正在构建的CLogify实例中。
  • Build():完成构建过程,通过delete GetPointer(this)释放构造器内存,并返回准备就绪的日志器。这强化了“构造器实例仅在构建过程中存在”的理念。

通过这种结构,构造器模式得以完善:模块化、清晰且可扩展。您可以在附件中查看完整代码。


优雅的入口点

尽管我们已经有了 CLogifyBuilder 构造函数,但让用户直接调用该构造函数,通过 new CLogifyBuilder() 进行实例化,并不是开始构建日志记录器的最直观或最清晰的方式。

为此,我们在CLogify类中添加了一个名为Create()的静态方法:

//+------------------------------------------------------------------+
//| Returns an instance of the builder                               |
//+------------------------------------------------------------------+
#include "LogifyBuilder.mqh"
CLogifyBuilder *CLogify::Create(void)
  {
   return(new CLogifyBuilder());
  }
//+------------------------------------------------------------------+

该方法在CLogify类中的声明如下:

class CLogify
  {
public:
   static CLogifyBuilder *Create(void);
  };

Create()方法是静态的,原因如下:

  1. 它属于类而非实例 — 当你想要开始构建日志器时,还没有CLogify的实例。
  2. 不依赖任何内部状态 — 它所做的只是创建并返回一个构造器。
  3. 避免与构造器直接耦合 — 如果明天构造器的实现发生变化,你可以在CLogify中保留相同的静态接口,从而保持与现有代码的兼容性。


默认设置

随着Logify库逐渐成型,我们需要考虑一个常见场景:用户希望快速记录日志,无需任何配置。他们不关心处理器、语言、格式或目录,只需要在开发或测试期间为消息提供可见的输出。为此,我们引入了EnsureDefaultHandler()方法。

此方法充当自动回退机制:如果未显式配置任何处理器,它会添加两个基本且可用的处理器,一个用于控制台,另一个用于 Comment()。两者均为MQL5原生,能确保消息立即可见。

void CLogify::EnsureDefaultHandler()
  {
   //--- Check if there is no handler
   if(this.SizeHandlers() == 0)
     {
      this.AddHandler(new CLogifyHandlerConsole());
      this.AddHandler(new CLogifyHandlerComment());
     }
  }

调用发生在Append() 方法内部,而非构造函数中:

bool CLogify::Append(ENUM_LOG_LEVEL level, string msg, string origin = "", string args = "", string filename = "", string function = "", int line = 0, int code_error = 0)
  {
   //--- Ensures that there is at least one handler
   this.EnsureDefaultHandler();
   
   // (continues...)
  }

这样做的目的在于:如果我们在构造函数中添加默认的处理器,它们将会被添加到后续的任何配置中,这意味着日志中最终会出现重复或非预期的处理器。当用户希望将所有输出重定向到单一目标(例如文件、数据库或远程服务器)时,这尤其令人头疼。

通过将此逻辑移至Append(),我们将控制权交到开发者手中。其工作方式如下:

  • 如果未配置任何处理器,EnsureDefaultHandler()会在首次调用Append()时激活两个默认处理器。
  • 如果至少手动添加了一个处理器,该方法不会执行任何操作。
  • 默认行为是安全且可见的,但在存在明确配置时不会存在干扰

这种方法平衡了简便性与可预测性。对于想要快速且功能完备的用户,系统“自动运行”。对于需要精细控制的用户,该库严格尊重开发者的选择。

由此,Logify 既实现了即插即用的便捷性,又未牺牲定制化能力,这是促进初学者以及对日志记录标准要求更严格的团队采用该库的重要一步。


测试

让我们对比一下,在使用本文实现的改进之前和之后,使用该库的情况。

之前,设置日志需要一系列手动步骤:实例化对象、定义级别、创建格式化器、填充配置结构,并逐个组装处理器。现在,通过EnsureDefaultHandler()引入默认设置,开发者只需一行代码即可开始使用该库。

下面是两种场景的并排对比:

原代码 新代码
//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   MqlLogifyHandleCommentConfig m_config;
   m_config.size = 5;
   m_config.frame_style = LOG_FRAME_STYLE_SINGLE;
   m_config.direction = LOG_DIRECTION_UP;
   m_config.title = "Expert name";

   CLogifyFormatter *formatter = new CLogifyFormatter("{date_time} [{levelname}]: {msg}");
   formatter.SetFormat(LOG_LEVEL_ERROR,"{date_time} [{levelname}]: {msg} [{err_constant} | {err_code} | {err_description}]");

   CLogifyHandlerComment *handler_comment = new CLogifyHandlerComment();
   handler_comment.SetConfig(m_config);
   handler_comment.SetLevel(LOG_LEVEL_DEBUG);
   handler_comment.SetFormatter(formatter);

   CLogifyHandlerConsole *handler_console = new CLogifyHandlerConsole();
   handler_console.SetLevel(LOG_LEVEL_DEBUG);
   handler_console.SetFormatter(formatter);

   logify = new CLogify();
   logify.AddHandler(handler_comment);
   logify.AddHandler(handler_console);

   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");

   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   logify = new CLogify();
   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+

这种极简方式在大多数情况下可以做到“零手动配置”,非常适合快速原型开发和测试。

而在需要更细粒度控制日志行为的场景下,新的构造器就派上用场了。它提供了顺畅的接口,最重要的是“100%类型化的”,意味着代码编辑器会实时提示可用的方法,从而减少出错,也不必再去死记函数签名。

当你输入 logify.Create().AddHandler 时,编辑器就会提示所有可用的处理器

接着你继续输入 .AddHandlerComment(),就只会显示与该类型处理器相关的有效设置项:

这是最终的代码,其中对注释处理器做了配置,并为报错信息指定了特定的格式。

//+------------------------------------------------------------------+
//| Import                                                           |
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify *logify;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   logify = logify.Create().AddHandlerComment().SetLevel(LOG_LEVEL_DEBUG).SetFormatter(LOG_LEVEL_ERROR,"{date_time} [{levelname}] {msg} ({err_constant} {err_code}: {err_description})").SetTitle("My expert").SetSize(5).Done().Build();
//---
   logify.Debug("Initializing Expert Advisor...", "Init", "");
   logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   logify.Error("Failed to send sell order", 10016,"Order Management");
//---
   return(INIT_SUCCEEDED);
  }
void OnDeinit(const int reason)
  {
   delete logify;
  }
//+------------------------------------------------------------------+

这样的“引导式”体验让开发过程几乎没有疑惑、阻力更小。写出来的代码简洁、直白,也不容易出错。


针对 MetaTrader 5 build 5100 及以上版本的修复

MetaTrader 5 自 5100版本 起,编译器内部做了一些调整,对 DatabaseColumnLong()、DatabaseColumnInteger() 这类调用里的类型处理要求更明确。

在实际操作中,这意味着在这些函数里,直接传递结构体字段的引用(比如 data[size].timestamp)已经不再安全。为了避免编译报错,最理想的做法是:先把这个值存到一个类型合适的临时变量里,然后再把这个临时变量的引用传给函数。

原代码 新代码
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- The rest of the method code remains the same

   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      DatabaseColumnLong(request,5,data[size].timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      DatabaseColumnInteger(request,7,data[size].level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      DatabaseColumnLong(request,11,data[size].line);
     }
   
   //--- The rest of the method code remains the same
  }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Get data by sql command                                          |
//+------------------------------------------------------------------+
bool CLogifyHandlerDatabase::Query(string query, MqlLogifyModel &data[])
  {
   //--- The rest of the method code remains the same

   //--- Reads query results line by line
   for(int i=0;DatabaseRead(request);i++)
     {
      int size = ArraySize(data);
      ArrayResize(data,size+1,size);
      
      //--- Maps database data to the MqlLogifyModel model
      DatabaseColumnText(request,1,data[size].formated);
      DatabaseColumnText(request,2,data[size].levelname);
      DatabaseColumnText(request,3,data[size].msg);
      DatabaseColumnText(request,4,data[size].args);
      long timestamp = (long)data[size].timestamp;
      DatabaseColumnLong(request,5,timestamp);
      string value;
      DatabaseColumnText(request,6,value);
      data[size].date_time = StringToTime(value);
      int level = data[size].level;
      DatabaseColumnInteger(request,7,level);
      DatabaseColumnText(request,8,data[size].origin);
      DatabaseColumnText(request,9,data[size].filename);
      DatabaseColumnText(request,10,data[size].function);
      long line = (long)data[size].line;
      DatabaseColumnLong(request,11,line);
     }
   
   //--- The rest of the method code remains the same
  }
//+------------------------------------------------------------------+

代码的其余部分保持不变。这虽然是一次性的调整,但对于保持与最新的 MetaTrader 5 的兼容性来说,是必不可少的。

值得注意的一点是:当编译器对类型检查变得更加严格时,这类调整其实很常见。这通常是为了从源头上避免那些在运行时才暴露出来的隐蔽问题。所以,虽然这看起来只是个不起眼的改动,但对于确保 Logify 在 MetaTrader 5 新版本中依然能稳定运行,这一步至关重要。


结论

在此之前,配置 Logify 库虽然功能强大,但确实有点繁琐。你得自己创建对象、手动设置配置、还得死记硬背调用的顺序…… 总之,能用归能用,但确实挺折腾人的。

而在本文介绍的这一部分里,我们彻底解决了这个问题。我们创造了一种全新的日志工作方式:简单、清晰、快速。构造器模式正式登场,让一切变得无比自然:当你输入 logify.Create() 时,编辑器会自动提示你接下来的选项。想要添加注释处理器?直接输入 AddHandlerComment()。想要修改标题?SetTitle() 立马就会出现。你不需要死记硬背任何东西,也不需要反复去翻文档。跟着流程走就行了。

我们还通过默认设置,让使用体验更加友好。如果你只是想记录消息,并不关心日志的定制化,那你什么都不用做。直接创建对象,然后开用就行。Logify 会自动搞定一切,把消息显示在控制台和图表上。

最后,我们还调整了一个关键的技术细节:随着 MetaTrader 5 build 5100 的发布,编译器在传递引用时变得更严格了,特别是在 DatabaseColumnLong() 和 DatabaseColumnInteger() 这类函数中。为了确保兼容性,我们在 CLogifyHandlerDatabase 中加入了一些微小的修正,在向这些函数传递数据前,先使用了中间变量。对于库的使用者来说,表面上没有任何变化,但在幕后,即使终端更新了,运行依然稳如泰山。

最终,我们实现了每个开发者都梦寐以求的效果:代码更少,错误更少,逻辑更清晰。现在的库,跟使用者的沟通更顺畅了,不再强加任何死板的规则,也不会把本该简单的事情搞复杂。随着 Logify 不断进化,变得更加灵活,并获得更多我认为对大家有用的新功能,我会陆续带来新的文章展示这些改进,让咱们的日常工作更轻松。我的理念是:让库与使用者共同成长,不搞华而不实的噱头,只写深思熟虑的好代码。

文件名 说明
Experts/Logify/LogiftTest.mq5
用于测试库功能的文件,包含一个实际示例
Include/Logify/Error/Languages/ErrorMessages.XX.mqh 统计每种语言的报错消息数量,其中 X 代表语言的缩写
Include/Logify/Error/Error.mqh
用于存储报错的数据结构
Include/Logify/Error/LogifyError.mqh
用于获取详细报错信息的类
Include/Logify/Formatter/LogifyFormatter.mqh
负责格式化日志记录的类,将占位符替换为具体值
Include/Logify/Handlers/LogifyHandler.mqh
用于管理日志处理器的基类,包括级别设置和日志发送
Include/Logify/Handlers/LogifyHandlerComment.mqh
将技术日志直接输出到MetaTrader图表注释的处理器
Include/Logify/Handlers/LogifyHandlerConsole.mqh
将格式化后的日志直接发送到 MetaTrader 终端控制台的日志处理器
Include/Logify/Handlers/LogifyHandlerDatabase.mqh
将格式化后的日志发送到数据库的日志处理器(目前仅包含打印输出,但很快我们会将其保存到真正的 sqlite 数据库中)
Include/Logify/Handlers/LogifyHandlerFile.mqh
将格式化后的日志发送到文件的日志处理器
Include/Logify/Utils/IntervalWatcher.mqh
检查一个时间间隔是否已经过去,允许您在库内部创建定时执行的例程。
Include/Logify/Logify.mqh 日志管理的核心类,集成了级别、模型和格式化功能
Include/Logify/LogifyBuilder.mqh 负责创建 CLogify 对象的类,旨在简化配置过程。
Include/Logify/LogifyLevel.mqh 定义 Logify 库日志级别的文件,支持精细控制
Include/Logify/LogifyModel.mqh 用于建模日志记录的结构体,涵盖级别、消息、时间戳和上下文等详细信息

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

附加的文件 |
Logify.zip (154.39 KB)
最近评论 | 前往讨论 (7)
Spoxus Spoxus
Spoxus Spoxus | 2 7月 2025 在 21:16

我想知道我是否只想输出调试信息和错误信息。而我在 EA 中内置了所有信息、警报等。是否可以在 "enum ENUM_LOG_LEVEL "中为每种类型设置一个 bool 值,以显示我们想要的信息?

对于生产 代码,如果我们关闭了某些日志,它就不应该被编译到最终的 ex5 文件中。

joaopedrodev
joaopedrodev | 31 7月 2025 在 16:58
Spoxus Spoxus 生产 代码,如果我们关闭了某些日志,它就不应该被编译到最终的 ex5 文件中。

为此,您可以在专家中使用一个变量或 IP 地址来存储所需的级别值,并将其传递给处理程序即可。下面是一个例子。

//+------------------------------------------------------------------+
//| 进口|
//+------------------------------------------------------------------+
#include <Logify/Logify.mqh>
CLogify Logify;
//+------------------------------------------------------------------+
//| 输入|
//+------------------------------------------------------------------+
input ENUM_LOG_LEVEL InpLogLevel = LOG_LEVEL_INFO; // 日志级别
//+------------------------------------------------------------------+
//| 专家初始化函数|
//+------------------------------------------------------------------+
int OnInit()
  {
   Logify.EnsureDefaultHandler();
   Logify.GetHandler(0).SetLevel(InpLogLevel);
   
   Logify.Debug("RSI indicator value calculated: 72.56", "Indicators", "Period: 14");
   Logify.Info("Buy order sent successfully", "Order Management", "Symbol: EURUSD, Volume: 0.1");
   Logify.Error("Failed to send sell order", 10016,"Order Management");
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

这将只显示严重程度大于或等于处理程序中定义的严重程度的信息。

joaopedrodev
joaopedrodev | 31 7月 2025 在 16:59
hini #:

根据以下代码,日志的默认错误输出语言可恢复为用户的终端语言

本文的第 10 部分即将发表。它讨论了抑制相同日志的方法,还设置了终端的默认语言。感谢您的建议!

Amy Liu
Amy Liu | 4 1月 2026 在 13:41
这是我见过的最好的文章--我喜欢!
Amy Liu
Amy Liu | 4 1月 2026 在 13:52
Spoxus Spoxus 生产 代码,如果我们关闭了某些日志,它就不应该被编译到最终的 ex5 文件中。
joaopedrodev#:

要做到这一点,可以在专家中使用一个变量或 IP 地址来存储所需的级别值,然后将其传递给处理程序即可。下面是一个例子。

这将只显示严重程度大于或等于处理程序中定义的严重程度的信息。

作者展示了如何在运行时更改级别,而无需修改代码。

我认为他只是想只显示一个级别的日志。他必须像下面这样修改代码:

//--- E.G.
void CLogifyHandlerComment::Emit(MqlLogifyModel &data)
  {
   //--- 检查日志级别是否允许
   if(data.level != this.GetLevel()) // 将'<'改为'!='
     {
      return;
     }

   //-- 保持历史记录的轮班日志
   for(int i = m_config.size-1; i > 0; i--)
     {
      m_cache[i] = m_cache[i-1];
     }
   m_cache[0] = data;

   //--- 生成完整的注释
   string comment = BuildHeader();
   comment += FormatLogLines();
   comment += BuildFooter();

   //--- 显示在图表上
   Comment(comment);
  }
价格行为分析工具包开发(第二十九部分):暴涨与暴跌拦截EA 价格行为分析工具包开发(第二十九部分):暴涨与暴跌拦截EA
了解暴涨与暴跌拦截EA如何将您的图表转变为一个主动预警系统 —— 通过超高速扫描价格变动速度、检查波动率激增情况、确认趋势走向以及运用关键枢轴区域过滤条件,精准识别市场的爆发性行情。该工具以清晰的绿色“暴涨”和红色“暴跌”箭头为您的每一次决策提供指引,助您排除市场杂音,以前所未有的方式把握市场价格飙升的机遇。深入探究其工作原理,了解它为何能成为您下一个不可或缺的交易优势。
MQL5交易管理面板开发(第十二部分):汇率计算器的集成 MQL5交易管理面板开发(第十二部分):汇率计算器的集成
精准计算核心交易价值,是每位交易者必不可少的日常工作。本文介绍如何将汇率计算器集成至交易管理面板,以此拓展多面板交易管理系统的功能。在执行交易时,快速确定风险敞口、头寸规模及潜在盈利至关重要,这一新功能旨在使面板内参数的计算过程更高效、更直观。让我们共同探索MQL5在构建高级交易面板中的实战应用。
市场模拟(第 14 部分):套接字(八) 市场模拟(第 14 部分):套接字(八)
许多程序员可能会认为,我们应该放弃使用 Excel,直接使用 Python,使用一些允许 Python 生成 Excel 文件以供以后分析结果的包。不过,正如前一篇文章提到的,虽然这个解决方案对于很多程序员来说是最简单的,但它不会被一些用户接受。在这种特殊情况下,用户总是正确的。作为程序员,我们必须找到一种让一切都能正常工作的方法。
从基础到中级:指标(四) 从基础到中级:指标(四)
在本文中,我们将探讨如何轻松创建和实施烛形着色操作方法。交易者们非常重视这一概念。在实现此类操作时,必须注意确保柱形或烛形保持其原有的外观,并且不会妨碍逐根烛形的解读。