MVCデザインパターンとその可能なアプリケーション(第2部): 3つのコンポーネント間の相互作用の図

16 5月 2022, 09:53
Andrei Novichkov
0
102

1. はじめに

前の記事の内容を簡単に思い出してください。MVCパターンによると、コードはモデル、ビュー、コントローラの3つのコンポーネントに分割されます。各コンポーネントは、個別のプログラマーまたは個別のチームが開発(コンポーネントを作成、サポート、および更新)できます。さらに、スクリプトコードが機能的に明確なコンポーネントで構成されている場合は、スクリプトコードを理解するのが常に簡単になります。 

各コンポーネントを見てみましょう。

  1. ビュー:ビューは情報の視覚的表現を担当します。モデルの動作を妨げることなく、モデルからデータを受信します。これは、チャート、表、画像など、任意の視覚的なものです。
  2. モデル:モデルはデータを処理します。データを受け取り、いくつかの内部ルールに従って処理し、操作結果をビューに提供します。ただし、モデルはビューについては何も知らず、その操作結果を利用可能にするだけです。モデルはコントローラからソースデータを受け取りますが、コントローラについても何も知りません。
  3. コントローラ:その主な役割は、ユーザーからデータを受け取り、モデルを操作することです。コントローラは、ソースデータをモデルに渡すだけなので、モデルの内部構造については何も知りません。

この記事では、これら3つのコンポーネント間の可能な相互作用の図を検討します。読者から、この側面が最初の記事では取り上げられていないとのコメントをいただきました。相互作用のメカニズムが十分に考慮されていないか不正確である場合、パターンを使用することのすべての利点が損なわれる可能性があるため、このトピックには特別な注意を払う必要があります。

実験用のオブジェクトが必要なので、標準指標を使用します(たとえばWPR)。新しい指標用に別のフォルダを作成する必要があります。このフォルダには、View、Controller、Modelの3つのサブフォルダが必要です。選択した指標は非常に単純なので、記事とは別のアイデアを示すために、さらに機能を追加します。この指標には実用的な価値はないため、実際の取引では使用しないでください。


2. コントローラの詳細

まず、ユーザーとのやり取りを担当するコントローラから始めます。コントローラは、ユーザーが指標またはエキスパートアドバイザーと対話するための入力パラメータを使用して操作を実行できます。

2.1. ソースデータモジュール

WPR指標に新しいオプションを追加することから始めましょう。指標は、買われ過ぎ/売られ過ぎのレベルを超えると、チャートにラベルを描画します。これらのラベルは、ローソク足の安値/高値から一定の距離に配置する必要があります。距離は、int型の「dist」パラメータによって決定されます。入力パラメータは次のようになります。
    //--- input parameters
    input int InpWPRPeriod = 14; // Period
    input int dist         = 20; // Distance
    

    ただし、多くの作業を必要とするパラメータは2つしかありません。パラメータに無効な値が含まれていないことを確認する必要があります。含まれていた場合、さらなる行動が必要です。たとえば、両方のパラメータをゼロ未満にすることはできません。最初のパラメータが誤って-2に設定されているとします。考えられるアクションの1つは、無効なデータをデフォルト値(14)に設定して修正することです。いずれの場合も、2番目の入力パラメータを変換する必要があります。このステップでは、次のようになります。

    //--- 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;
    }
    
    

    グローバルスコープには非常に長いコードと2つの変数があります。さらにパラメータがある場合、OnInitハンドラは混乱します。さらに、ハンドラは、入力パラメータの検証と変換を除いて、他のタスクを実行できます。このため、入力パラメータを含むすべてのソースデータを処理するコントローラ用の新しいモジュールを作成しましょう。

    Controllerフォルダで、Input.mqhファイルを作成してWPR.mq5からのすべての入力をそこに移動します。同じファイルに、使用可能な既存のパラメータを処理するCInputParamクラスを記述します。

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

    クラスの構造は明確です。両方の入力パラメータはprotectedフィールドに保存され、それらにアクセスする2つのメソッドがあります。今後、ビュー、コントローラ、モデルを含むすべてのコンポーネントは、コントローラで作成されたこのクラスオブジェクトでのみ機能するようになります。コンポーネントは通常の入力では機能しなくなります。ビューとモデルは、このオブジェクトのGetXXXメソッドを使用して、このオブジェクトと入力パラメータにアクセスします。InpWPRPeriodパラメータはGetPeriod()を介してアクセスされ、「dist」はGetDistance()メソッドを使用してアクセスされます。

    dDistanceフィールドのタイプはdoubleであり、すぐに使用できます。これで、両方のパラメータがチェックされ、間違いなく正しいものになりました。ただし、クラス内ではチェックは実行されず、すべてのチェックは、同じファイルに書き込む別のクラスCInputManagerで実行されます。クラスは単純で、次のようになります。

    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;
    };
    

    このメソッドには、必要なチェックを実装し、必要に応じて入力を変換するInitialize()メソッドがあります。初期化が失敗した場合、メソッドはINIT_SUCCEEDED以外の値を返します。

    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;
    

    SymbolInfoХХХХ(...)および同類の関数を呼び出す必要がある頻度を覚えていますか。これは、銘柄パラメータを取得したり、ウィンドウデータを開いたりする必要がある場合に行います。非常に頻繁です。これらの関数呼び出しはテキスト全体に実装されており、繰り返されています。ただし、これらは、入力データと同様に、ソースデータでもあります。

    SYMBOL_BACKGROUND_COLORの値を取得し、それをビューで使用する必要があるとします。CInputParamクラスにprotectedフィールドを作成しましょう。

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

    CInputManagerも編集します。

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

    作業は2つの新しいメソッドに分割されます。

    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;
    }
    

    このように2つのメソッドに分割すると、別の便利な可能性が提供されます。必要に応じて一部のパラメータを更新する機能です。public Update()メソッドを追加しましょう。

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

    ユーザーが指定した入力パラメータと、1つのクラス(CInputParam)でターミナルから受け取ったパラメータの組み合わせは、完全なソリューションとは言えません。原則と矛盾しているからです。この不整合は、コードの可変性の程度の違いにあります。開発者は、別のパラメータの名前や型の変更、パラメータの削除、新しいパラメータの追加など、入力を頻繁かつ簡単に変更することができます。この操作スタイルは、入力パラメータが別のモジュールに実装される理由の1つです。SymbolInfoХХХХ()関数呼び出しを介して受信したデータでは状況は異なります。ここに開発者が変更を加えることははるかに少なくなります。次の理由は、ソースが異なることです。前者の場合はユーザーであり、後者の場合はターミナルです。

    これらのステートメントを修正することは難しくありません。これを行うには、すべてのソースデータを2つのサブモジュールに分割できます。それらの1つは入力パラメータで機能し、もう1つはターミナルデータを処理します。3つ目が必要な場合はどうなるでしょうか。たとえば、XMLまたはJSONを含む構成ファイルを操作するには、別のサブモジュールを作成して追加します。次に、CInputManagerクラスをそのままにして、CInputParamクラスでコンポジションを作成します。もちろん、これによってコード全体が複雑になります。テスト指標は非常に単純なので、これは実装しないことにします。」ただし、このアプローチは、より複雑なスクリプトに対して正当化できます。

    特別な注意を払うべき瞬間があります。2番目のクラスCInputManagerが必要なのはなぜでしょうか。このクラスのすべてのメソッドは、CInputParam基本クラスに簡単に移動できます。ただし、このソリューションには理由があります。すべてのコンポーネントがCInputManagerクラスからInitialize()、Update()および同様のメソッドを呼び出せるようにしてはいけません。CInputManager型のオブジェクトがコントローラに作成され、他のコンポーネントはそのCInputParam基本クラスにアクセスするようになるのはこのためです。これにより、他のコンポーネントからの繰り返しの初期化やUpdate(...)の予期しない呼び出しから保護されます。</ s3>


    2.2. CControllerクラス

    ControllerフォルダにController.mqhファイルを作成します。ファイルをソースデータモジュールに接続し、このファイルにCControllerクラスを作成します。クラスに次のprivateフィールドを追加します。

    CInputManager pInput;  

    ここで、このモジュールを初期化し、その中のデータを更新し、場合によってはまだ実装されていない他のメソッドを呼び出す可能性を提供する必要があります。少なくとも、ソースデータによって使用される一部のリソースをクリーンアップして解放できるRelease()メソッドが必要です。現在は必要ありませんが、後で必要になる可能性があります。 

    Initialize()メソッドおよびUpdate()更新メソッドをクラスに追加しましょう。次のようになります。

    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;
    }
    

    ControllerクラスのInitialize()メソッドのソースデータを使用してモジュールを初期化します。結果が不十分な場合は、初期化を解除してください。明らかに、ソースデータにエラーが発生した場合、それ以上の操作を実行することはできません。

    ソースデータの更新時にもエラーが発生する場合があります。この場合、Update()はfalseを返します。

    コントローラの次のタスクは、他のコンポーネントにそのソースデータモジュールへのアクセスを提供することです。コントローラが他のコンポーネントを所有している場合、つまりモデルとビューが含まれている場合、このタスクは簡単に解決できます。

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

    コントローラは、すべてのコンポーネントのライフサイクルの初期化、更新、保守も担当します。Initialize()メソッドとUpdate()メソッド(およびその他の必要なメソッド)をモデルコンポーネントとビューコンポーネントに追加すると、これはコントローラが簡単に実行できます。

    WPR.mq5指標のメインファイルは次のようになります。

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

    OnInit()ハンドラはコントローラを作成し、そのInitialize()メソッドを呼び出します。次に、コントローラは関連するモデルとビューのメソッドを呼び出します。たとえば、OnCalculate(...)指標ハンドラの場合、コントローラでTick(...)メソッドを作成し、メイン指標ファイルのOnCalculate(...)ハンドラで呼び出します。

    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);
    
    }
    

    後でコントローラのTick(...)メソッドに戻ります。ここで、次のことに注意してください。

    1. 指標イベントハンドラごとに、コントローラで関連するメソッドを作成できます。
      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. WPR.mq5指標のメインファイルは非常に小さく、単純であることがわかりました。


    3. モデル

    次に、指標の主要部分であるモデルに移りましょう。モデルは、決定を下すコンポーネントです。コントローラは計算用のデータをモデルに提供し、モデルは結果を受け取ります。これにはソースデータが含まれます。このデータを操作するためのモジュールを作成しました。また、これには、OnCalculate(...)ハンドラで受信され、コントローラに渡されるデータが含まれます。OnTick()、OnChartEvent()などの他のハンドラからのデータが存在する可能性があります(これらは単純な指標では必要ありません)。

    既存のModelフォルダに、CModelクラスとコントローラのCModelタイプのプライベートフィールドを使用してModel.mqhファイルを作成します。ここで、モデルがソースデータにアクセスできるようにする必要があります。これは2つの方法で行うことができます。1つは、モデル内の必要なデータを複製し、SetXXX(...)メソッドを使用してデータを初期化することです。

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

    入力データが多い場合にSetXXX()関数が多くなるので、これは適切な解決法にはなりません。

    もう1つは、コントローラからCInputParamクラスのオブジェクトへのポインタをモデルに渡すことです。

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

    モデルは、一連のGetXXX()関数を使用してソースデータを受信できるようになりました。

    pInput.GetPeriod();

    ただし、この方法もあまり良くありません。モデルの目的は何であるかの、決定を下す必要があります。主な計算はここで実行され、最終結果を生成します。それは、ビジネスロジックを集結したもので、ほとんど変わらないままであるべきです。たとえば、開発者が2つの移動平均の交差に基づいてエキスパートアドバイザーを作成する場合、モデルはそのような交差の事実を判断し、EAが市場に参入するかどうかを決定します。開発者は、入力のセットや出力方法を変更したり、トレーリングストップを追加/削除したりできます。ただし、これはモデルには影響しません。2つの移動平均の交差点は引き続き存在します。ただし、Modelクラスを含むファイルの行

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

    は、コントローラモジュールからのモデルの依存関係をソースデータで設定します。コントローラはモデルに次のように通知します。「このソースデータがあります。受け取ってください。私が何かを変更したら、これを考慮に入れて自分で変更してください。」したがって、最も重要なのは、中心的でめったに変更されない要素が、簡単かつ頻繁に変更できるモジュールに依存していることです。しかし、それは反対のはずです。モデルはコントローラに次のように指示する必要があります。「初期化を実行してください。作業するのにデータが必要です。必要なデータを私に渡してください。」

    この条件を実装するには、Input.mqhを含む行(および同様の行)をCModelクラスのファイルから削除する必要があります。次に、モデルがソースデータを受信する方法を定義する必要があります。このタスクを実装するには、ModelフォルダにInputBase.mqhという名前のファイルを作成します。このファイルで、次のインターフェイスを作成します。

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

    次のコードをモデルクラスに追加します。

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

    CInputParamクラスに次の変更を加えます。新しく作成されたインターフェイスを実装します。

    class CInputParam: public IInputBase

    ここでも、CInputManageクラスを削除し、その機能をCInputParamに移動できますが、Initialize()およびUpdate()の制御されていない呼び出しを回避するためにしないことにします。IInputBaseの代わりにCInputParamへのポインタを使用する機能は、InputBase.mqhと定義されたインターフェイスとの接続から生じる依存関係を回避したいモジュールに必要になる場合があります。

    これが今あるものです。
    1. モデルに新たな依存関係は形成されていません。追加されたインターフェイスはモデルの一部です。
    2. 非常に単純な例を使用しているため、モデルに関連しないメソッド(GetBckColor()およびGetDistance())を含め、すべてのGetXXX()メソッドをこのインターフェイスに追加できます。

    モデルによって実装される主な計算に移りましょう。ここでは、コントローラから受信したデータに基づいて、モデルが指標値を計算します。Controllerの場合と同様に、Tick(...)メソッドを追加する必要があります。次に、コードを元のWRP指標からこのメソッドに移動し、補助メソッドを追加します。モデルは元の指標のOnCalculateハンドラコードとほぼ同じになります。

    ただし、ここで問題が発生します。指標バッファです。バッファに直接データを書き込む必要がありますが、指標バッファをモデルに配置するのは正しくありません。これは、ビューにあるはずです。繰り返しになりますが、以前と同じように実装します。モデルが配置されているのと同じフォルダにIOutputBase.mqhファイルを作成します。このファイルにインターフェイスを書き込みます。

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

    最初のメソッドは指定されたインデックスに値を保存し、2番目のメソッドは値を返します。後で、ビューはこのインターフェイスを実装します。次に、モデル初期化メソッドを編集して、新しいインターフェイスへのポインタを受け取るようにする必要があります。privateフィールドを追加します。

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

    計算では、指標バッファアクセスをメソッド呼び出しに置き換えます。

    pOutput.SetValue(...);

      モデルで結果として得られるTick(...)関数は、次のようになります(元の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);
      
      }
      
      

      ここで、モデルを使用して操作を完了します。


      4. ビュー

      指標の最後のコンポーネントはビューです。モデルによって提供されるデータのレンダリングを担当します。ソースデータモジュールと同様に、ビューは頻繁に更新されるコンポーネントです。バッファの追加、スタイル、デフォルトの色の変更など、頻繁に行われるすべての変更は、ビューで実装されます。注意が必要なもう1つの側面は、ビューの変更は、多くの場合、ソースデータモジュールの変更に起因し、その逆も同様だということです。これは、ビューとソースデータモジュールをモデルから分離するもう1つの理由です。

      繰り返しますが、上記の手順を繰り返します。ViewフォルダにCViewクラスを作成します。IOutputBase.mqhファイルを接続します。ビューのクラスで、使い慣れたInitialize(...)メソッドを作成します。モデルとビューにはUpdate(...)メソッドとRelease(...)メソッドを作成しません。現在、指標はそれらを必要としません。

      指標バッファを通常のprivateフィールドとして追加し、IOutputBaseコントラクトを実装し、IndicatorSetХХХ、PlotIndexSetХХХおよび同様の呼び出しをInitialize(...)に非表示にします。これにより、メインの指標ファイルからほとんどのマクロが削除されます。

      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;
         
      }
      

      それだけです。指標は作成すると機能します。スクリーンショットは、元のWPRとカスタムWPRの両方を示しています。これは、以下の添付ファイルで入手できます。

      明らかに、それらの値は同じです。それでは、上記のルールに従って、指標に追加機能を実装してみましょう。


      5. 新しい指標での作業

      指標の描画スタイルを線からヒストグラムに動的に変更する必要があるとします。このオプションを追加して、新機能の実装が簡単になったかどうかを確認しましょう。

      シグナルを送る方法が必要です。これはグラフィカルオブジェクトになります。クリックすると、指標が線からヒストグラムに、またはその逆に切り替わります。指標サブウィンドウにボタンを作成しましょう。

      CButtonObjクラスを作成して、「Button」グラフィカルオブジェクトを初期化、保存、および削除します。このコードクラスは非常に単純なので、ここでは示しません。クラス(およびボタン)はコントローラによって制御されます。このボタンはユーザー操作要素であり、コントローラの直接の責任です。

      OnChartEventハンドラをメインプログラムファイルに追加し、関連するメソッドをコントローラに追加します。

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

      変更の大部分はビューに実装されます。ここでは、シグナルの列挙といくつかのメソッドを追加する必要があります。

      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();
      }
      
      

      メイン指標ファイルのOnChartEventハンドラで呼び出される結果のControllerメソッドは、次のようになります。

      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)
      }
      
      


      このメソッドは、マウスが正しいオブジェクトをクリックしたかどうかを確認してから、ビューの表示モードを切り替えました。

      関連する変更を追加するのは非常に簡単で迅速でした。1年後に同様の変更を実行するとしても、それほど長くはかからないでしょう。開発者はスクリプトの構造と各コンポーネントで行なったことを覚えているため、ドキュメントを紛失したり、プロジェクトの原則を忘れたりした場合でも、プロジェクトの保守は簡単です。


      7. コードコメント

      それでは、書かれたコードの分析を行いましょう。

      1. モデルにはほとんど依存関係がありません。反対に、コントローラは他のすべてのモジュールに依存しています。これは、ファイルの先頭にある#includeのセットから明らかです。正式には、それは本当です。ファイルを含める場合、開発者は依存関係を導入します。コントローラの特定の機能は、モジュールを作成し、それらのライフサイクルを制御し、イベントをそれらに転送することです。コントローラは「エンジン」として機能し、ダイナミクスを提供し、ユーザーとの対話を実装します。
      2. すべてのコンポーネントには、初期化、更新、リリースという同様のメソッドが含まれています。さらなる論理的なステップは、仮想メソッドのセットを使用して基本クラスを作成することです。Initializeメソッドのシグネチャはコンポーネントごとに異なりますが、これに対する解決策を見つけることは可能です。
      3. おそらく、より魅力的なバリアント(より難しいものですが)は、CInputManagerにインターフェイスへのポインタを返すようにすることです。
        class CInputManager {
          ...
         public:
           InputBase*   GetInput();
          ...
        };
        
        このアイデアを実装すると、個々のコンポーネントが限られた入力パラメータのセットにのみアクセスできるようになります。これはここでは行いません。記事全体を通して入力パラメータのモジュールに非常に大きな注意が払われたことに注意してください。 これは、後で必要になる可能性のある他のモジュールを構築するための可能なアプローチを示したかったためです。たとえば、CViewコンポーネントは、記事で行われているように、階層関係を介してIOutputBaseインターフェイスを実装する必要はありません。他の形式の構成を選択できます。


        8. 終わりに

        このトピックはここで完了したと見なすことができます。最初の記事では、MVCパターンが一般的に考慮されました。今回は、MVCパターンの個々のコンポーネント間の可能な相互作用を調べて、トピックを掘り下げました。もちろん、この主題はそれほど単純ではありませんが、提供された情報が適切に適用されれば、非常に役立つものです。


        以下は本稿で使用されているプログラムです。

         # 名前
        種類
         説明
        1 WPR_MVC.zip ZIPアーカイブ
        改訂されたWPR指標

        MetaQuotes Software Corp.によってロシア語から翻訳されました。
        元の記事: https://www.mql5.com/ru/articles/10249

        添付されたファイル |
        WPR_MVC.ZIP (18.77 KB)
        MQLアプリケーションでのCCanvasクラスの使用 MQLアプリケーションでのCCanvasクラスの使用
        この記事では、MQLアプリケーションでのCCanvasクラスの使用について検討します。理論には、CCanvasの基本を完全に理解するための詳細な説明と例が付属しています。
        DoEasyライブラリのグラフィックス(第95部):複合グラフィカルオブジェクトコントロール DoEasyライブラリのグラフィックス(第95部):複合グラフィカルオブジェクトコントロール
        本稿では、複合グラフィカルオブジェクトを管理するためのツールキット(拡張された標準グラフィカルオブジェクトを管理するためのコントロール)について検討します。今日は、複合グラフィカルオブジェクトの再配置から少し脱線して、複合グラフィカルオブジェクトを特徴とするチャートに変更イベントのハンドラを実装します。さらに、複合グラフィカルオブジェクトを管理するためのコントロールに焦点を当てます。
        エキスパートアドバイザーが失敗する理由の分析 エキスパートアドバイザーが失敗する理由の分析
        この記事では、通貨データの分析を示して、エキスパートアドバイザーが特定の時間領域で良好なパフォーマンスを示し他の領域でパフォーマンスが低下する理由をよりよく理解します。
        単一チャート上の複数インジケータ(第03部): ユーザー向け定義の開発 単一チャート上の複数インジケータ(第03部): ユーザー向け定義の開発
        今日はインジケータシステムの機能を初めて更新します。前回の「単一チャート上の複数のインジケータ」稿では、チャートのサブウィンドウで複数のインジケータを使用できるようにする基本的なコードについて検討しましたが、提示されたのは、はるかに大規模なシステムの出発点にすぎませんでした。