Искусство работы с логами (Часть 9): Применяем паттерн Builder-класса и настраиваем конфигурации по умолчанию
Введение
С тех пор как я начал использовать Logify в различных личных и профессиональных проектах, я быстро осознал, что самая большая сложность заключалась не в надёжности или функциональности самой библиотеки, а в её настройке. Logify — это мощный инструмент для управления логированием в советниках (Expert Advisors), предлагающий множество обработчиков, уровней логирования, настраиваемые форматы, языковую поддержку и многое другое. Однако всё это требовало от пользователя ручной настройки каждого обработчика, форматтера и параметра, что может быть приемлемо для небольших проектов, но быстро превращается в повторяющуюся, утомительную и чреватую ошибками задачу по мере роста количества советников и проектов.
Представьте необходимость воспроизводить для каждого советника один и тот же сложный набор конфигураций: создание специфических обработчиков для комментирования на графике, вывода в консоль, записи в файлы и базы данных; установку минимальных уровней логирования для каждого обработчика; определение конкретных форматов для сообщений об ошибках, отладочной информации, уведомлений и так далее. Каждый из этих шагов, хоть и необходимый, порождает длинный, детализированный и неинтуитивный код, прерывая процесс разработки. Это может отпугнуть от использования Logify, даже несмотря на его безупречное управление логами во время выполнения.
Это понимание подтолкнуло меня к размышлению: как можно упростить эту настройку, облегчить жизнь пользователю, не жертвуя при этом гибкостью и возможностью кастомизации? Именно тогда и родилась идея создания Builder-класса для Logify — класса, который позволяет собрать всю конфигурацию плавным, цепочным способом, с помощью интуитивно понятных методов, создающих обработчики с разумными шаблонами и допускающих быстрые, точечные корректировки. Цель — превратить десятки строк конфигурации в несколько вызовов методов, как если бы мы писали чёткую сводку того, что нам нужно, вместо того чтобы заниматься всей ручной сборкой.
В этой статье я покажу, как я реализовал эти улучшения. Я представлю Builder, объяснив его дизайн и использование. А затем продемонстрирую, как можно настроить Logify, на практических примерах.
Понимание паттерна Builder: упрощение создания сложных объектов
Прежде чем мы погрузимся в реализацию нашего CLogifyBuilder, важно понять идею, лежащую в основе используемого нами паттерна Builder.
На практике Builder — это паттерн проектирования, имеющий единственную цель: упростить создание сложных объектов, особенно когда эти объекты требуют нескольких этапов настройки или имеют множество возможных опций. Его смысл заключается в том, чтобы отделить процесс конструирования от конечного представления объекта, позволяя одной и той же структуре сборки создавать различные "вариации" готовых к использованию объектов.
Возьмем простой пример: представьте, что вы собираетесь собрать автомобиль. Вы можете выбрать модель, цвет, тип двигателя, коробку передач, дополнительные опции, размер колес, отделку салона и еще десятки параметров. Делать все это непосредственно в коде, передавая сотни аргументов в конструктор, — непрактично и трудно поддерживать.
Именно для такой ситуации паттерн Builder и подходит лучше всего. Он разбивает процесс создания на цепочку методов (так называемый цепочный интерфейс), каждый из которых отвечает за настройку определенной части объекта.
В конце вы вызываете метод .Build() или аналогичный ему и получаете готовый к использованию объект. Этот подход дает три основных преимущества:
- Понятный и линейный код: создание объекта выглядит как логичный "сценарий", почти как если бы вы описывали, что хотите получить.
- Снижение количества ошибок: поскольку каждый шаг имеет изолированную цель, гораздо легче обнаружить и исправить неправильные настройки.
- Гибкость и повторное использование: один и тот же Builder может быть повторно использован для создания вариаций объекта с небольшими изменениями.
Применение паттерна Builder к Logify
В случае с Logify создание экземпляра логгера (CLogify) включает в себя создание множества обработчиков (например, для консоли, комментариев на графике, файлов), настройку минимальных уровней логирования, определение специфических форматтеров для каждого обработчика, а также такие параметры, как размер панели или стиль рамки. Выполнять всё это вручную, строчка за строчкой, снова и снова, стало загромождающим процессом.
Использование здесь паттерна Builder решает эту проблему. Вместо того чтобы возлагать на пользователя ответственность за ручную сборку каждого компонента, мы предлагаем более интуитивный интерфейс, например, такой:
CLogify *logify = logify .Create() .AddHandlerComment() .SetTitle("My Logger") .SetSize(5) .Done() .AddHandlerConsole() .Done() .Build();
Обратите внимание, насколько это понятно и выразительно. Метод .Create() запускает процесс конструирования, каждый .AddHandlerXXX() открывает настройку определенного обработчика, методы .SetX() регулируют его параметры, метод .Done() завершает настройку обработчика, а .Build() возвращает готовый экземпляр CLogify.
В этом и заключается сила паттерна Builder: он позволяет разработчику описать, что он хочет получить, не беспокоясь о деталях внутренней реализации.
Теперь, когда мы поняли, почему мы используем этот паттерн и как он помогает сделать Logify более практичным и масштабируемым, давайте практически посмотрим, как был построен этот класс и как мы можем использовать его в реальных условиях.
Специализированные Builder-классы для каждого обработчика
Мы создали новый файл <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.
Каждый такой Builder-класс инкапсулирует детали настройки конкретного типа вывода логов. Это позволяет избежать смешивания ответственности и сохраняет ядро процесса сборки чистым и модульным. Давайте посмотрим на их структуру, взяв в качестве примера ConsoleBuilder.
CLogifyHandlerConsoleBuilder
Класс начинается с определения минимальной структуры для поддержки текучего интерфейса (fluent API). В своем конструкторе он получает указатель на основной Builder (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 и уничтожает промежуточный Builder. Это обеспечивает непрерывность цикла сборки и позволяет избежать ненужного удержания памяти в коде.
Все специализированные Builder-классы имеют одинаковую анатомию:
- CLogifyBuilder *m_parent — ссылка на родительский Builder, используемая для возврата через метод Done().
- CLogifyFormatter *m_formatter — экземпляр форматтера, который будет связан с обработчиком.
- CLogifyHandlerX *m_handler — сам настраиваемый обработчик.
Более сложные Builder-классы (например, для файлов или базы данных) также используют внутреннюю структуру конфигурации (MqlLogifyHandleXConfig), которая временно хранит значения параметров до того момента, пока обработчик не будет готов к регистрации.
Такое разделение между данными конфигурации и их применением к обработчику позволяет выполнять проверки, использовать готовые шаблоны (presets) и комбинировать опции, не усложняя при этом логику самого обработчика.
Полный Builder-класс для консоли (Console Builder)
Далее представлен Builder с уже реализованными методами настройки:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
С этим готово, переходим к остальным специализированным строителям.
Другие специализированные Builder-классы
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)
Структура идентична другим Builder-классам, что обеспечивает согласованность.
CLogifyHandlerFileBuilder
Самый полный из всех. Настраивает запись в файлы (.log, .txt и т.д.) через MqlLogifyHandleFileConfig.
Доступные опции:
- SetDirectory(), SetFilename(), SetFileExtension()
- SetRotationMode(): по дате, размеру или вручную
- SetMessagesPerFlush()
- SetCodepage(): например, CP_UTF8
- SetFileSizeMB(), SetMaxFileCount()
А также три служебных метода с готовыми предустановками (пресетами):
- ConfigNoRotation() — без ротации
- ConfigDateRotation() — ротация по дате
- ConfigSizeRotation() — ротация по размеру
Эти сокращения инкапсулируют полную настройку в один вызов метода, что очень удобно для часто используемых шаблонов конфигурации. Builder-классы для остальных обработчиков следуют той же структуре, с вариациями конфигурации, специфичными для каждого типа. Вы можете ознакомиться с полными кодами в прилагаемых файлах.
Основной класс: CLogifyBuilder
Теперь, когда мы рассмотрели, как работают специализированные Builder-классы, пришло время взглянуть на компонент, который координирует их: класс CLogifyBuilder.
Он отвечает за создание и поддержку главного экземпляра CLogify, в который будут добавлены все обработчики. Но вместо того чтобы настраивать всё напрямую, он делегирует эту ответственность специализированным Builder-классам, каждый из которых заботится о конкретном типе обработчика. Таким образом, 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), чтобы впоследствии можно было вернуться к основному Builder-классу через метод Done() после завершения настройки.
- AddHandler(CLogifyHandler *handler) — этот метод вызывается внутренне специализированными Builder-классами в конце настройки (в методе Done() ). Он регистрирует готовый обработчик в создаваемом экземпляре CLogify.
- Build() — завершает процесс сборки, удаляет Builder из памяти с помощью delete GetPointer(this) и возвращает готовый логгер. Это подчеркивает идею о том, что экземпляр Builder-класса существует только в процессе сборки.
Благодаря такой структуре паттерн Builder реализован полностью: он модульный, понятный и расширяемый. Вы можете ознакомиться с полным кодом в прилагаемых файлах.
Элегантная точка входа
Хотя у нас уже есть конструктор CLogifyBuilder, прямой вызов этого конструктора пользователем с помощью new CLogifyBuilder() — не самый выразительный и интуитивно понятный способ начать создание логгера.
Именно поэтому мы добавили статический метод под названием Create() в класс CLogify:
//+------------------------------------------------------------------+ //| 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() является статическим по нескольким причинам:
- Он принадлежит классу, а не экземпляру — у вас еще нет экземпляра CLogify, когда вы хотите начать его создание.
- Он не зависит от какого-либо внутреннего состояния — все, что он делает, это создает и возвращает Builder.
- Позволяет избежать прямого связывания с классом строителя — если завтра реализация строителя изменится, вы сможете сохранить тот же статический интерфейс в классе CLogify и обеспечить совместимость с существующим кодом.
Стандартные настройки
По мере того как библиотека Logify обретает форму, нам нужно подумать о распространенном сценарии: пользователь, который хочет быстро начать логирование, ничего не настраивая. Ему нет дела до обработчиков, языков, форматов или директорий; ему просто нужен видимый вывод сообщений во время разработки или тестирования. Чтобы предусмотреть это, мы внедрили метод EnsureDefaultHandler().
Этот метод действует как автоматический запасной вариант: если ни один обработчик не был явно настроен, он добавляет два базовых, функциональных обработчика: один для Консоли (Console), другой для комментариев на графике (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 становится решением типа "включи и работай" (plug-and-play), не жертвуя при этом возможностью кастомизации. Это важный шаг в облегчении его внедрения как новичками, так и командами, требующими более строгих стандартов логирования.
Тестирование
Давайте сравним, каким было использование библиотеки до и после улучшений, реализованных в этой статье.
Раньше настройка лога требовала серии ручных шагов: создания экземпляров объектов, определения уровней, создания форматтеров, заполнения структур конфигурации и сборки обработчиков один за другим. Теперь, с введением настроек по умолчанию через 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; } //+------------------------------------------------------------------+ |
Такой минималистичный подход покрывает большинство случаев с нулевой ручной настройкой, что идеально подходит для быстрого прототипирования и тестирования.
Для случаев, когда нам нужен больший контроль над поведением лога, в игру вступает новый конструктор (Builder). Он предоставляет "текучий" интерфейс (fluent interface) и, что самое важное, является на 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 и выше
С выходом сборки 5100 терминала MetaTrader 5 некоторые внутренние изменения в компиляторе потребовали большей ясности при обработке типов в вызовах таких функций, как 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 } //+------------------------------------------------------------------+ |
Остальная часть кода остается без изменений. Это разовая корректировка, но необходимая для поддержания совместимости с последними версиями терминала.
Стоит помнить, что такого рода правки обычны, когда компилятор становится более требовательным к типам, и это обычно делается для предотвращения трудноуловимых проблем во время выполнения. Поэтому, хотя это может показаться простым изменением, это важное обновление, гарантирующее, что Logify продолжит стабильно работать в новых версиях MetaTrader 5.
Заключение
До сих пор настройка библиотеки Logify была мощной, но немного бюрократичной. Нужно было создавать объекты, настраивать конфигурации вручную, запоминать порядок вызовов... В общем, это работало, но было несколько утомительно.
В этой части статьи мы решили эту проблему. Мы создали новый способ работы с логгером: простой, понятный и быстрый. В дело вступил паттерн Builder, чтобы сделать всё более естественным: вы печатаете logify.Create(), и сам редактор показывает вам следующие опции. Нужен обработчик для комментариев на графике? Вводите AddHandlerComment(). Хотите изменить заголовок? Появляется метод SetTitle(). Вам не нужно ничего запоминать, не нужно возвращаться к документации. Просто следуйте потоку.
Мы также сделали библиотеку еще более дружелюбной, добавив настройки по умолчанию. Если вы просто хотите записывать сообщения и не беспокоитесь о кастомизации лога, вам не нужно делать ничего особенного. Просто создайте объект и начинайте его использовать. Logify сам позаботится о том, чтобы выводить сообщения в консоль и на график.
Наконец, мы подкорректировали одну важную техническую деталь: с выходом сборки 5100 MetaTrader 5 компилятор стал строже относиться к передаче ссылок в таких функциях, как 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Моделирование рынка (Часть 22): Первые шаги на SQL (V)
Знакомство с языком MQL5 (Часть 39): Руководство для начинающих по работе с файлами в MQL5 (I)
Знакомство с языком MQL5 (Часть 40): Руководство для начинающих по работе с файлами в MQL5 (II)
Возможности Мастера MQL5, которые вам нужно знать (Часть 71): Использование паттернов MACD и OBV
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Я задаюсь вопросом, хочу ли я выводить только сообщения об ошибках и отладке. А у меня все Info, Alert и т.д. встроены в советник. Может быть, установить значение bool для каждого типа в 'enum ENUM_LOG_LEVEL', чтобы показать, что мы хотим?
В производственном коде, если мы отключим некоторые из журналов, они не должны компилироваться в финальный файл ex5.
Для этого можно использовать переменную или даже IP-адрес в эксперте, который хранит нужное значение уровня и просто передает его обработчику. Вот пример.
В результате будут отображаться только сообщения с уровнем серьезности, большим или равным заданному в обработчике.
Язык вывода журнала ошибок по умолчанию может быть возвращен на язык терминала пользователя в соответствии с этим кодом
Часть 10 этой статьи находится в очереди на публикацию. В ней рассматривается способ подавления идентичных журналов, а также задается язык по умолчанию для терминала. Спасибо за предложение!
Для этого можно использовать переменную или даже IP-адрес в эксперте, который хранит нужное значение уровня и просто передает его обработчику. Вот пример.
В результате будут отображаться только сообщения с уровнем серьезности, большим или равным заданному в обработчике.
Автор показывает, как изменить уровень во время выполнения программы без модификации кода.
Я думаю, что он просто хочет показать только один уровень журнала. Для этого ему нужно изменить код, как показано ниже: