English 中文 Español Deutsch 日本語 Português
Шаблон проектирования MVC и возможность его использования (Часть 2): Схема взаимодействия между тремя компонентами

Шаблон проектирования MVC и возможность его использования (Часть 2): Схема взаимодействия между тремя компонентами

MetaTrader 5Торговые системы | 16 февраля 2022, 13:50
1 973 2
Andrei Novichkov
Andrei Novichkov

1. Введение

Очень коротко напомню, о чем шла речь в предыдущей статье. Шаблон MVC предполагает разделение кода на три компонента — Модель (Model), Представление (View) и Контроллер (Controller). Над каждым компонентом может работать отдельный разработчик  — создать, сопровождать, вносить правки и проч. Кроме того, будет значительно проще разобраться в ранее написанном скрипте, состоящем из функционально понятных компонентов. 

В двух словах вспомним, что представляет собой каждый компонент:

  1. Представление (View). Представление — это визуальное отображение данных пользователю. Оно получает данные от Модели, не вмешиваясь в её работу. Это может быть все, что угодно  — график, таблица, рисунок.
  2. Модель (Model). Модель обрабатывает данные. Она их получает, обрабатывает по собственным правилам и выдает результаты своей работы Представлению.  При этом Модель ничего не знает о Представлении, она просто делает доступными результаты своей работы. Исходные данные Модель получает от Контроллера, причем тоже ничего о нем не зная.
  3. Контроллер (Controller). Его основная задача  — получение данных от пользователя и взаимодействие с Моделью. Контроллер ничего не знает о внутреннем устройстве Модели, он просто передает ей исходные данные.

В этой статье мы рассмотрим возможную схему взаимодействия между этими тремя компонентами, о чем ни слова не сказали в первой части статьи и на что вполне справедливо указал один из читателей. Стоит обратить внимание, что неточный, не до конца продуманный механизм взаимодействия, сведет на нет все преимущества использования шаблона и поэтому  имеет смысл внимательно в нем разобраться.

Для экспериментов нам нужен объект. Подберем стандартный индикатор, пусть это будет WPR. Создадим для нового индикатора отдельную папку, а в ней подпапки View, Controller и Model. Поскольку выбранный индикатор весьма простой, договоримся, что для наших целей мы будем добавлять в него дополнительную функциональность с единственной целью  — продемонстрировать определенные эпизоды статьи. Никакой прикладной ценности наш индикатор иметь не будет и в реальной торговле его использовать не следует.


2. Контроллер в подробностях

Мы начнем работу с Контроллера, т.к. Контроллер отвечает за взаимодействие с пользователем. Следовательно, к Контроллеру можно отнести работу со входными параметрами, с помощью которых пользователь взаимодействует с индикатором, или советником.

2.1. Модуль исходных данных

Начнем с того, что добавим в индикатор WPR новую опцию: Пусть индикатор рисует на графике отметки, когда будет пересекать линии перекупленности / перепроданности. Эти отметки должны располагаться на определенном расстоянии от Low / High свечей. Это расстояние будет определять входной параметр dist типа int. Тогда на текущий момент входные параметры будут выглядеть так:
    //--- input parameters
    input int InpWPRPeriod = 14; // Period
    input int dist         = 20; // Distance

    У нас всего два параметра, но работы с ними уже много. Нужно проверить, не содержат ли они недопустимые значения. И в том случае, если содержат, принять необходимое решение. Например, оба параметра не могут быть меньше нуля. Предположим, что первый параметр ошибочно имеет значение минус два (-2). Один из вариантов  — "исправить" ошибочные данные, на "значение по умолчанию", равное четырнадцати (14). Второй входной параметр придется преобразовать в любом случае. Сейчас это может выглядеть так:

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

    Довольно много кода и две переменных в глобальной области видимости. Если входных параметров будет больше, то обработчик OnInit рискует превратиться в кашу, тем более что у него может быть значительно больше обязанностей, чем проверка и преобразование входных параметров. Поэтому напишем новый модуль для Контроллера, который будет заниматься исходными данными вообще, в частности — входными параметрами.

    Создадим в папке Controller файл Input.mqh и перенесем в него все "input-ы" из файла 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;
    };

    Устройство класса вполне очевидно. Мы сохраняем оба входных параметра в защищенных полях и предоставляем два метода для доступа к ним. С этого момента все компоненты  — Представление, Контроллер и Модель  — будут работать только с объектом этого класса, который создается в Контроллере, и не будут обращаться к штатным input-ам. Представление и Модель получат доступ к этому объекту и ко входным параметрам через методы 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:

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

    Ну и, конечно, давайте изменим CInputManager:

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

    Мы разделим работу между двумя новыми методами очевидным способом:

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

    Такое разделение на два метода даст разработчику еще одну полезную возможность  — обновлять некоторые параметры по необходимости. Просто добавим общедоступный метод Update():

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

    Нельзя назвать смешение в одном классе (CInputParam) входных параметров, задаваемых пользователем, и данных, получаемых от терминала, идеальным решением. Дело в определенном несоответствии принципам. Это несоответствие заключается в разной степени изменяемости кода. Входные параметры разработчик меняет легко и часто. Название отдельного параметра, его тип, можно убрать один параметр, добавить несколько новых. Такой стиль работы является одной из причин, по которой входные параметры нами были вынесены в отдельный модуль. С данными, получаемыми вызовом функций SymbolInfoХХХХ() ситуация совершенно иная, Разработчик в значительно меньшей степени склонен совершать здесь какие-то изменения. Следующая причина — различные источники. В первом случае это пользователь, во втором — терминал.

    Устранить описанные замечания легко. Для этого достаточно разбить все исходные данные на два подмодуля. Один из них будет работать со входными параметрами, второй с данными терминала. Нужен третий? Например, для работы с файлом конфигурации, содержащим XML или JSON? Пишем и добавляем третий подмодуль. Потом создаем композицию в классе CInputParam, а класс CInputManager оставим в том виде, в котором он есть. Конечно, это определенное усложнение кода и поэтому этой работой мы здесь заниматься не станем, ввиду простоты нашего тестового индикатора. Но для более сложных скриптов такой подход может быть оправдан.

    Стоит остановиться на одном моменте. Для чего нам вообще понадобился второй класс CInputManager? Все методы, которые есть в этом классе, можно безболезненно перенести в базовый класс CInputParam. Однако для такого подхода есть причина. Не стоит давать всем компонентам возможность вызывать методы Initialize(), Update() и аналогичные из класса CInputManager. Поэтому в Контроллере будет создан объект типа CInputManager, но прочие компоненты получат доступ к его базовому классу CInputParam. Таким образом разработчик застрахуется от повторных инициализаций и неожиданных вызовов Update(...) из других компонентов.


    2.2. Класс CController

    Создадим файл Controller.mqh в папке Controller. Сразу подключим файл с модулем исходных данных и создадим класс CController в этом файле. Добавим в класс закрытое поле:

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

    Мы инициализируем модуль с исходными данными в методе 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 файл Model.mqh с классом CModel и закрытое поле в Контроллере типа CModel. Теперь нужно дать возможность Модели обращаться к исходным данным. Это можно сделать двумя способами. Во-первых, продублировать в Моделе все нужные ей исходные данные, а затем инициализировать их, используя методы вида SetXXX(...):

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

    Если исходных данных будет много, то это приведет к большому количеству функций SetXXX(), чего бы хотелось избежать.

    Во-вторых, мы можем передать в Модель указатель на обьект класса CInputParam из Контроллера:

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

    Теперь Модель может получать исходные данные, используя множество функций GetXXX():

    pInput.GetPeriod();

    Но и этот способ плох. Чем занимается Модель? Принимает решения. В ней выполняются основные вычисления. Она выдает окончательный результат. Это сосредоточение бизнес логики, которая в наименьшей степени подвергается изменениям. Например, если разработчик пишет советник, основанный на пересечении двух скользящих средних, то факт такого пересечения и необходимость входа в рынок будет определяться в Модели. Разработчик может поменять набор входных параметров, способы вывода, добавить / убрать трал, но все это не коснется Модели. Пересечение двух средних останется. Однако, делая такую запись в файле с классом Модели:

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

    Мы, тем самым, устанавливаем зависимость Модели от модуля Контроллера с исходными данными! Контроллер говорит Модели: "Вот такие исходные данные у меня есть. Бери их. А если я внесу изменения, то тебе придется это учесть и измениться самой". Т.е. самый важный, центральный, редко изменяемый компонент поставлен в зависимость от часто и легко изменяемого модуля. Все должно быть наоборот. Модель должна сказать Контроллеру: "Ты проводишь инициализацию. Вот такие данные мне нужны для работы. Будь добр мне их дать".

    Для реализации такого условия, строку, включающую Input.mqh (и ей подобные строки) нужно удалить из файла с классом CModel. А затем как то определить, каким именно образом Модель хочет получать исходные данные. Выполним эту задачу создав файл в папке с Моделью с названием 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, но делать этого не станем. У нас по-прежнему актуальна задача не допустить безконтрольного вызова Ininialize() и Update(). Поэтому возможность использовать указатель на CInputParam вместо указателя на IInputBase может понадобиться для тех модулей, у которых мы не захотим установить зависимость подключением файла InputBase.mqh с определением интерфейса.

    Стоит сделать несколько замечаний по проделанной работе.
    1. У нас НЕ образовалась новая зависимость в Модели. Добавленный интерфейс является её частью.
    2. Поскольку у нас все таки очень простой пример, в этот интерфейс могли бы быть добавлены все методы GetXXX(), включая и те, которые к Модели не относятся (GetBckColor() и GetDistance()).

    Перейдем к основным вычислениям, которыми занимается Модель. В нашем случае, Модель, на основании данных, полученных от Контроллера, будет рассчитывать значения индикатора. Для этого напишем метод Tick(...), такой же, как и в Контроллере. Затем перенесем в этот метод код из оригинального индикатора WPR, добавим вспомогательные методы. Т.е. Модель в нашем случае получилась практически идентичной коду из обработчика OnCalculate оригинального индикатора.

    Однако у нас появилась проблема  — индикаторный буфер. Есть необходимость записывать данные непосредственно туда. Однако, индикаторный буфер в Модель помещать не правильно, его место в Представлении. Поэтому пойдем уже знакомым путем. Создадим файл IOutputBase.mqh в папке, где находится Модель. в этом файле напишем интерфейс:

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

    Первый метод сохраняет значения по указанному индексу, второй их возвращает. В последствии Представление реализует этот интерфейс, а пока мы изменим метод инициализации Модели с целью получения указателя на новый интерфейс и добавим закрытое поле:

       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. Представление

      Последний компонент создаваемого индикатора  — Представление. Он отвечет за отображение предоставляемых Моделью данных. Кроме того, Представление, наряду с модулем исходных данных, представляет собой часто изменяемый, обновляемый компонент. Добавить еще один буфер, изменить стиль, цвет по умолчанию и прочее  — все эти, столь часто используемые изменения, приходятся именно на Представление. И еще одно обстоятельство, на которое стоит обратить внимание  — изменения в Представлении часто являются следствием изменений в модуле исходных данных и наоборот. Еще одна причина, что бы держать Представление и модуль исходных данных подальше от Модели.

      Начнем работу уже привычным способом. В папке View создадим класс class CView. Подключим файл IOutputBase.mqh. В классе Представления создадим уже хорошо знакомый метод Initialize(...). Обратите внимание, мы не создавали в Моделе и здесь, в Представлениии, не создаем методов Update(...), или Release(...). Пока наш индикатор не требует их присутствия.

      Добавим индикаторный буфер в качестве обычного закрытого поля, реализуем контракт IOutputBase, а в метод Initialize(...) спрячем все вызовы IndicatorSetХХХ, PlotIndexSetХХХ и т.д. , тем самым убрав большинство макросов из главного файла индикатора:

      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 и тот, который мы сделали только что и который находится в прилагаемом архиве:

      Вполне очевидно, что их показания одинаковы. Однако, давайте попробуем усложнить индикатор, внося дополнительный функционал по нами же разработанным правилам.


      5. Работа с новым индикатором

      Например, допустим, что имеется необходимость динамически менять способ отрисовки индикатора с линии на гистограмму. Давайте добавим этот функционал, прежде всего для того, что бы посмотреть насколько удобнее это стало делать, или, напротив, сложнее.

      Во-первых, нам понадобится способ сигнализации. Пусть это будет некий графический объект, при щелчке мышью на котором, линия будет меняться на гистограмму и наоброт. Сделаем этот объект в виде кнопки в подокне индикатора:

      Для создания, инициализации, хранения и удаления графического объекта "кнопка" создадим класс CButtonObj. Код этого класса не сложен и здесь приводиться не будет. Управление объектом этого класса, а следовательно и кнопкой, будет выполнять Контроллер, что и понятно  — эта кнопка представляет собой элемент взаимодействия с пользователем, чем и занимается Контроллер.

      Теперь нужно добавить обработчик 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 основного файла индикатора, выглядит так:

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


      Метод проверяет, на нужном ли графическом объекте произошел щелчок мыши и переключает способ отображения в Представлении:

      Все дополнения внесены весьма быстро. Если бы пришлось выполнять аналогичные действия через год, то это не потребовало намного больше времени. Разработчик помнит структуру скрипта, за что отвечает каждый компонент, поэтому ориентироваться можно и в том случае, если утрачена документация, или забылись принципы работы.


      7. Комментарии к коду

      Теперь, когда работа завершена, позволим себе выполнить некоторый анализ.

      1. Наша Модель практически не имеет зависимостей. Контроллер же наоборот, зависит практически от всех других модулей, если судить по набору #include в начале файла. Так ли это? Формально, ответ — Да. Вместе с включением файла, разработчик устанавливает зависимость. Специфика нашего Контроллера в том, что он создает модули, контролирует их жизненный цикл, транслирует им события. Контроллер служит "мотором", обеспечивает динамику, что вполне логично вписывается в его изначальную задачу взаимодействия с пользователем.
      2. Все Компоненты содержат очень похожие методы  — Initialize, Update, Release. Напрашивается логичный вопрос о создании некоего базового класса с набором виртуальных методов. Да, сигнатура того же метода Initialize отличается для разных компонент, но это можно попытаться преодолеть.
      3. Вполне возможно, что более привлекательным вариантом (хоть и более сложным) был бы вариант CInputManager таким образом возвращающий указатели на интерфейсы:
        class CInputManager {
          ...
         public:
           InputBase*   GetInput();
          ...
        };
        Этот схематичный набросок, в случае реализации, позволит отдельным компонентам иметь доступ только к к ограниченному набору входных параметров. Здесь этого мы делать не станем, но заметим, что такое большое внимание, которое был уделено модулю входных параметров на протяжении всей статьи, обусловлено желанием показать возможные подходы к построению и прочих модулей, необходимых в работе. Например, компонент CView не обязательно должен реализовывать интерфейс IOutputBase, как сделано это в статье, путем иерархических отношений, но избрав некоторую форму композиции, как было только что предложено.


        8. Заключение

        На этом поднятая тема считается завершенной. В первой статье шаблон MVC был рассмотрен в основном, в этой же мы углубились в тему, изучив возможный вариант взаимодействия между отдельными компонентами шаблона. Конечно же, тема не из простых. Но и эффект, в случае правильного применения полученных сведений, может быть весьма полезным.


        Программы, используемые в статье:

         # Имя
        Тип
         Описание
        1 WPR_MVC.zip Архив
        Перестроенный индикатор WPR.

        Прикрепленные файлы |
        WPR_MVC.ZIP (18.77 KB)
        Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
        Daniil Kurmyshev
        Daniil Kurmyshev | 21 февр. 2022 в 00:19
        Андрей, спасибо за статью.

        Какие хочется внести идеи в Ваш проект...

        1. Во входных параметрах можно использовать без знаковые типы, тогда терминал просто не позволяет пользователю вводить отрицательные значения, например uint.

        2. Не рекомендую переопределять параметры на входе к значению по умолчанию, иначе когда вы будете использовать тестер стратегий, получите очень много одинаковых прогонов и не только по этому, второй момент, что это скрыто от глаз и пользователь не поймет, что допускает ошибку, лучше сказать об ошибке и прекратить работу.

        3. Рекомендую использовать словари для хранения данных и переменных в вашем случае они подойдут идеально, код можно уменьшить в разы даже в больших проектах.

        4. Используйте стандартные классы терминала, например для создания индикаторов и т.п. не делайте велосипедов, вряд-ли Ваш код будут использовать опытные разработчики, но вы очень сильно прокачаете навык используя стандартные классы.

        5. Используйте виртуальные методы, там где это допустимо, чтобы развязать руки другим разработчикам, которые будут использовать ваши классы и наследоваться от них, чтобы не изменяли ваш класс напрямую.
        Andrei Novichkov
        Andrei Novichkov | 21 февр. 2022 в 10:57
        Спасибо за комментарий. Я представляю статью (все свои статьи), как нечто подталкивающее к реакции, к самостоятельному творчеству. Ни в коем случае не как догму какую то. Поэтому Ваши замечания весьма кстати, из них можно почерпнуть много полезного.
        Веб-проекты (Часть III): Система авторизации Laravel/MetaTrader 5 Веб-проекты (Часть III): Система авторизации Laravel/MetaTrader 5
        В этот раз создадим систему авторизации в торговом терминале MetaTrader 5 на чистом MQL5. Пользователи приложения смогут зарегистрироваться в системе, предоставив свои учётные данные, чтобы впоследствии можно было авторизоваться и получить доступ, к каким-нибудь данным, которые хранятся в серверной части приложения.
        Использование класса CCanvas в MQL приложениях Использование класса CCanvas в MQL приложениях
        Статья об использовании класса CCanvas в MQL приложениях с подробным разбором и примерами, что даёт пользователю понимание основ работы с данным инструментом
        Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм Графика в библиотеке DoEasy (Часть 97): Независимая обработка перемещения объектов-форм
        В статье рассмотрим реализацию независимого перемещения мышкой любых объектов-форм, а также дополним библиотеку сообщениями об ошибках и новыми свойствами сделок, ранее уже введёнными в терминал и MQL5.
        Веб-проекты (Часть II): Система авторизации Laravel/Nuxt Веб-проекты (Часть II): Система авторизации Laravel/Nuxt
        В этой статье создадим систему авторизации через браузерное приложение и через торговый терминал MetaTrader 5. Можно будет зарегистрироваться в системе, указав свои учётные данные.