English Русский 中文 Español 日本語 Português
MVC-Entwurfsmuster und seine Anwendung (Teil 2): Diagramm der Interaktion zwischen den drei Komponenten

MVC-Entwurfsmuster und seine Anwendung (Teil 2): Diagramm der Interaktion zwischen den drei Komponenten

MetaTrader 5Handelssysteme | 5 April 2022, 10:47
293 0
Andrei Novichkov
Andrei Novichkov

1. Einführung

Lassen Sie mich kurz an den Inhalt des vorherigen Artikels erinnern. Nach dem MVC-Muster wird der Programmcode in drei Komponenten aufgeteilt: Model, View (Darstelung) und Controller. Jede der Komponenten kann von einem separaten Programmierer oder einem separaten Team entwickelt werden: Sie erstellen, unterstützen und aktualisieren sie. Außerdem ist es immer einfacher, einen Skriptcode zu verstehen, wenn er aus funktional klaren Komponenten besteht. 

Werfen wir einen Blick auf die einzelnen Komponenten.

  1. View. View ist für die visuelle Darstellung von Informationen zuständig. Sie empfängt Daten vom Modell, ohne sich in dessen Verarbeitung einzumischen. Dies kann alles Visuelle sein: Chart, Tabelle, Bild.
  2. Model. Model verarbeitet Daten. Es empfängt Daten, verarbeitet sie nach einigen internen Regeln und liefert die Ergebnisse der Operation an View. Model weiß jedoch nichts über View und stellt nur seine Operationsergebnisse zur Verfügung. Model empfängt Quelldaten vom Controller, weiß aber auch nichts über diesen.
  3. Controller. Seine Hauptaufgabe ist es, Daten vom Nutzer zu empfangen und mit dem Model zu interagieren. Der Controller weiß nichts über die interne Struktur des Modells, da er nur die Quelldaten an ihn weitergibt.

In diesem Artikel werden wir ein Diagramm der möglichen Interaktion zwischen diesen drei Komponenten betrachten. Dieser Aspekt wurde im ersten Artikel nicht behandelt, und einer der Leser hat ihn in den Kommentaren erwähnt. Wenn der Interaktionsmechanismus nicht gründlich bedacht wird oder ungenau ist, können alle Vorteile der Verwendung des Musters zunichte gemacht werden. Deshalb sollte diesem Thema besondere Aufmerksamkeit gewidmet werden.

Wir brauchen ein Objekt für Experimente. Wir werden einen Standardindikator, sagen wir WPR, verwenden. Für den neuen Indikator sollte ein eigener Ordner angelegt werden. Dieser Ordner sollte drei Unterordner haben: View, Controller und Model. Da der gewählte Indikator sehr einfach ist, werden wir weitere zusätzliche Funktionen hinzufügen, nur um einzelne Ideen aus dem Artikel zu demonstrieren. Der Indikator wird keinen praktischen Wert haben und sollte nicht im realen Handel verwendet werden.


2. Controller im Detail

Wir beginnen mit dem Controller, da er für die Interaktion mit dem Nutzer verantwortlich ist. So kann der Controller Operationen mit Eingabeparametern durchführen, durch die der Nutzer mit dem Indikator oder Expert Advisor interagiert.

2.1. Quelldaten-Modul

Beginnen wir damit, dem WPR-Indikator eine neue Option hinzuzufügen: Der Indikator zeichnet kleine Schildchen auf dem Chart, wenn er überkaufte/überverkaufte Niveaus überschreitet. Diese Markierungen sollten in einem bestimmten Abstand zu den Tiefst- und Höchstständen der Kerzen positioniert werden. Der Abstand wird durch den Parameter 'dist' vom Typ int bestimmt. Die Eingabeparameter lauten also wie folgt:
    //--- input parameters
    input int InpWPRPeriod = 14; // Period
    input int dist         = 20; // Distance
    

    Wir haben nur zwei Parameter, die jedoch viel Arbeit erfordern. Es muss sichergestellt werden, dass die Parameter keine ungültigen Werte enthalten. Falls doch, sind weitere Maßnahmen erforderlich. Zum Beispiel dürfen nicht beide Parameter kleiner als Null sein. Angenommen, der erste Parameter wurde fälschlicherweise auf -2 gesetzt. Eine der möglichen Maßnahmen besteht darin, die ungültigen Daten zu korrigieren, indem man ihn auf einen Standardwert setzt, der gleich 14 ist. Der zweite Eingabeparameter sollte auf jeden Fall umgewandelt werden. So könnte es bei diesem Schritt aussehen:

    //--- input parameters
    input int InpWPRPeriod = 14; // Period
    input int dist         = 20; // Distance
    
    int       iRealPeriod;
    double    dRealDist;
    //+------------------------------------------------------------------+
    //| Custom indicator initialization function                         |
    //+------------------------------------------------------------------+
    int OnInit() {
    
       if(InpWPRPeriod < 3) {
          iRealPeriod = 14;
          Print("Incorrect InpWPRPeriod value. Indicator will use value=", iRealPeriod);
       }
       else
          iRealPeriod = InpWPRPeriod;
    
       int tmp = dist;
    
       if (dist <= 0) {
          Print("Incorrect Distance value. Indicator will use value=", dist);
          tmp = 14;      
       }      
       dRealDist = tmp * _Point;
       
       .....
       
       return INIT_SUCCEEDED;
    }
    
    

    Wir haben einen ziemlich langen Programmcode und zwei Variablen im globalen Bereich. Wenn es mehr Parameter gibt, entsteht in OnInit ein Durcheinander. Außerdem kann der Handler neben der Validierung und Konvertierung von Eingabeparametern noch andere Aufgaben haben. Aus diesem Grund erstellen wir ein neues Modul für den Controller, das alle Quelldaten, einschließlich der Eingabeparameter, verarbeiten wird.

    Im Ordner Controller erstellen wir die Datei Input.mqh und verschieben alle Eingaben aus WPR.mq5 in diese Datei. In dieselbe Datei schreiben wir die Klasse CInputParam, um mit den vorhandenen Parametern zu arbeiten:

    class CInputParam {
       public:
          CInputParam() {}
         ~CInputParam() {}
         
         const int    GetPeriod()   const {return iWprPeriod;}
         const double GetDistance() const {return dDistance; }
         
       protected:
          int    iWprPeriod;
          double dDistance;
    };
    

    Die Struktur der Klasse sollte klar sein. Die beiden Eingabeparameter werden in geschützten Feldern gespeichert, und es gibt zwei Methoden für den Zugriff auf sie. Von nun an werden alle Komponenten, einschließlich View, Controller und Model, nur noch mit diesem Klassenobjekt arbeiten, das im Controller erstellt wird. Die Komponenten werden also nicht mit regulären Eingaben arbeiten. View und Model greifen auf dieses Objekt und die Eingabeparameter über die Methoden GetXXX dieses Objekts zu. Auf den Parameter InpWPRPeriod wird über GetPeriod() zugegriffen, und auf 'dist' wird über die Methode GetDistance() zugegriffen.

    Bitte beachten Sie, dass das Feld dDistance den Typ double hat und zur Verwendung bereit ist. Nun wurden beide Parameter überprüft und sind zweifelsohne korrekt. Es werden jedoch keine Prüfungen innerhalb der Klasse durchgeführt. Alle Prüfungen werden in einer anderen Klasse, CInputManager, durchgeführt, die wir in dieselbe Datei schreiben werden. Die Klasse ist einfach und sieht wie folgt aus:

    class CInputManager: public CInputParam {
       public:
                      CInputManager(int minperiod, int defperiod): iMinPeriod(minperiod),
                                                                   iDefPeriod(defperiod)
                      {}                                             
                      CInputManager() {
                         iMinPeriod = 3;
                         iDefPeriod = 14;
                      }
                     ~CInputManager() {}
               int   Initialize();
          
       protected:
       private:
               int    iMinPeriod;
               int    iDefPeriod;
    };
    

    Die Methode verfügt über die Methode Initialize(), die die erforderlichen Prüfungen durchführt und die Eingaben gegebenenfalls umwandelt. Wenn die Initialisierung fehlschlägt, gibt die Methode einen anderen Wert als INIT_SUCCEEDED zurück:

    int CInputManager::Initialize() {
    
       int iResult = INIT_SUCCEEDED;
       
       if(InpWPRPeriod < iMinPeriod) {
          iWprPeriod = iDefPeriod;
          Print("Incorrect InpWPRPeriod value. Indicator will use value=", iWprPeriod);
       }
       else
          iWprPeriod = InpWPRPeriod;
          
       if (dist <= 0) {
          Print("Incorrect Distance value. Indicator will use value=", dist);
          iResult = INIT_PARAMETERS_INCORRECT;
       } else      
          dDistance = dist * _Point;
       
       return iResult;
    

    Erinnern Sie sich noch daran, wie oft wir Funktionen des Typs SymbolInfoХХХХХ(...) und ähnliche aufrufen müssen? Wir tun dies, wenn wir Symbolparameter, Daten zum Öffnen von Fenstern usw. abrufen müssen. Dies geschieht sehr häufig. Diese Funktionsaufrufe sind im gesamten Text implementiert, und sie können wiederholt werden. Aber sie sind auch Quelldaten, ähnlich wie Eingabedaten.

    Nehmen wir an, wir müssen den Wert von SYMBOL_BACKGROUND_COLOR ermitteln und ihn dann in der Ansicht verwenden. Erstellen wir ein geschütztes Feld in der Klasse CInputParam:

    class CInputParam {
         ...
         const color  GetBckColor() const {return clrBck;    }
         
       protected:
               ...
               color  clrBck;
    };
    

    Außerdem sollten wir CInputManager bearbeiten:

    class CInputManager: public CInputParam {
       public:
               ...
               int   Initialize();
          
       protected:
               int    VerifyParam();
               bool   GetData();
    }; 
    

    Die Arbeit wird auf die beiden neuen Methoden aufgeteilt:

    int CInputManager::Initialize() {
       
       int iResult = VerifyParam();
       if (iResult == INIT_SUCCEEDED) GetData();
       
       return iResult;
    }
    
    bool CInputManager::GetData() {
      
      long tmp;
    
      bool res = SymbolInfoInteger(_Symbol, SYMBOL_BACKGROUND_COLOR, tmp);
      if (res) clrBck = (color)tmp;
      
      return res;
    
    }
    
    int CInputManager::VerifyParam() {
    
       int iResult = INIT_SUCCEEDED;
       
       if(InpWPRPeriod < iMinPeriod) {
          iWprPeriod = iDefPeriod;
          Print("Incorrect InpWPRPeriod value. Indicator will use value=", iWprPeriod);
       }
       else
          iWprPeriod = InpWPRPeriod;
          
       if (dist <= 0) {
          Print("Incorrect Distance value. Indicator will use value=", dist);
          iResult = INIT_PARAMETERS_INCORRECT;
          dDistance = 0;
       } else      
          dDistance = dist * _Point;
       
       return iResult;
    }
    

    Diese Aufteilung in zwei Methoden bietet eine weitere nützliche Möglichkeit: die Möglichkeit, einige der Parameter bei Bedarf zu aktualisieren. Fügen wir eine öffentliche Methode Update() hinzu:

    class CInputManager: public CInputParam {
       public:
               ...
               bool   Update() {return GetData(); }
               ...
    }; 
    
    

    Die Kombination von Eingabeparametern, die vom Nutzer angegeben werden, und solchen, die vom Terminal empfangen werden, in einer Klasse (CInputParam) kann kaum als perfekte Lösung angesehen werden. Denn sie ist mit den Grundsätzen unvereinbar. Diese Inkonsistenz besteht in den unterschiedlichen Graden der Veränderbarkeit des Programmcodes. Der Entwickler kann Eingaben häufig und einfach ändern: den Namen eines einzelnen Parameters, seinen Typ ändern, einen Parameter entfernen oder neue hinzufügen. Diese Arbeitsweise ist einer der Gründe, warum Eingabeparameter in einem separaten Modul implementiert sind. Anders verhält es sich mit den Daten, die über den Funktionsaufruf SymbolInfoХХХХХ() empfangen werden: hier ist der Entwickler viel weniger geneigt, Änderungen vorzunehmen. Der nächste Grund ist, dass die Quellen unterschiedlich sind. Im ersten Fall ist es der Nutzer, im zweiten Fall ist es das Terminal.

    Es ist nicht schwierig, diese Bemerkungen zu korrigieren. Zu diesem Zweck können wir alle Quelldaten in zwei Untermodule aufteilen. Eines davon wird mit Eingabeparametern arbeiten, das andere mit Terminaldaten. Was ist, wenn wir ein drittes Modul benötigen? Zum Beispiel, um mit der Konfigurationsdatei zu arbeiten, die XML oder JSON enthält? Wir schreiben ein weiteres Submodul und fügen es hinzu. Dann erstellen wir eine Zusammenstellung in der Klasse CInputParam, während wir die Klasse CInputManager unverändert lassen. Das verkompliziert natürlich den gesamten Programmcode. Wir werden dies also nicht implementieren, da unser Testindikator sehr einfach ist. Für komplexere Skripte kann dieser Ansatz jedoch gerechtfertigt sein.

    Es gibt einen Moment, dem besondere Aufmerksamkeit gewidmet werden sollte. Warum brauchen wir die zweite Klasse CInputManager? Alle Methoden dieser Klasse können einfach in die Basisklasse CInputParam verschoben werden. Für diese Lösung gibt es jedoch einen Grund. Man sollte nicht allen Komponenten erlauben, Initialize(), Update() und ähnliche Methoden aus der Klasse CInputManager aufzurufen. Deshalb wird im Controller ein Objekt vom Typ CInputManager erzeugt, während andere Komponenten auf dessen Basisklasse CInputParam zugreifen. Dies schützt vor wiederholten Initialisierungen oder unerwarteten Aufrufen von Update(...) aus anderen Komponenten.


    2.2. Die Klasse CController

    Wir erstellen die Datei Controller.mqh im Ordner Controller. Wir verbinden die Datei mit dem Quelldatenmodul und erstellen die Klasse CController in dieser Datei. Außerdem fügen wir ein privates Feld in die Klasse ein:

    CInputManager pInput;  

    Jetzt müssen wir dieses Modul initialisieren, die Möglichkeit bieten, Daten darin zu aktualisieren und möglicherweise andere Methoden aufzurufen, die noch nicht implementiert wurden. Zumindest brauchen wir die Methode Release(), die einige von den Quelldaten verwendete Ressourcen bereinigen und freigeben kann. Sie wird in unserem Fall jetzt nicht benötigt, kann aber später notwendig sein. 

    Fügen wir also der Klasse die Aktualisierungsmethoden Initialize() und Update() hinzu. Jetzt sieht er wie folgt aus:

    class CController {
     public:
                     CController();
                    ~CController();
       
               int   Initialize();
               bool  Update();   
     protected:
     private:
       CInputManager* pInput;  
    };
    
    ...
    
    int CController::Initialize() {
       
       int iResult = pInput.Initialize();
       if (iResult != INIT_SUCCEEDED) return iResult;
       
       return INIT_SUCCEEDED;
    }
    
    bool CController::Update() {
       
       bool bResult = pInput.Update();
       
       return bResult;
    }
    

    Wir initialisieren das Modul mit Quelldaten in der Methode Initialize() der Klasse Controller. Wenn das Ergebnis nicht zufriedenstellend ist, brechen wir die Initialisierung ab. Offensichtlich können bei einem Fehler in den Quelldaten keine weiteren Operationen durchgeführt werden.

    Ein Fehler kann auch bei der Aktualisierung von Quelldaten auftreten. Die Funktion Update() gibt in diesem Fall false zurück.

    Die nächste Aufgabe des Controllers besteht darin, anderen Komponenten den Zugriff auf sein Quelldatenmodul zu ermöglichen. Diese Aufgabe kann leicht gelöst werden, wenn der Controller andere Komponenten besitzt, d.h. das Model und die View einschließt:

    class CController {
     public:
       ...
     private:
       CInputManager* pInput;  
       CModel*        pModel;
       CView*         pView;
    }
    ...
    CController::CController() {
       pInput = new CInputManager();
       pModel = new CModel();
       pView  = new CView();
    }
    

    Der Controller wird auch für die Initialisierung, Aktualisierung und Wartung des Lebenszyklus aller Komponenten verantwortlich sein, was der Controller leicht tun kann, wenn wir die Methoden Initialize() und Update() (sowie alle anderen erforderlichen Methoden) zu den Model- und View-Komponenten hinzufügen.

    Die Hauptdatei des Indikators WPR.mq5 sieht dann wie folgt aus:

    ...
    
    CController* pController;
    
    int OnInit() {
       pController = new CController();
       return pController.Initialize();
    }
    
    ...
    
    void OnDeinit(const int  reason) {
       if (CheckPointer(pController) != POINTER_INVALID) 
          delete pController;
    }
    
    

    In OnInit() wird der Controller erstellt, der dessen Methode Initialize() aufruft. Anschließend ruft der Controller die entsprechenden Model- und View-Methoden auf. Für OnCalculate(...) eines Indikators erstellen wir beispielsweise die Methode Tick(...) im Controller und rufen sie in OnCalculate(...) der Hauptindikator-Datei auf:

    int OnCalculate(const int rates_total,
                    const int prev_calculated,
                    const datetime &time[],
                    const double &open[],
                    const double &high[],
                    const double &low[],
                    const double &close[],
                    const long &tick_volume[],
                    const long &volume[],
                    const int &spread[]) {
    
       return pController.Tick(rates_total, prev_calculated, 
                               time, 
                               open, high, low, close, 
                               tick_volume, volume, 
                               spread);
    
    }
    

    Wir werden später auf die Methode Tick(...) des Controllers zurückkommen. Achten Sie jetzt darauf, dass:

    1. Für jede Ereignisbehandlung eines Indikators können wir eine entsprechende Methode im Controller erstellen:
      int CController::Initialize() {
      
         if (CheckPointer(pInput) == POINTER_INVALID ||
             CheckPointer(pModel) == POINTER_INVALID ||
             CheckPointer(pView)  == POINTER_INVALID) return INIT_FAILED;
                  
         int iResult =  pInput.Initialize();
         if (iResult != INIT_SUCCEEDED) return iResult;
         
         iResult = pView.Initialize(GetPointer(pInput) );
         if (iResult != INIT_SUCCEEDED) return iResult;
         
         iResult =  pModel.Initialize(GetPointer(pInput), GetPointer(pView) );
         if (iResult != INIT_SUCCEEDED) return iResult;
         
        
         return INIT_SUCCEEDED;
      } 
      ...
      bool CController::Update() {
         
         bool bResult = pInput.Update();
         
         return bResult;
      }
      ...
      

    2. Die Hauptdatei des WPR.mq5-Indikators erweist sich als sehr klein und einfach.


    3. Modell

    Kommen wir nun zum wichtigsten Teil unseres Indikators, dem Modell. Das Modell ist die Komponente, die die Entscheidungen trifft. Der Controller gibt dem Modell Daten zur Berechnung, und das Modell erhält das Ergebnis. Dazu gehören auch die Quelldaten. Wir haben gerade ein Modul für die Arbeit mit diesen Daten erstellt. Dazu gehören auch Daten, die in OnCalculate(...) empfangen und an den Controller weitergegeben werden. Es können auch Daten von anderen Ereignisbehandlungen wie OnTick(), OnChartEvent() und anderen sein (sie werden in unserem einfachen Indikator nicht benötigt).

    Im bestehenden Model-Ordner erstellen wir die Datei Model.mqh mit der CModel-Klasse und einem privaten Feld vom Typ CModel im Controller. Nun sollten wir dem Model den Zugriff auf die Quelldaten ermöglichen. Dies kann auf zwei Arten geschehen. Die eine ist, die benötigten Daten im Model zu replizieren und die Daten mit SetXXX(...) Methoden zu initialisieren:

    #include "..\Controller\Input.mqh"
    
    class CModel {
     public:
       ...
       void SetPeriod(int value) {iWprPeriod = value;}   
       ...
    private:
       int    iWprPeriod;   
       ...
    };
    

    Wenn es viele Eingabedaten gibt, wird es viele Funktionen des Typs SetXXX() geben, was keine gute Lösung ist.

    Eine andere Möglichkeit besteht darin, dem Modell einen Zeiger auf das Objekt der Klasse CInputParam aus dem Controller zu übergeben:

    #include "..\Controller\Input.mqh"
    
    class CModel {
     public:
       int Initialize(CInputParam* pI){
          pInput = pI;
          return INIT_SUCCEEDED;
       }
    private:
       CInputParam* pInput;
    };
    

    Das Modell kann nun die Quelldaten mithilfe einer Reihe von GetXXX()-Funktionen empfangen:

    pInput.GetPeriod();

    Aber auch diese Methode ist nicht sehr gut. Was ist der Zweck des Modells? Es soll Entscheidungen treffen. Die wichtigsten Berechnungen werden hier durchgeführt. Es erzeugt das Endergebnis. Es sollte die Konzentration der Geschäftslogik sein, die fast unverändert bleiben sollte. Wenn der Entwickler beispielsweise einen Expert Advisor erstellt, der auf dem Schnittpunkt zweier gleitender Durchschnitte basiert, ermittelt das Modell die Tatsache eines solchen Schnittpunkts und entscheidet, ob der EA in den Markt eintreten soll. Der Entwickler kann den Satz der Eingaben, die Ausgabemethode, das Hinzufügen/Entfernen des Trailing-Stops usw. ändern. Dies hat jedoch keine Auswirkungen auf das Modell. Es wird weiterhin den Schnittpunkt zweier gleitender Durchschnitte geben. Die folgende Zeile in der Datei mit der Model-Klasse:

    #include "..\Controller\Input.mqh"

    setzt die Abhängigkeit des Models vom Controller-Modul mit Quelldaten! Der Controller teilt dem Model mit: "Ich habe diese Quelldaten. Nimm sie. Wenn ich etwas ändere, musst du das berücksichtigen und selbst etwas ändern". Das wichtigste, das zentrale und sich selten ändernde Element hängt also von einem Modul ab, das leicht und häufig geändert werden kann. Aber das Gegenteil sollte der Fall sein. Das Modell sollte den Controller anweisen: "Du machst die Initialisierung. Ich brauche Daten für die Arbeit. Gib mir die benötigten Daten".

    Um diese Bedingung umzusetzen, müssen wir die Zeile, die Input.mqh (und ähnliche Zeilen) enthält, aus der Datei mit der CModel-Klasse löschen. Dann müssen wir definieren, wie das Model die Quelldaten erhalten soll. Um diese Aufgabe zu erfüllen, erstellen wir im Ordner Model eine Datei mit dem Namen InputBase.mqh. In dieser Datei erstellen wir die folgende Schnittstelle:

    interface IInputBase {
         const int    GetPeriod()   const;
    };
    

    Folgenden Programmcode wird in die Klasse Model eingetragen:

    class CModel {
    
     public:
       ...
       int Initialize(IInputBase* pI){
          pInput = pI;
          return INIT_SUCCEEDED;
       }
       ...
    private:
       IInputBase* pInput;
    };
    

    Nehmen wir nun die folgenden Änderungen an der Klasse CInputParam vor. Wir werden die neu geschriebene Schnittstelle implementieren:

    class CInputParam: public IInputBase

    Auch hier könnten wir die Klasse CInputManage eliminieren und ihre Funktionsweise nach CInputParam verschieben. Aber wir werden das nicht tun, um unkontrollierte Aufrufe von Initialize() und Update() zu vermeiden. Die Möglichkeit, einen Zeiger auf CInputParam anstelle von IInputBase zu verwenden, könnte also für jene Module erforderlich sein, bei denen wir die Abhängigkeit vermeiden wollen, die sich aus der Verbindung von InputBase.mqh mit einer definierten Schnittstelle ergibt.

    Hier ist, was wir jetzt haben.
    1. Es hat sich keine neue Abhängigkeit im Model gebildet. Die hinzugefügte Schnittstelle ist Teil des Modells.
    2. Da wir ein sehr einfaches Beispiel verwenden, könnten alle GetXXX()-Methoden zu dieser Schnittstelle hinzugefügt werden, einschließlich derjenigen, die sich nicht auf das Model beziehen (GetBckColor() и GetDistance()).

    Kommen wir nun zu den wichtigsten Berechnungen, die das Modell durchführt. Hier wird das Model auf der Grundlage der vom Controller empfangenen Daten die Indikatorwerte berechnen. Wir müssen die Methode Tick(...) hinzufügen, genau wie im Controller. Dann werden wir Programmcode aus dem ursprünglichen WRP-Indikator in diese Methode verschieben und Hilfsmethoden hinzufügen. Unser Modell ist also fast identisch mit dem Programmcode von OnCalculate des ursprünglichen Indikators.

    Allerdings haben wir hier ein Problem: den Indikatorpuffer. Es ist notwendig, Daten direkt in den Puffer zu schreiben. Es ist jedoch nicht korrekt, den Indikatorpuffer im Model zu platzieren, da er in der View sein sollte. Also implementieren wir ihn wieder so, wie wir es schon früher getan haben. Wir erstellen die Datei IOutputBase.mqh in demselben Ordner, in dem sich das Model befindet. Die Schnittstelle schreiben wir in diese Datei:

    interface IOutputBase {
    
       void SetValue(int shift, double value);
       const double GetValue(int shift) const;
       
    };
    

    Die erste Methode speichert die Werte am angegebenen Index, während die zweite sie zurückgibt. Später wird die Ansicht diese Schnittstelle implementieren. Nun müssen wir die Initialisierungsmethode von Model so bearbeiten, dass sie einen Zeiger auf die neue Schnittstelle erhält. Fügen wir ein privates Feld hinzu:

       int Initialize(IInputBase* pI, IOutputBase* pO){
          pInput  = pI;
          pOutput = pO;
          ...
       }
          ...
    private:
       IInputBase*  pInput;
       IOutputBase* pOutput; 
    

    In der Berechnung ersetzen wir den Zugriff auf den Indikatorpuffer durch den Methodenaufruf:

    pOutput.SetValue(...);

      Die resultierende Funktion Tick(...) im Modell sieht wie folgt aus (vergleichen Sie sie mit dem ursprünglichen OnCalculate):

      int CModel::Tick(const int rates_total,const int prev_calculated,const datetime &time[],const double &open[],const double &high[],const double &low[],const double &close[],const long &tick_volume[],const long &volume[],const int &spread[]) {
      
         if(rates_total < iLength)
            return(0);
            
         int i, pOutputs = prev_calculated - 1;
         if(pOutputs < iLength - 1) {
            pOutputs = iLength - 1;
            for(i = 0; i < pOutputs; i++)
               pOutput.SetValue(i, 0);
         }
      
         double w;
         for(i = pOutputs; i < rates_total && !IsStopped(); i++) {
            double max_high = Highest(high, iLength,i);
            double min_low  = Lowest(low, iLength, i);
            //--- calculate WPR
            if(max_high != min_low) {
               w = -(max_high - close[i]) * 100 / (max_high - min_low);
               pOutput.SetValue(i, w);
            } else
                  pOutput.SetValue(i, pOutput.GetValue(i - 1) ); 
         }
         return(rates_total);
      
      }
      
      

      Hier schließen wir die Operationen mit dem Modell ab.


      4. View

      Die letzte Komponente unseres Indikators ist View. Sie ist für die Darstellung der vom Model gelieferten Daten verantwortlich. Wie das Quelldatenmodul ist auch View eine häufig aktualisierte Komponente. Alle häufigen Änderungen, wie das Hinzufügen eines Puffers, das Ändern des Stils, der Standardfarbe und andere, werden in View implementiert. Ein weiterer Aspekt, auf den wir achten sollten: Änderungen in View resultieren oft aus Änderungen im Quelldatenmodul und umgekehrt. Dies ist ein weiterer Grund, View und das Quelldatenmodul vom Modell zu trennen.

      Wir wiederholen die obigen Schritte, und erstellen die Klasse CView im Ordner View. Wir binden die Datei IOutputBase.mqh ein und erstellen in der View-Klasse die bekannte Methode Initialize(...). Beachten Sie, dass wir die Methoden Update(...) und Release(...) im Model und in der View nicht erstellen. Unser Indikator benötigt sie derzeit nicht.

      Fügen wir einen Indikatorpuffer als reguläres privates Feld hinzu, implementieren den Vertrag IOutputBase und verstecken alle IndicatorSetХХХ, PlotIndexSetХХХХ und ähnliche Aufrufe in Initialize(...). Dadurch werden die meisten Makros aus der Hauptindikator-Datei entfernt:

      class CView : public IOutputBase {
      
       private:
         const  CInputParam* pInput;
                double       WPRlineBuffer[];
            
       public:
                             CView(){}
                            ~CView(){}
                         
                int          Initialize(const CInputParam* pI);
                void         SetValue(int shift, double value);
         const  double       GetValue(int shift) const {return WPRlineBuffer[shift];}      
      };
      
      int CView::Initialize(const CInputParam *pI) {
      
         pInput = pI;
         
         IndicatorSetString(INDICATOR_SHORTNAME, NAME      );
         IndicatorSetInteger(INDICATOR_DIGITS, 2           );  
         IndicatorSetDouble(INDICATOR_MINIMUM,-100         );
         IndicatorSetDouble(INDICATOR_MAXIMUM, 0           );     
         IndicatorSetInteger(INDICATOR_LEVELCOLOR,clrGray  ); 
         IndicatorSetInteger(INDICATOR_LEVELWIDTH,1        );
         IndicatorSetInteger(INDICATOR_LEVELSTYLE,STYLE_DOT);     
         IndicatorSetInteger(INDICATOR_LEVELS, 2           ); 
         IndicatorSetDouble(INDICATOR_LEVELVALUE,0,  -20   );     
         IndicatorSetDouble(INDICATOR_LEVELVALUE,1,  -80   );   
         
         SetIndexBuffer(0, WPRlineBuffer);
         
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE   );    
         PlotIndexSetInteger(0, PLOT_LINE_STYLE, STYLE_SOLID); 
         PlotIndexSetInteger(0, PLOT_LINE_WIDTH, 1          ); 
         PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrRed     ); 
         PlotIndexSetString (0, PLOT_LABEL, NAME + "_View"  );       
         
         return INIT_SUCCEEDED;
      }
      
      void CView::SetValue(int shift,double value) {
      
         WPRlineBuffer[shift] = value;
         
      }
      

      Das war's. Wir haben den Indikator erstellt und er funktioniert. Der Screenshot zeigt beide — den originalen WPR und unseren benutzerdefinierten Indikator, der im Anhang unten zu finden ist:

      Offensichtlich sind ihre Werte identisch. Lassen Sie uns nun versuchen, zusätzliche Funktionen in den Indikator zu implementieren, entsprechend den oben genannten Regeln.


      5. Arbeit an einem neuen Indikator

      Nehmen wir an, dass wir den Zeichenstil des Indikators dynamisch von Linie auf Histogramm ändern müssen. Fügen wir diese Option hinzu, um zu sehen, ob die Implementierung neuer Funktionen einfacher geworden ist oder nicht.

      Wir brauchen eine Möglichkeit zur Signalisierung. Es wird ein grafisches Objekt sein, dessen Anklicken den Indikator von Linie auf Histogramm und umgekehrt umschalten wird. Lassen Sie uns eine Schaltfläche im Unterfenster des Indikators erstellen:

      Wir erstellen die Klasse CButtonObj, um das grafische Objekt "Button" zu initialisieren, zu speichern und zu löschen. Diese Programmcodeklasse ist sehr einfach, daher werde ich sie hier nicht zeigen. Die Klasse (und die Schaltfläche) werden vom Controller gesteuert: Diese Schaltfläche ist ein Element der Benutzerinteraktion, für das der Controller direkt verantwortlich ist.

      Die Ereignisbehandlung durch OnChartEvent tragen wir in die Hauptprogrammdatei ein und fügen die entsprechende Methode zum Controller hinzu:

      void OnChartEvent(const int     id,
                        const long   &lparam,
                        const double &dparam,
                        const string &sparam)
        {
            pController.ChartEvent(id, lparam, dparam, sparam);
        }
      

      Ein großer Teil der Änderungen wird in der Ansicht implementiert werden. Hier müssen wir eine Enumeration für ein Signal und ein paar Methoden hinzufügen:

      enum VIEW_TYPE {
         LINE,
         HISTO
      };
      
      class CView : public IOutputBase {
      
       private:
                             ...
                VIEW_TYPE    view_type;
                
       protected:
                void         SwitchViewType();
                
       public:
                             CView() {view_type = LINE;}
                             ...  
         const  VIEW_TYPE    GetViewType()       const {return view_type;}
                void         SetNewViewType(VIEW_TYPE vt);
         
      };
      void CView::SetNewViewType(VIEW_TYPE vt) {
      
         if (view_type == vt) return;
         
         view_type = vt;
         SwitchViewType();
      }
      
      void CView::SwitchViewType() {
         switch (view_type) {
            case LINE:
               PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE      ); 
               break;
            case HISTO:
               PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_HISTOGRAM ); 
               break;
         }
         ChartRedraw();
      }
      
      

      Die resultierende Controller-Methode, die in OnChartEvent der Hauptindikator-Datei aufgerufen wird, sieht wie folgt aus:

      void CController::ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) {
      
            switch (id) {
               case CHARTEVENT_OBJECT_CLICK:
                  if (StringCompare(sparam, pBtn.GetName()) == 0) {
                     if (pView.GetViewType() == LINE)
                        pView.SetNewViewType(HISTO);
                     else pView.SetNewViewType(LINE);   
                  }
                  break;      
              default:
                  break;    
            }//switch (id)
      }
      
      


      Die Methode prüft, ob die Maus auf das richtige Objekt geklickt wurde, und schaltet dann den Anzeigemodus in View um:

      Es war recht einfach und schnell, die entsprechenden Änderungen vorzunehmen. Wenn wir ein Jahr später ähnliche Änderungen vornehmen müssten, würde es nicht viel länger dauern. Der Entwickler merkt sich die Struktur des Skripts und was in den einzelnen Komponenten gemacht wird. Das Projekt ist also leicht zu pflegen, selbst wenn die Dokumentation verloren geht oder man die Projektprinzipien vergisst.


      7. Kommentare im Programmcode

      Lassen Sie uns nun eine Analyse des geschriebenen Programmcodes durchführen.

      1. Das Model hat fast keine Abhängigkeiten. Im Gegenteil, der Controller hängt von allen anderen Modulen ab: Das geht aus der Reihe von #include am Anfang der Datei hervor. Formal ist es wahr. Wenn der Entwickler eine Datei einbindet, führt er eine Abhängigkeit ein. Die Besonderheit des Controllers besteht darin, dass er Module erstellt, ihre Lebenszyklen steuert und Ereignisse an sie weiterleitet. Der Controller dient als "Motor", er sorgt für Dynamik und realisiert die Interaktion mit dem Nutzer.
      2. Alle Komponenten enthalten ähnliche Methoden: Initialisieren, Aktualisieren, Freigeben. Ein weiterer logischer Schritt kann die Erstellung einer Basisklasse mit einer Reihe von virtuellen Methoden sein. Die Signatur der Methode Initialize ist für verschiedene Komponenten unterschiedlich, aber es ist möglich, dafür eine Lösung zu finden.
      3. Eine wahrscheinlich attraktivere (wenn auch schwierigere) Variante wäre es, CInputManager Zeiger auf Schnittstellen zurückgeben zu lassen:
        class CInputManager {
          ...
         public:
           InputBase*   GetInput();
          ...
        };
        
        Diese Idee würde, wenn sie umgesetzt würde, einzelnen Komponenten nur den Zugriff auf eine begrenzte Anzahl von Eingabeparametern ermöglichen. Wir werden dies hier nicht tun. Bitte beachten Sie, dass dem Modul der Eingabeparameter im gesamten Artikel so viel Aufmerksamkeit gewidmet wurde, weil ich mögliche Ansätze für den Aufbau anderer Module zeigen wollte, die später benötigt werden könnten. So muss die Komponente CView die Schnittstelle IOutputBase nicht über hierarchische Beziehungen implementieren, wie es im Artikel geschieht. Sie kann auch eine andere Form der Komposition wählen.


        8. Schlussfolgerung

        Dieses Thema kann hier als abgeschlossen betrachtet werden. Im ersten Artikel wurde das MVC-Muster im Allgemeinen betrachtet. Diesmal haben wir das Thema vertieft, indem wir die möglichen Wechselwirkungen zwischen den einzelnen Komponenten des MVC-Musters untersucht haben. Natürlich ist dieses Thema nicht ganz einfach. Aber die bereitgestellten Informationen können, wenn sie richtig angewendet werden, sehr hilfreich sein.


        Die Programme dieses Artikels:

         # Name
        Typ
         Beschreibung
        1 WPR_MVC.zip ZIP-Archive
        Überarbeiteter WPR-Indikator

        Übersetzt aus dem Russischen von MetaQuotes Ltd.
        Originalartikel: https://www.mql5.com/ru/articles/10249

        Beigefügte Dateien |
        WPR_MVC.ZIP (18.77 KB)
        Mehrere Indikatoren auf einem Chart (Teil 01): Die Konzepte verstehen Mehrere Indikatoren auf einem Chart (Teil 01): Die Konzepte verstehen
        Heute werden wir lernen, wie man mehrere Indikatoren gleichzeitig auf einem Chart anzeigt, ohne einen separaten Bereich zu belegen. Viele Händler fühlen sich sicherer, wenn sie mehrere Indikatoren gleichzeitig beobachten (z.B. RSI, STOCASTIC, MACD, ADX und einige andere), oder in einigen Fällen sogar verschiedene Vermögenswerte, aus denen ein Index besteht.
        Mathematik im Handel: Sharpe- und Sortino-Ratio Mathematik im Handel: Sharpe- und Sortino-Ratio
        Die Kapitalrendite ist der offensichtlichste Indikator, den Anleger und unerfahrene Händler für die Analyse der Handelseffizienz verwenden. Professionelle Händler verwenden zuverlässigere Instrumente zur Analyse von Strategien, wie z.B. die Sharpe- oder die Sortino-Ratio.
        Verwendung der Klasse CCanvas in MQL-Anwendungen Verwendung der Klasse CCanvas in MQL-Anwendungen
        Der Artikel befasst sich mit der Verwendung der Klasse CCanvas in MQL-Anwendungen. Die Theorie wird von detaillierten Erklärungen und Beispielen begleitet, um ein gründliches Verständnis der CCanvas-Grundlagen zu ermöglichen.
        Grafiken in der DoEasy-Bibliothek (Teil 96): Grafiken in Formularobjekten und Behandlung von Mausereignissen Grafiken in der DoEasy-Bibliothek (Teil 96): Grafiken in Formularobjekten und Behandlung von Mausereignissen
        In diesem Artikel beginne ich mit dem Erstellen der Funktionsweise für die Behandlung von Mausereignissen in Formularobjekten und füge neue Eigenschaften und deren Verfolgung zu einem Symbolobjekt hinzu. Außerdem werde ich die Klasse der Symbolobjekte verbessern, da die Chart-Symbole jetzt neue Eigenschaften haben, die berücksichtigt und verfolgt werden müssen.