English Русский 中文 Español Deutsch 日本語
O padrão de design MVC e suas possibilidades de uso (Parte 2): Esquema de interação entre três componentes

O padrão de design MVC e suas possibilidades de uso (Parte 2): Esquema de interação entre três componentes

MetaTrader 5Sistemas de negociação | 16 junho 2022, 10:08
505 0
Andrei Novichkov
Andrei Novichkov

1. Introdução

Muito brevemente, gostaria de lembrar o que falamos no artigo anterior. O padrão MVC divide o código em três componentes, isto é: Modelo, Visualização e Controlador. Um desenvolvedor em separado trata cada componente: criar, acompanhar, editar e assim por diante. Além disso, desse modo, será muito mais fácil lidar com scripts já escritos, que consistem em componentes funcionalmente claros. 

Em poucas palavras, vamos lembrar o que é cada componente:

  1. Visualização (View): trata-se da exibição de dados para o usuário. Ela recebe dados do Modelo sem interferir em seu trabalho. Pode ser qualquer coisa, um gráfico, uma tabela ou um desenho.
  2. Modelo (Model): basicamente processa os dados. Ele os recebe, os processa de acordo com suas próprias regras e envia seus resultados para a Visualização.  Enquanto isso, o Modelo não sabe nada sobre a Visualização, e simplesmente disponibiliza o produto de seu funcionamento. O Modelo recebe os dados iniciais do Controlador e também não sabe nada sobre ele.
  3. Controlador (Controller): principalmente recebe dados do usuário e interage com o Modelo. O controlador não sabe nada sobre a estrutura interna do Modelo, ele simplesmente passa os dados iniciais para ele.

Neste artigo vamos analisar um possível padrão de interação entre estes três componentes, algo que não foi mencionado na primeira parte do artigo e que foi corretamente apontado por um dos leitores. Note que um mecanismo de interação impreciso e mal concebido anulará os benefícios do padrão, portanto, faz sentido entendê-lo completamente.

Para os experimentos, precisamos de um objeto. Vamos selecionar o indicador padrão, hoje será o WPR. Vamos criar uma pasta separada para o novo indicador e as subpastas View, Controller e Modelo nela contidas. Como o indicador selecionado é muito simples, vamos convir que acrescentaremos funcionalidade adicional a ele para nossos fins com o único propósito de demonstrar certos casos do artigo. Nosso indicador não terá nenhum valor aplicado e não deve ser utilizado na negociação real.


2. Controlador em detalhes

Começaremos com o Controlador, porque ele é responsável por interagir com o usuário. Consequentemente, os parâmetros de entrada com os quais o usuário interage com o indicador ou o EA podem ser atribuídos ao Controlador.

2.1. Módulo de dados iniciais

Vamos começar adicionando uma nova opção ao indicador WPR: O indicador deverá desenhar marcas no gráfico quando cruzar as linhas de sobrecompra/sobrevenda. Essas marcas devem estar localizadas a uma certa distância dos candles Low/High. Esta distância definirá o parâmetro de entrada dist do tipo int. No momento, os parâmetros de entrada ficarão assim:
    //--- input parameters
    input int InpWPRPeriod = 14; // Period
    input int dist         = 20; // Distance
    

    Apesar de termos apenas dois parâmetros, devemos trabalhar com eles muito. Precisamos verificar se eles contêm valores inválidos. E, no caso de conterem, teremos que tomar a decisão necessária. Por exemplo, ambos os parâmetros não podem ser menores que zero. Vamos supor que o primeiro parâmetro tenha erroneamente um valor de menos dois (-2). Uma hipótese é "corrigir" os dados errados para um "valor padrão" de quatorze (14). O segundo parâmetro de entrada terá que ser convertido de qualquer maneira. Isso pode ficar assim:

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

    Bastante código e duas variáveis no escopo global. Se houver mais parâmetros de entrada, o manipulador OnInit corre o risco de se tornar uma bagunça, especialmente porque pode ter muito mais responsabilidades do que verificar e converter parâmetros de entrada. Com isso em mente, escreveremos um novo módulo para o Controlador, módulo esse que tratará dos dados iniciais em geral e, em particular, dos parâmetros de entrada.

    Vamos criar um arquivo Input.mqh na pasta Controller e transferir todos os "inputs" do arquivo WPR.mq5 para ele. No mesmo arquivo, escreveremos a classe CInputParam para trabalhar com os parâmetros de entrada disponíveis:

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

    A estrutura da classe é bastante óbvia. Armazenamos ambos os parâmetros de entrada em campos protegidos e fornecemos dois métodos para acessá-los. A partir de agora, todos os componentes - Visualização, Controlador e Modelo - funcionarão apenas com um objeto desta classe, que é criado no Controlador, e não acessarão "inputs" regulares. A Visualização e o Modelo irão acessar este objeto e os parâmetros de entrada através dos métodos GetXXX deste objeto. Para o parâmetro InpWPRPeriod pelo método GetPeriod() e para o parâmetro dist pelo método GetDistance().

    Observe que o campo dDistance é do tipo double e está pronto para uso direto. Ambos os parâmetros já foram verificados e sem dúvida estão certos. No entanto, nenhuma verificação é executada na própria classe. Para fazer isso, usamos outra classe, nomeadamente CInputManager, que escreveremos no mesmo arquivo. Esta classe também não é complicada e fica assim:

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

    Essa classe agora tem um método Initialize() que faz todo o trabalho de verificação e conversão dos parâmetros de entrada conforme necessário. Se a inicialização falhar, esse método retornará um valor diferente de 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;
    

    Vamos lembrar: com que frequência temos que chamar funções como SymbolInfoХХХХ(...) e outras semelhantes? E com que regularidade as chamamos para obter os parâmetros de um símbolo, uma janela aberta e assim por diante? Quase constantemente. As chamadas para essas funções estão espalhadas pelo texto e podem ser repetidas. Mas eles, afinal, também são dados iniciais semelhantes aos parâmetros de entrada.

    Imaginemos que precisamos obter o valor de SYMBOL_BACKGROUND_COLOR e depois usá-lo na Visualização. Criamos um campo protegido na classe CInputParam:

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

    E, claro, vamos alterar CInputManager:

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

    Vamos dividir o trabalho entre os dois novos métodos de forma óbvia:

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

    Essa divisão em dois métodos dará ao desenvolvedor a chance de atualizar alguns parâmetros conforme necessário. Vamos apenas adicionar o método público Update():

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

    Misturar os parâmetros de entrada de uma classe (CInputParam) definidos pelo usuário e os dados recebidos do terminal não pode ser considerado uma solução ideal. Quando assim acontece é porque os princípios não são atendidos, e isto tem a ver com o nível de variação do código. O desenvolvedor altera os parâmetros de entrada com facilidade e frequência. Nós podemos remover um parâmetro, seu nome e tipo ou adicionar vários novos. Esse estilo de trabalho é uma das razões pelas quais movemos os parâmetros de entrada para um módulo separado. Com os dados obtidos chamando as funções SymbolInfoХХХХ() , a situação é completamente diferente, o desenvolvedor está muito menos inclinado a fazer qualquer alteração. A seguinte razão são as várias fontes. No primeiro caso, é o usuário, no segundo, o terminal.

    É fácil contornar os problemas descritos acima. Para fazer isso, basta dividir todos os dados de entrada em dois submódulos. Um irá trabalhar com os parâmetros de entrada, o segundo com os dados do terminal. Será que precisamos de um terceiro? Por exemplo, para trabalhar com um arquivo de configuração contendo XML ou JSON? Escrevemos e adicionamos o terceiro submódulo. Em seguida, criamos uma na classe CInputParam e deixamos a classe CInputManager como ela está. Naturalmente, isso complica o código e é por isso que não faremos esse trabalho aqui porque nosso indicador de teste é muito simples. Mas, para scripts mais complexos, esta abordagem pode ser razoável.

    Devemos parar e pensar por que precisamos mesmo da segunda classe CInputManager? Todos os métodos que estão nesta classe podem ser transferidos com segurança para a classe de base CInputParam. Mas há uma razão para fazer isto. Não devemos permitir que todos os componentes chamem os métodos Initialize(), Update() e similares da classe CInputManager. Portanto, objetos do tipo CInputManager serão criados no Controlador, mas outros componentes terão acesso à sua classe base CInputParam. Assim, o desenvolvedor se assegurará contra reinicialização e chamadas inesperadas de Update(...) a partir de outros componentes.


    2.2. Classe CController

    Vamos criar um arquivo Controller.mqh na pasta Controller. Vamos incluir imediatamente o arquivo com o módulo de dados iniciais e criar a classe CController neste arquivo. Vamos adicionar um campo privado à classe:

    CInputManager pInput;  

    Agora precisamos inicializar este módulo, facilitar a atualização de dados nele e possivelmente chamar outros métodos que ainda não foram implementados. Deveríamos criar, pelo menos, um método Release() que poderia limpar, liberar alguns recursos capturados nos dados originais. No nosso caso, isto último não é necessário, mas pode aparecer no futuro. 

    Vamos adicionar os métodos Initialize() e Update() à classe. Agora fica assim:

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

    Inicializamos o módulo com os dados iniciais no método Initialize() da classe Controlador, e se obtivermos um resultado insatisfatório, abortamos a inicialização. Obviamente, é impossível continuar o trabalho se obtivermos um erro nos dados iniciais.

    Ao atualizar os dados iniciais, também podem ocorrer erros. O método Update() indica isso retornando false.

    A próxima tarefa do Controlador é fornecer a outros componentes acesso ao seu módulo de dados iniciais. Esta tarefa é fácil de resolver se o Controlador for o dono de outros componentes, incluir um Modelo e uma Visualização:

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

    O Controlador tem então a tarefa adicional de inicializar, atualizar e acompanhar o ciclo de vida de todos os componentes, que o Controlador pode facilmente lidar se o desenvolvedor adicionar Initialize() e Update() (e outros métodos necessários) aos componentes do Modelo e Visualização.

    Nesse caso, o arquivo principal do indicador WPR.mq5 começa a ficar assim:

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

    O manipulador OnInit() cria um Controlador e chama seu método Initialize(). Em seguida, o Controlador, por sua vez, chama os métodos apropriados do Modelo e da Visualização. Por exemplo, para o manipulador do indicador OnCalculate(...), vamos criar o método Tick(...) no Controlador e chamá-lo no manipulador OnCalculate(...) do arquivo principal do indicador:

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

    Voltaremos ao método Tick(...) do Controlador um pouco mais tarde, mas por enquanto, prestemos atenção ao fato de que:

    1. Para cada manipulador de eventos do indicador, podemos criar o método correspondente no Controlador:
      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. O arquivo principal do indicador WPR.mq5 é muito simples e curto.


    3. Modelo

    Vamos passar para a parte principal do nosso indicador, o Modelo. Deixe-me lembrá-lo que o Modelo é o componente onde são tomadas as decisões. O Controlador fornece os dados ao Modelo para calcular, o Modelo recebe o resultado. Mas que dados fornece? Antes de tudo, provê os dados iniciais. E o Módulo para trabalhar com eles acabou de ser criado. Em segundo lugar, supre os dados recebidos no manipulador OnCalculate(...) e passados para o Controlador. Podem ser outros dados de outros manipuladores, como OnTick(), OnChartEvent(), etc., o que, neste caso, não é necessário.

    Vamos criar um arquivo Model.mqh com a classe CModel e um campo privado no Controlador do tipo CModel na pasta Model existente. Agora precisamos habilitar o modelo para acessar os dados iniciais. Isso pode ser feito de duas maneiras. Primeiro, duplicamos todos os dados iniciais necessários no Modelo e, em seguida, os inicializamos usando métodos como SetXXX(...):

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

    Se houver muitos dados iniciais, isso causará um grande número de funções SetXXX(), que gostaríamos de evitar.

    Em segundo lugar, podemos passar um ponteiro para um objeto da classe CInputParam do Controlador para o Modelo:

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

    Agora o Modelo pode obter os dados iniciais usando muitas funções GetXXX():

    pInput.GetPeriod();

    Mas essa também não é uma boa maneira. O que o Modelo faz? Ele toma decisões. Ele realiza cálculos básicos. Ele produz o resultado final. Isso é uma concentração da lógica empresarial, que é a menos sujeita a mudanças. Por exemplo, se o desenvolvedor escreve um EA baseado no cruzamento de duas médias móveis, então o cruzamento e a necessidade de entrar no mercado serão determinados no Modelo. O desenvolvedor pode alterar o conjunto de parâmetros de entrada, métodos de saída, adicionar/remover o trailing, mas tudo isso não afetará o Modelo. A interseção das duas médias permanecerá. No entanto, após fazer o seguinte registro no arquivo que contém a classe do Modelo:

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

    Estamos estabelecendo a dependência do Modelo no módulo do Controlador com os dados iniciais! O Controlador lhe indica aos Modelos: "Aqui estão os dados iniciais que tenho. Pegue eles. E se eu fizer mudanças, você terá que levar isso em consideração e mudar a si mesmo." Ou seja, o componente mais importante, central e pouco alterado depende de um módulo modificado com frequência e facilidade. Tudo deveria ser ao contrário. O Modelo deve indicar ao Controlador: "Você é quem faz a inicialização. Esses são os dados que preciso para trabalhar. Por favor, me dê eles."

    Para aplicar essa condição, a linha que inclui Input.mqh (e linhas semelhantes) deve ser removida do arquivo com a classe CModel. E é necessário determinar exatamente como o Modelo deseja receber os dados iniciais. Vamos realizar esta tarefa criando um arquivo na pasta que contém o Modelo chamado InputBase.mqh. Neste arquivo, criaremos a seguinte interface:

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

    e adicionamos o código necessário à classe Modelo:

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

    E vamos fazer as últimas alterações na classe CInputParam. Ela criará a interface que acabamos de escrever:

    class CInputParam: public IInputBase

    Poderíamos nos livrar novamente da classe CInputManage e transferir todos os seus recursos para CInputParam, mas não faremos isso. Nossa tarefa ainda é evitar chamadas não controladas para Ininialize() e Update(). Portanto, o uso de um ponteiro para CInputParam em vez de um ponteiro para IInputBase pode ser necessário para os módulos para os quais não desejamos estabelecer uma dependência incluindo o arquivo InputBase.mqh com a definição da interface.

    Vale a pena fazer alguns comentários sobre o trabalho realizado.
    1. NÃO tivemos uma nova dependência no Modelo. A interface adicionada faz parte dela.
    2. Como ainda temos um exemplo muito simples, todos os métodos GetXXX() poderiam ser adicionados a essa interface, inclusive aqueles que não estão relacionados ao Modelo (GetBckColor() e GetDistance()).

    Vamos passar para os principais cálculos que o Modelo faz. No nosso caso, o Modelo, com base nos dados recebidos do Controlador, calculará os valores do indicador. Para isso, escreveremos o método Tick(...), o mesmo que no Controlador. Em seguida, transferiremos o código do indicador WPR original para este método e adicionaremos métodos auxiliares. Ou seja, no nosso caso, o modelo acabou sendo quase idêntico ao código do manipulador OnCalculate do indicador original.

    No entanto, temos um problema, o buffer de indicador. Temos de escrever dados diretamente lá nele. Porém, não é correto colocar o buffer do indicador no Modelo, pois seu lugar é na Visualização. Por isso, vamos por trilhos conhecidos. Vamos criar o arquivo IOutputBase.mqh na pasta onde o Modelo está localizado. Neste arquivo vamos escrever a interface:

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

    O primeiro método armazena os valores com base no índice especificado, o segundo os retorna. Em consequência, a Visualização gera essa interface, mas por enquanto vamos alterar o método de inicialização do Modelo para obter um ponteiro para a nova interface e adicionar um campo privado:

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

    E nos cálculos, substituiremos a chamada ao buffer do indicador por uma chamada de método:

    pOutput.SetValue(...);

      Como resultado, o método Tick(...) O modelo assume o seguinte aspeto (compare-o com o manipulador OnCalculate original):

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

      Assim fica concluído nosso trabalho com o Modelo.


      4. Visualização

      O último componente do indicador que estamos criando é a Visualização. Ela será responsável por exibir os dados fornecidos pelo Modelo. Além disso, a Visualização, juntamente com o módulo de dados iniciais, é um componente atualizado e alterado com frequência. Adicionamos outro buffer, alteramos o estilo, a cor padrão e assim por diante. Todas essas alterações frequentes calham na Visualização. E mais uma circunstância que vale a pena prestar atenção, as alterações na Visualização geralmente são o resultado de alterações no módulo de dados de origem e vice-versa. Essa é outra razão para manter a Visualização e o módulo de dados de origem longe do Modelo.

      Vamos começar como de costume. Na pasta View, criamos a classe CView. Vamos incluir o arquivo IOutputBase.mqh. Na classe Visualização, vamos criar o conhecido método Initialize(...). Observemos que não criamos no Modelo e aqui, na Visualização, não criamos os métodos Update(...), ou Release(...). Até agora, nosso indicador não exige sua presença.

      Vamos adicionar um buffer de indicador como um campo privado regular, implementar o contrato IOutputBase e ocultar todas as chamadas para IndicatorSetХХХ, PlotIndexSetХХХ, etc. no método Initialize(...). , removendo assim a maioria das macros do arquivo principal do indicador:

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

      Isso é tudo, o indicador foi criado e está funcionando, ambos - o WPR original e o que acabamos de fazer e que está no arquivo anexo - são mostrados na tela:

      É bastante óbvio que suas leituras são as mesmas. No entanto, vamos tentar complicar o indicador introduzindo recursos adicionais de acordo com as regras que desenvolvemos.


      5. Trabalhando com o novo indicador

      Por exemplo, digamos que seja necessário alterar dinamicamente a forma como o indicador é desenhado, de linhas para histograma. Vamos adicionar esse recurso, em primeiro lugar, para ver o quanto se tornou mais útil ou, pelo contrário, mais difícil.

      Primeiro, precisamos de uma maneira de sinalizar. No nosso caso, será um objeto gráfico, ao clicar nele, a linha mudará para um histograma e vice-versa. Tal objeto será um botão em uma subjanela do indicador:

      Para criar, inicializar, armazenar e excluir o objeto gráfico "botão", vamos criar a classe CButtonObj. O código desta classe não é complexo e não será apresentado aqui. O controle do objeto dessa classe e, portanto, do botão, será realizado pelo Controlador, o que é lógico, pois esse botão é um elemento de interação com o usuário, que é o que o Controlador faz.

      Agora precisamos adicionar o manipulador OnChartEvent ao arquivo principal do programa e o método correspondente ao Controlador:

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

      A maioria das alterações será na Visualização. Mas aqui tudo se resume a adicionar uma enumeração para sinalização e a criar alguns métodos:

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

      Como resultado, o método do Controlador, que é chamado no manipulador OnChartEvent do arquivo principal do indicador, se parece com isso:

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


      O método verifica se o clique ocorreu mo objeto gráfico necessário e alterna o método de exibição na Visualização:

      Todas as adições foram feitas muito rapidamente. Se eu tivesse que realizar ações semelhantes em um ano, eu não demoraria muito mais do que isso. Como o desenvolvedor consegue se lembrar da estrutura do script, da função de que cada componente, é possível orientar-se mesmo que a documentação seja perdida ou os princípios do trabalho sejam esquecidos.


      7. Comentários sobre o código

      Agora que o trabalho está feito, vamos fazer algumas análises.

      1. Nosso modelo quase não tem dependências. O controlador, longe disso, depende de quase todos os outros módulos, a julgar pelo #include definido no início do arquivo. É assim mesmo? Formalmente, a resposta é sim. Junto com a inclusão do arquivo, o desenvolvedor instala a dependência. A particularidade do nosso Controlador é que ele cria módulos, controla seu ciclo de vida, transmite eventos para eles. O controlador serve como um "motor", fornece dinâmica, que se encaixa logicamente em sua tarefa original de interagir com o usuário.
      2. Todos os componentes contêm métodos muito semelhantes (Initialize, Update, Release). Com a criação de algum tipo de classe de base com um conjunto de métodos virtuais, surge uma pergunta óbvia. E, sim, a assinatura do mesmo método Initialize é diferente para componentes diferentes, mas podemos tentar superar isso.
      3. É bem possível que uma opção mais atraente (embora mais complicada) seja CInputManager que retorna ponteiros de interface desta forma:
        class CInputManager {
          ...
         public:
           InputBase*   GetInput();
          ...
        };
        
        Este esquema, se implementado, permitiria que componentes individuais tivessem acesso apenas a um conjunto limitado de parâmetros de entrada. Não faremos isso aqui, mas notamos que tanta atenção, que foi dada ao módulo de parâmetros de entrada ao longo do artigo, se deve ao desejo de mostrar possíveis abordagens para a construção de outros módulos necessários no trabalho. Por exemplo, o componente CView não precisa implementar a interface IOutputBase, como feito no artigo, por meio de relacionamentos hierárquicos, mas, sim, escolhendo alguma forma de composição, conforme sugerido.


        8. Conclusão

        Isto conclui o tópico colocado. No primeiro artigo, consideramos o padrão MVC de forma geral, no mesmo aprofundamos o tema, pois estudamos uma possível variante de interação entre os componentes individuais do modelo. Claro que o tema não é simples. Mas o efeito, se a informação obtida for aplicada corretamente, pode ser muito útil.


        Programas usados no artigo:

         # Nome
        Tipo
         Descrição
        1 WPR_MVC.zip Arquivo
        Indicador WPR reconstruído.

        Traduzido do russo pela MetaQuotes Ltd.
        Artigo original: https://www.mql5.com/ru/articles/10249

        Arquivos anexados |
        WPR_MVC.ZIP (18.77 KB)
        Desenvolvendo um EA de negociação do zero (Parte 20): Um novo sistema de ordens (III) Desenvolvendo um EA de negociação do zero (Parte 20): Um novo sistema de ordens (III)
        Vamos continuar a implementação do novo sistema de ordens . A criação deste sistema é algo que demanda um bom domínio do MQL5, além de entender como de fato a plataforma MetaTrader 5 funciona e os recursos que ela nos fornece.
        Desenvolvendo um EA de negociação do zero (Parte 19): Um novo sistema de ordens (II) Desenvolvendo um EA de negociação do zero (Parte 19): Um novo sistema de ordens (II)
        Aqui vamos desenvolver um sistema gráfico de ordens, do tipo veja o que esta acontecendo. Mas não iremos partir do zero, iremos modificar o sistema já existente adicionando ainda mais objetos e eventos ao gráfico do ativo que estamos operando.
        Usando a classe CCanvas em aplicativos MQL Usando a classe CCanvas em aplicativos MQL
        Neste artigo falaremos sobre o uso da classe CCanvas em aplicações MQL, com uma descrição detalhada e exemplos, para que o usuário tenha uma compreensão básica de como usar esta ferramenta
        Como desenvolver um sistema de negociação baseado nas bandas de Bollinger Como desenvolver um sistema de negociação baseado nas bandas de Bollinger
        Neste artigo falaremos sobre as bandas de Bollinger, um dos indicadores mais populares no mundo do trading. Discutiremos sobre análise técnica e aprenderemos a desenvolver sistemas de negociação algorítmica baseados no indicador bandas de Bollinger.