Beherrschung von Protokollaufzeichnungen (Teil 9): Implementierung des Builder-Musters und Hinzufügen von Standardkonfigurationen
Einführung
Seit ich angefangen habe, Logify in verschiedenen privaten und beruflichen Projekten einzusetzen, habe ich schnell gemerkt, dass die größte Herausforderung nicht in der Robustheit oder Funktionalität der Bibliothek selbst liegt, sondern in ihrer Konfiguration. Logify ist ein leistungsfähiges Tool für die Verwaltung von Protokollen in Expert Advisors und bietet mehrere Handler, Protokollebenen, angepasste Formate, Sprachunterstützung und vieles mehr. All dies erforderte jedoch, dass der Nutzer jeden Handler, Formatierer und Parameter manuell konfigurieren musste, was für kleine Projekte gut funktionieren mag, aber schnell zu einer sich wiederholenden, ermüdenden und fehleranfälligen Aufgabe wird, wenn die Anzahl der EAs und Projekte wächst.
Stellen Sie sich vor, Sie müssten für jeden EA denselben komplexen Satz von Konfigurationen replizieren: Erstellen spezifischer Handler für Tabulator-Kommentare, Konsole, Dateien, Datenbanken; Festlegen von Mindeststufen für jeden Handler; Definieren spezifischer Formate für Fehlermeldungen, Debugging, Warnungen und so weiter. Jeder dieser Schritte ist zwar notwendig, erzeugt aber langen, detaillierten und unintuitiven Code, der den Entwicklungsfluss unterbricht. Diese anfängliche Komplexität wird zu einem Hindernis, einer Reibung, die von der Einführung von Logify abhalten kann, selbst wenn es eine tadellose Protokollverwaltung zur Laufzeit bietet.
Diese Erkenntnis hat mich zum Nachdenken angeregt: Wie kann ich diese Konfiguration vereinfachen und dem Nutzer das Leben erleichtern, ohne auf Flexibilität und Anpassbarkeit zu verzichten? An diesem Punkt wurde die Idee geboren, einen Builder für Logify zu entwickeln, eine Klasse, die es Ihnen ermöglicht, die gesamte Konfiguration in einer fließenden, verketteten Weise zusammenzustellen, mit intuitiven Methoden, die Handler mit sinnvollen Mustern erstellen und schnelle, lokale Anpassungen ermöglichen. Ziel ist es, Dutzende von Konfigurationszeilen in einige wenige Methodenaufrufe umzuwandeln, fast so, als ob wir eine klare Zusammenfassung dessen schreiben würden, was wir wollen, anstatt die ganze manuelle Zusammenstellung.
In diesem Artikel werde ich Ihnen zeigen, wie ich diese Verbesserungen umgesetzt habe. Ich werde den Builder vorstellen und seinen Aufbau und seine Verwendung erläutern. Anschließend werde ich anhand von praktischen Beispielen demonstrieren, wie Logify konfiguriert werden kann.
Verständnis des Builder-Musters: Vereinfachung der Konstruktion komplexer Objekte
Bevor wir uns mit der Implementierung unseres CLogifyBuilders beschäftigen, ist es wichtig, die Idee hinter dem von uns verwendeten Muster zu verstehen: den Builder.
In der Praxis ist Builder ein Entwurfsmuster, das ein einziges Ziel verfolgt: die Erstellung komplexer Objekte zu erleichtern, insbesondere wenn diese Objekte mehrere Konfigurationsschritte erfordern oder viele mögliche Optionen haben. Der Vorschlag besteht darin, den Konstruktionsprozess von der endgültigen Darstellung des Objekts zu trennen, sodass mit derselben Baugruppenstruktur verschiedene „Geschmacksrichtungen“ von gebrauchsfertigen Objekten erstellt werden können.
Nehmen wir ein einfaches Beispiel: Stellen Sie sich vor, Sie wollen ein Auto zusammenbauen. Sie können das Modell, die Farbe, den Motortyp, das Getriebe, die Optionen, die Größe der Räder, die Innenausstattung und Dutzende anderer Entscheidungen wählen. All dies direkt im Code zu tun, indem Hunderte von Argumenten an einen Konstruktor übergeben werden, ist weder praktisch noch einfach zu pflegen.
Genau für diese Art von Situationen ist Builder hervorragend geeignet. Es unterteilt diese Konstruktion in verkettete Methoden (auch fließende Schnittstellen genannt), die jeweils für die Konfiguration eines bestimmten Teils des Objekts verantwortlich sind, bis Sie am Ende .Build() oder etwas Ähnliches aufrufen und das fertige Objekt erhalten, das zu 100 % einsatzbereit ist.
Dieser Ansatz bringt drei wesentliche Vorteile mit sich:
- Klare und lineare Lesart: Der Aufbau des Objekts wirkt wie ein logisches „Drehbuch“, fast so, als würden Sie beschreiben, was Sie wollen.
- Fehlerreduzierung: Da jeder Schritt einen isolierten Zweck hat, ist es viel einfacher, falsche Konfigurationen zu erkennen und zu korrigieren.
- Flexibilität und Wiederverwendung: derselbe Builder kann wiederverwendet werden, um mit geringfügigen Änderungen Variationen des Objekts zu erstellen.
Anwendung des Builders auf Logify
Im Fall von Logify umfasst die Erstellung einer Logger-Instanz (CLogify) die Erstellung mehrerer Handler (z. B. Konsole, Kommentar, Datei), die Konfiguration von Mindestprotokollebenen, die Definition spezifischer Formatierer für jeden Handler und sogar Parameter wie Panelgröße oder Rahmenstil. All dies manuell, Zeile für Zeile, immer und immer wieder zu tun, hat sich zu einem schmutzigen Prozess entwickelt.
Durch die Verwendung des Builder-Musters wird dieses Problem gelöst. Anstatt den Nutzer der Verantwortung auszusetzen, jedes Teil manuell zusammenzusetzen, bieten wir eine intuitivere Schnittstelle, wie diese hier:
CLogify *logify = logify .Create() .AddHandlerComment() .SetTitle("My Logger") .SetSize(5) .Done() .AddHandlerConsole() .Done() .Build();
Beachten Sie, wie einfach und aussagekräftig er sich liest. Mit .Create() wird die Konstruktion gestartet, mit .AddHandlerXXX() wird die Konfiguration eines Handlers eröffnet, mit SetX() werden dessen Parameter angepasst, mit .Done() wird die Konfiguration eines Handlers beendet, und mit .Build() wird die fertige Instanz von CLogify geliefert.
Das ist die Stärke des Builders: Er ermöglicht es dem Entwickler, seine Wünsche zu beschreiben, ohne sich um die Details der Implementierung zu kümmern.
Nachdem wir nun verstanden haben, warum wir dieses Muster verwenden und wie es uns hilft, Logify praktischer und skalierbarer zu machen, lassen Sie uns einen praktischen Blick darauf werfen, wie diese Klasse gebaut wurde und wie wir sie in der realen Welt verwenden können.
Spezialisierte Konstrukteure für jeden Handler
Wir haben eine neue Datei <Include/Logify/LogifyBuilder.mqh> erstellt. Darin befindet sich die Klasse CLogifyBuilder, die als privates Feld bereits eine Instanz von CLogify enthält. Diese Instanz wird manipuliert und am Ende an den Nutzer zurückgegeben.
//+------------------------------------------------------------------+ //| 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) { } //+------------------------------------------------------------------+
Die Klasse CLogifyBuilder ist der zentrale Punkt für die Konstruktion eines CLogify-Objekts, delegiert aber die Konfiguration der einzelnen Handler-Typen an spezialisierte Builder: CLogifyHandlerCommentBuilder, CLogifyHandlerConsoleBuilder, CLogifyHandlerDatabaseBuilder und CLogifyHandlerFileBuilder.
Jeder Builder kapselt die Details einer einzelnen Art von Protokollausgabe. Dadurch wird eine Vermischung der Zuständigkeiten vermieden und der Kern des Builds bleibt sauber und modular. Werfen wir einen Blick auf ihre Struktur am Beispiel des ConsoleBuilders.
CLogifyHandlerConsoleBuilder
Die Klasse beginnt mit der Definition der Mindeststruktur zur Unterstützung der fließenden API. Er erhält im Konstruktor einen Zeiger auf den Haupt-Builder (CLogifyBuilder*) und behält einen Verweis auf den Build-Kontext. So können Sie mit Done() zu diesem Kontext zurückkehren, nachdem Sie den Handler eingerichtet haben:
//+------------------------------------------------------------------+ //| 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() ist der Rücksprungpunkt zum Hauptbuilder, fügt den Handler zu CLogify hinzu und vernichtet den Zwischenbuilder. Dadurch wird der Kreislauf in Gang gehalten und eine unnötige Speicherung vermieden.
Alle spezialisierten Builder folgen der gleichen Anatomie:
- CLogifyBuilder *m_parent: Verweis auf den übergeordneten Builder, der für die Rückkehr über Done() verwendet wird.
- CLogifyFormatter *m_formatter: Formatierungsinstanz, die mit dem Handler verknüpft wird.
- CLogifyHandlerX *m_handler: der Handler selbst, der konfiguriert wird.
Komplexere Builder (wie File oder Database) verwenden auch eine interne Konfigurationsstruktur (MqlLogifyHandleXConfig), die die Werte vorübergehend speichert, bis der Handler bereit ist, registriert zu werden.
Diese Trennung zwischen Konfigurationsdaten und Anwendung auf den Handler ermöglicht die Anwendung von Validierungen, die Verwendung von Voreinstellungen und die Kombination von Optionen, ohne die Logik des Handlers selbst aufzublähen.
Vollständiger Konsolenbauer
Als Nächstes folgt der Builder mit den bereits implementierten Konfigurationsmethoden:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Damit sind wir bereit, uns mit dem Rest der spezialisierten Builder auseinanderzusetzen
Andere spezialisierte Builder
CLogifyHandlerCommentBuilder
Verantwortlich für die Konfiguration des Handlers, der Nachrichten direkt in den Graphen schreibt (Comment()), unter Verwendung der Struktur MqlLogifyHandleCommentConfig.
Ermöglicht Ihnen die Definition:
- SetSize(int): Anzahl der angezeigten Meldungen.
- SetFrameStyle(ENUM_LOG_FRAME_STYLE): Rahmen um den Protokollbereich.
- SetDirection(ENUM_LOG_DIRECTION): vertikale oder horizontale Ausrichtung.
- SetTitle(string): fester Titel am Anfang des Protokolls.
Akzeptiert auch SetLevel() und SetFormatter(), global oder nach Level. Die Konfiguration wird mit Done() abgeschlossen.
CLogifyHandlerDatabaseBuilder
Konfiguriert den Datenbank-Persistenz-Handler (vorerst binäre Struktur). Verwendet MqlLogifyHandleDatabaseConfig. Es bietet:
- SetDirectory(string)
- SetBaseFileName(string)
- SetMessagesPerFlush(int)
Der Aufbau ist identisch mit dem der anderen Builder, sodass die Konsistenz gewahrt bleibt.
CLogifyHandlerFileBuilder
Die vollständigste von allen. Konfiguriert das Schreiben in .log-, .txt- usw. Dateien über MqlLogifyHandleFileConfig.
Verfügbare Optionen:
- SetDirectory(), SetFilename(), SetFileExtension()
- SetRotationMode(), nach Datum, Größe oder manuell
- SetMessagesPerFlush()
- SetCodepage(), wie z.B. CP_UTF8
- SetFileSizeMB(), SetMaxFileCount()
Und auch drei Utility-Methoden mit vorgefertigten Presets:
- ConfigNoRotation()
- ConfigDateRotation()
- ConfigSizeRotation()
Diese Abkürzungen kapseln die gesamte Konfiguration mit einzelnen Aufrufen, was für wiederkehrende Muster nützlich ist. Die Builder für die anderen Handler folgen der gleichen Struktur, mit spezifischen Konfigurationsvarianten. Sie können die vollständigen Codes in den beigefügten Dateien einsehen.
Die Kernklasse: CLogifyBuilder
Nachdem wir nun untersucht haben, wie die spezialisierten Builder funktionieren, ist es an der Zeit, sich den Teil anzusehen, der sie koordiniert: die Klasse CLogifyBuilder.
Sie ist für die Erstellung und Pflege der Hauptinstanz von CLogify zuständig, zu der alle Handler hinzugefügt werden. Aber anstatt alles direkt zu konfigurieren, delegiert es diese Verantwortung an spezialisierte Builder, von denen sich jeder um einen bestimmten Typ von Handler kümmert. CLogifyBuilder fungiert also als eine Art Dirigent, der den modularen Aufbau des Loggers leitet.
Nachstehend finden Sie die vollständige Implementierung der Klasse:
//+------------------------------------------------------------------+ //| 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); }; //+------------------------------------------------------------------+
In dieser Klasse sind einige wichtige Funktionen zusammengefasst:
- UseLanguage(ENUM_LANGUAGE language): Ermöglicht die Einstellung der Hauptsprache des Protokollierungssystems. Dies betrifft interne Fehlermeldungen (über CLogifyError) und die Formatierung, die von der Lokalisierung abhängt.
- AddHandlerX(): Dies sind die Einstiegspunkte für die Konfiguration der Handler. Jede Methode (AddHandlerConsole(), AddHandlerFile() usw.) instanziiert einen spezialisierten Builder, der sich selbst als Zeiger (this) übergibt, sodass er über Done() zurückkehren kann, sobald er konfiguriert ist.
- AddHandler(CLogifyHandler *handler): Diese Methode wird intern von spezialisierten Buildern am Ende der Konfiguration (Done()) aufgerufen. Er registriert den vorgefertigten Handler in der zu erstellenden CLogify-Instanz.
- Build(): Beendet den Erstellungsprozess, gibt den Builder mit delete GetPointer(this) aus dem Speicher frei und gibt den fertigen Logger zurück. Dies unterstreicht den Gedanken, dass die Instanz des Builders nur während des Montageprozesses existiert.
Mit dieser Struktur ist das Muster des Builders vollständig: modular, klar und erweiterbar. Sie können den vollständigen Code in den angehängten Dateien sehen.
Ein eleganter Einstieg
Obwohl wir bereits über den CLogifyBuilder-Konstruktor verfügen, ist es nicht die ausdrucksstärkste oder intuitivste Art, mit der Erstellung des Loggers zu beginnen, wenn der Nutzer diesen Konstruktor direkt mit new CLogifyBuilder() instanziiert.
Aus diesem Grund haben wir der Klasse CLogify eine statische Methode namens Create() hinzugefügt:
//+------------------------------------------------------------------+ //| Returns an instance of the builder | //+------------------------------------------------------------------+ #include "LogifyBuilder.mqh" CLogifyBuilder *CLogify::Create(void) { return(new CLogifyBuilder()); } //+------------------------------------------------------------------+
Und die Methode wird in der Klasse CLogify wie folgt deklariert:
class CLogify { public: static CLogifyBuilder *Create(void); };
Die Methode Create() ist statisch, weil:
- Sie gehört zur Klasse, nicht zur Instanz – Noch gibt es keine Instanz von CLogify, wenn Sie mit der Erstellung beginnen wollen.
- Es hängt nicht von einem internen Zustand ab – es wird lediglich ein Builder erstellt und zurückgegeben.
- Vermeidet die direkte Kopplung mit dem Builder – wenn sich die Builder-Implementierung morgen ändert, können Sie die gleiche statische Schnittstelle in CLogify beibehalten und die Kompatibilität mit dem vorhandenen Code wahren.
Standardeinstellungen
Während die Logify-Bibliothek Gestalt annimmt, müssen wir an ein häufiges Szenario denken: den Nutzer, der sich schnell anmelden möchte, ohne etwas zu konfigurieren. Sie kümmern sich nicht um Handler, Sprachen, Formate oder Verzeichnisse, sie brauchen nur eine sichtbare Ausgabe für Nachrichten während der Entwicklung oder des Testens. Um dem Rechnung zu tragen, haben wir die Methode EnsureDefaultHandler() eingeführt.
Diese Methode fungiert als automatischer Fallback: Wenn kein Handler explizit konfiguriert wurde, fügt sie zwei grundlegende, funktionale Handler hinzu, einen für die Konsole, den anderen für Comment(). Beide sind Bestandteil von MQL5 und garantieren eine sofortige Sichtbarkeit der Nachricht.
void CLogify::EnsureDefaultHandler() { //--- Check if there is no handler if(this.SizeHandlers() == 0) { this.AddHandler(new CLogifyHandlerConsole()); this.AddHandler(new CLogifyHandlerComment()); } }
Der Aufruf erfolgt innerhalb der Methode Append(), nicht im Konstruktor:
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...) }
Diese Entscheidung hat einen strategischen Zweck: Wenn wir im Konstruktor einen Standard-Handler hinzufügen würden, würden dieser zu jeder späteren Konfiguration hinzugefügt werden, was bedeuten würde, dass das Protokoll mit doppelten oder unbeabsichtigten Handlern enden würde. Dies ist vor allem dann problematisch, wenn der Nutzer alle Ausgaben an ein einziges Ziel, z. B. eine Datei, eine Datenbank oder einen Remote-Server, leiten möchte.
Indem wir diese Logik zu Append() verschieben, behalten wir die Kontrolle in den Händen des Entwicklers. Es funktioniert folgendermaßen:
- Wenn kein Handler konfiguriert ist, aktiviert EnsureDefaultHandler() beim ersten Aufruf von Append() beide Standardwerte.
- Wenn mindestens ein Handler manuell hinzugefügt wird, bewirkt die Methode nichts.
- Das Standardverhalten ist sicher und sichtbar, stört aber nicht, wenn es eine explizite Konfiguration gibt.
Dieser Ansatz schafft ein Gleichgewicht zwischen Bequemlichkeit und Berechenbarkeit. Für diejenigen, die etwas Schnelles und Funktionelles wollen, läuft das System „von selbst“. Für diejenigen, die eine genaue Kontrolle benötigen, respektiert die Bibliothek strikt die Entscheidungen des Entwicklers.
Damit wird Logify zum Plug-and-Play-System, ohne dass die Anpassungsmöglichkeiten eingeschränkt werden. Dies ist ein wichtiger Schritt, um die Akzeptanz von Logify sowohl bei Anfängern als auch bei Teams, die strengere Protokollierungsstandards verlangen, zu erleichtern.
Tests
Vergleichen wir einmal, wie die Bibliothek vor und nach den in diesem Artikel vorgenommenen Verbesserungen zu verwenden war.
Früher war für die Einrichtung eines Protokolls eine Reihe von manuellen Schritten erforderlich: Instanziierung von Objekten, Definition von Ebenen, Erstellung von Formatierern, Ausfüllen von Konfigurationsstrukturen und Zusammenstellung von Handlern, einer nach dem anderen. Mit der Einführung von Standardeinstellungen über EnsureDefaultHandler() kann der Entwickler die Bibliothek nun mit einer einzigen Zeile verwenden.
Unten sehen Sie die beiden Szenarien nebeneinander:
| Alter Code | Neuer Code |
|---|---|
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+ |
Dieser minimalistische Ansatz deckt die meisten Fälle ohne manuelle Konfiguration ab und ist ideal für Rapid Prototyping und Tests.
In Fällen, in denen wir mehr Kontrolle über das Verhalten des Protokolls benötigen, kommt der neue Builder ins Spiel. Die Schnittstelle ist flüssig und vor allem zu 100 % typisiert, was bedeutet, dass der Code-Editor selbst die verfügbaren Methoden in Echtzeit vorschlägt, was Fehler reduziert und das Auswendiglernen von Funktionssignaturen überflüssig macht.
Wenn Sie logify.Create().AddHandler eingeben, schlägt der Editor bereits alle verfügbaren Handler vor:

Und wenn Sie mit .AddHandlerComment() fortfahren, erscheinen nur die gültigen Einstellungen für diesen speziellen Typ von Handler:

So sieht der Code am Ende aus, wenn der Kommentar-Handler konfiguriert wird, mit einem spezifischen Format für Fehler.
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Diese geführte Erfahrung beseitigt Zweifel und verringert die Reibung bei der Entwicklung. Der Code ist sauber, geradlinig und fehlerfrei.
Fix für MetaTrader 5 Build 5100 oder höher
Mit der Veröffentlichung des Build 5100 von MetaTrader 5 erfordern einige interne Änderungen am Compiler nun mehr Klarheit bei der Handhabung von Typen in Aufrufen wie DatabaseColumnLong() und DatabaseColumnInteger().
In der Praxis bedeutet dies, dass es nicht mehr sicher ist, Verweise auf Strukturfelder (wie data[size].timestamp) in diesen Funktionen direkt zu übergeben. Um Kompilierungsfehler zu vermeiden, ist es ideal, den Wert zunächst in einer temporären Variablen des entsprechenden Typs zu speichern und ihn erst dann per Referenz an die Funktion zu übergeben.
| Alter Code | Neuer Code |
|---|---|
//+------------------------------------------------------------------+ //| 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 } //+------------------------------------------------------------------+ |
Der Rest des Codes bleibt unverändert. Dies ist eine einmalige Anpassung, die jedoch notwendig ist, um die Kompatibilität mit neueren Versionen des Terminals zu gewährleisten.
Es sei daran erinnert, dass diese Art der Anpassung üblich ist, wenn der Compiler anspruchsvoller mit Typen wird, und dass dies in der Regel geschieht, um subtile Probleme zur Laufzeit zu vermeiden. Obwohl es sich um eine einfache Änderung handelt, ist es ein wichtiges Update, um sicherzustellen, dass Logify in neueren Versionen von MetaTrader 5 weiterhin stabil funktioniert.
Schlussfolgerung
Bisher war die Konfiguration der Logify-Bibliothek zwar leistungsfähig, aber auch ein wenig bürokratisch. Man musste Objekte erstellen, Konfigurationen manuell einrichten, sich die Reihenfolge der Aufrufe merken ... kurz gesagt, es funktionierte, aber es war etwas mühsam.
In diesem Teil des Artikels haben wir das Problem gelöst. Wir haben eine neue Art der Arbeit mit dem Protokoll geschaffen: einfach, klar und schnell. Der Builder kam ins Spiel, um alles natürlicher zu machen: Sie geben logify.Create() ein und der Editor selbst zeigt Ihnen die nächsten Optionen. Möchten Sie einen Kommentarhandler? Typ AddHandlerComment(). Möchten Sie den Titel ändern? SetTitle() erscheint. Sie müssen nichts auswendig lernen, Sie müssen die Unterlagen nicht noch einmal durchgehen. Folgen Sie einfach dem Fluss.
Außerdem haben wir die Standardeinstellungen noch nutzerfreundlicher gestaltet. Wenn Sie nur Nachrichten aufzeichnen wollen und sich keine Gedanken über die Anpassung des Protokolls machen, brauchen Sie nichts zu tun. Erstellen Sie einfach das Objekt und verwenden Sie es. Logify selbst dreht sich um und zeigt die Meldungen in der Konsole und im Diagramm an.
Schließlich haben wir noch ein wichtiges technisches Detail angepasst: Mit der Einführung von MetaTrader 5 Build 5100 ist der Compiler strenger geworden, was die Übergabe von Referenzen in Funktionen wie DatabaseColumnLong() und DatabaseColumnInteger() angeht. Um die Kompatibilität zu gewährleisten, haben wir kleine Korrekturen an CLogifyHandlerDatabase vorgenommen, indem wir Zwischenvariablen verwenden, bevor wir Daten an diese Funktionen übergeben. Für diejenigen, die die Bibliothek verwenden, ändert sich nichts, aber hinter den Kulissen bleibt sie stabil, auch bei Terminal-Updates.
Am Ende haben wir das erreicht, was jeder Entwickler mag: weniger Code, weniger Fehler und mehr Übersichtlichkeit. Die Bibliothek spricht jetzt besser mit den Nutzern, ohne sie in eine starre Form zu zwingen und ohne das, was einfach sein sollte, zu verkomplizieren. Wenn Logify sich weiterentwickelt, noch flexibler wird und neue Funktionen erhält, die meiner Meinung nach für die meisten Nutzer nützlich sind, werde ich Ihnen neue Artikel vorlegen, die die Verbesserungen zeigen und unser tägliches Leben erleichtern. Die Idee ist, dass die Bibliothek mit den Nutzern mitwächst, ohne Magie, nur mit gut durchdachtem Code.
| Dateiname | Beschreibung |
|---|---|
| Experts/Logify/LogiftTest.mq5 | die Datei, in der wir die Funktionen der Bibliothek testen, mit einem praktischen Beispiel. |
| Include/Logify/Error/Languages/ErrorMessages.XX.mqh | zählt die Fehlermeldungen in jeder Sprache, wobei X für das Akronym der Sprache steht. |
| Include/Logify/Error/Error.mqh | die Datenstruktur zur Speicherung von Fehlern |
| Include/Logify/Error/LogifyError.mqh | die Klasse zum Abrufen detaillierter Fehlerinformationen |
| Include/Logify/Formatter/LogifyFormatter.mqh | die Klasse, die für die Formatierung von Protokolldatensätzen zuständig ist, indem sie Platzhalter durch bestimmte Werte ersetzt. |
| Include/Logify/Handlers/LogifyHandler.mqh | die Basisklasse für die Verwaltung von Log-Handlern, einschließlich der Einstellung des Levels und des Versands von Logs. |
| Include/Logify/Handlers/LogifyHandlerComment.mqh | der Log-Handler, der formatierte Logs direkt an den Kommentar auf dem Terminal-Chart im MetaTrader sendet. |
| Include/Logify/Handlers/LogifyHandlerConsole.mqh | der Handler der Logs, der formatierte Logs direkt an die Terminal-Konsole im MetaTrader sendet. |
| Include/Logify/Handlers/LogifyHandlerDatabase.mqh | der Log-Handler, der formatierte Logs an eine Datenbank sendet (derzeit enthält er nur einen Ausdruck, aber bald werden wir ihn in einer echten Sqlite-Datenbank speichern). |
| Include/Logify/Handlers/LogifyHandlerFile.mqh | der Log-Handler, der formatierte Logs in eine Datei sendet. |
| Include/Logify/Utils/IntervalWatcher.mqh | prüft, ob ein Zeitintervall verstrichen ist, sodass Sie Routinen in der Bibliothek erstellen können. |
| Include/Logify/Logify.mqh | die Kernklasse für die Protokollverwaltung, die Ebenen, Modelle und Formatierung integriert. |
| Include/Logify/LogifyBuilder.mqh | die Klasse, die für die Erstellung eines CLockify-Objekts verantwortlich ist und die Konfiguration vereinfacht |
| Include/Logify/LogifyLevel.mqh | die Datei, die die Log-Ebenen der Logify-Bibliothek definiert und eine detaillierte Kontrolle ermöglicht. |
| Include/Logify/LogifyModel.mqh | die Struktur, die die Protokolleinträge modelliert, einschließlich Details wie Ebene, Nachricht, Zeitstempel und Kontext. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/18602
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Implementierung von praktischen Modulen aus anderen Sprachen in MQL5 (Teil 01): Aufbau der SQLite3-Bibliothek, inspiriert von Python
Senden von Nachrichten von MQL5 an Discord, Erstellen eines Discord-Bots für MetaTrader 5
MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 72): Verwendung der Muster von MACD und OBV mit überwachtem Lernen
Selbstoptimierende Expert Advisors in MQL5 (Teil 8): Analyse mehrerer Strategien (2)
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
Ich frage mich, ob ich nur Debug- und Fehlermeldungen ausgeben möchte. Und ich habe alle Info, Alert, etc. in der EA gebaut. Vielleicht einen bool-Wert für jeden Typ in 'enum ENUM_LOG_LEVEL' zu setzen, um zu zeigen, was wir wollen?
Für den Produktionscode, wenn wir einige der Protokolle ausschalten, sollte es nicht in die endgültige ex5-Datei kompiliert werden.
Um dies zu erreichen, können Sie eine Variable oder sogar eine IP-Adresse im Experten verwenden, die den gewünschten Level-Wert speichert und einfach an den Handler weitergibt. Hier ist ein Beispiel.
Damit werden nur Meldungen angezeigt, deren Schweregrad größer oder gleich dem im Handler definierten ist.
Die Standard-Fehlerausgabesprache des Protokolls kann mit folgendem Code auf die Terminalsprache des Benutzers zurückgesetzt werden
Teil 10 dieses Artikels ist in der Warteschlange zur Veröffentlichung. Darin wird eine Möglichkeit beschrieben, identische Protokolle zu unterdrücken und die Standardsprache für das Terminal festzulegen. Vielen Dank für die Anregung!
Um dies zu tun, können Sie eine Variable oder sogar eine IP-Adresse im Experten verwenden, die den gewünschten Level-Wert speichert und ihn einfach an den Handler weitergibt. Hier ist ein Beispiel.
Es werden nur Meldungen mit einem Schweregrad angezeigt, der größer oder gleich dem im Handler definierten Schweregrad ist.
Der Autor zeigt, wie man den Schweregrad während der Laufzeit ändern kann, ohne den Code zu ändern.
Ich denke, er möchte nur eine Ebene des Protokolls anzeigen. Er muss den Code wie unten modifizieren: