Plantilla para proyectar el MVC y posibilidades de uso (Parte 2): Esquema de interacción entre los tres componentes
1. Introducción
De forma muy breve, vamos a recordar lo comentado en el artículo anterior. El patrón MVC divide el código en tres componentes: Modelo (Model), Vista (View) y Controlador (Controller). Con cada componente puede trabajar un desarrollador independiente, ocupándose de la creación, el mantenimiento, la edición, etc. Además, resultará mucho más sencillo trabajar con un script previamente escrito, y conformado por componentes funcionalmente claros.
Vamos a recordar en pocas palabras qué supone cada componente:
- Vista (View). Entendemos por vista la representación visual de los datos para el usuario. Dicha representación recibe los datos del Modelo sin interferir en su funcionamiento. Puede ser cualquier cosa: un gráfico, un recuadro, una imagen.
- Modelo (Model). El modelo se encarga de procesar los datos: los recibe, los procesa según sus propias reglas y luego pone los resultados de su trabajo a disposición de la Vista. El Modelo no sabe nada de la Vista, solo hace accesibles los resultados de su funcionamiento. El Modelo obtiene sus datos de entrada del Controlador, sin saber tampoco nada de él.
- Controlador (Controller). Su tarea principal es recibir los datos del usuario e interactuar con el Modelo. El Controlador no sabe nada del funcionamiento interno del Modelo, simplemente le transmite los datos en bruto.
En este artículo, veremos un posible esquema de interacción entre estos tres componentes; dicho esquema no lo mencionamos en la primera parte del artículo, pero uno de los lectores lo indicó muy acertadamente. Hay que tener en cuenta que un mecanismo de interacción inexacto e incompleto anulará las ventajas de la plantilla, por lo que debemos entender este a fondo.
Para los experimentos, necesitaremos un objeto. Vamos a seleccionar un indicador estándar, por ejemplo WPR. A continuación, crearemos una carpeta aparte para el nuevo indicador, y en este, las subcarpetas View, Controller y Model. Como el indicador seleccionado es muy sencillo, le añadiremos para nuestros propios fines una funcionalidad adicional con el propósito de demostrar ciertos episodios del artículo. Nuestro indicador no tendrá ningún valor práctico, y no se deberá utilizar en el comercio real.
2. Controlador en detalle
Empezaremos por el controlador, dado que este se encarga de la interacción con el usuario. Por lo tanto, el trabajo con los parámetros de entrada que ayudan al usuario a interactuar con un indicador o asesor experto, puede ser atribuido al Controlador.
2.1. Módulo de datos de origen
//--- input parameters input int InpWPRPeriod = 14; // Period input int dist = 20; // Distance
Solo tenemos dos parámetros, pero ya hay mucho trabajo que hacer con ellos. Tenemos que comprobar si contienen valores no válidos. De ser así, tomaremos la decisión necesaria. Por ejemplo, ambos parámetros no pueden ser inferiores a cero. Supongamos que el primer parámetro tiene erróneamente un valor de menos dos (-2). Una opción es "corregir" los datos erróneos por un "valor por defecto" de catorce (14). El segundo parámetro de entrada debería ser convertido en cualquier caso. Ahora podría tener este aspecto:
//--- 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 y dos variables en el ámbito global. Si hay más parámetros de entrada, el manejador OnInit corre el riesgo de convertirse en una papilla, sobre todo porque puede tener muchas más responsabilidades que simplemente comprobar y convertir los parámetros de entrada. Por ello, vamos a escribir para el Controlador un nuevo módulo que se ocupará de los datos de entrada en general y, en particular, de los parámetros de entrada.
Así, crearemos el archivo Input.mqh en la carpeta Controller y colocaremos en él todas los "inputs" de WPR.mq5. En el mismo archivo, escribiremos la clase CInputParam para trabajar con los parámetros de entrada existentes:
class CInputParam { public: CInputParam() {} ~CInputParam() {} const int GetPeriod() const {return iWprPeriod;} const double GetDistance() const {return dDistance; } protected: int iWprPeriod; double dDistance; };
El diseño de la clase es bastante obvio. Almacenamos ambos parámetros de entrada en campos protegidos y proporcionamos dos métodos para acceder a ellos. A partir de este momento, todos los componentes: Vista, Controlador y Modelo solo trabajarán con el objeto de esta clase creado en el Controlador, y no accederán a las entradas normales. La Vista y el Modelo accederán a este objeto y a los parámetros de entrada a través de los métodos GetXXX de este objeto. Al parámetro InpWPRPeriod se accede con el método GetPeriod(), mientras que al parámetro dist se accede con el método GetDistance().
Tenga en cuenta que el campo dDistance es de tipo double y está preparado para su uso directo. Ambos parámetros ya han sido comprobados y son sin duda correctos. Sin embargo, no se realizan comprobaciones en la propia clase. Usaremos otra clase para ello: CInputManager, que escribiremos en el mismo archivo. Esta clase tampoco es compleja y tiene el aspecto que sigue:
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; };
Esta clase tiene ahora un método Initialize() que hace todo el trabajo de comprobación, así como las transformaciones necesarias de los parámetros de entrada. Si la inicialización falla, este método retornará un valor distinto a INIT_SUCCEED:
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;
Recordemos cuántas veces tenemos que llamar a funciones como SymbolInfo XXXXX(...) y otras similares para obtener los parámetros de un símbolo, una ventana abierta, etc. Casi todo el tiempo. Estas funciones se llaman en muchos lugares en el texto y pueden repetirse, pero también suponen datos de entrada que se asemejan a los parámetros de entrada.
Supongamos que necesitamos obtener el valor de SYMBOL_BACKGROUND_COLOR y luego usarlo en una vista. Vamos a crear un campo protegido en la clase CInputParam:
class CInputParam { ... const color GetBckColor() const {return clrBck; } protected: ... color clrBck; };
Y, por supuesto, también cambiaremos CInputManager:
class CInputManager: public CInputParam { public: ... int Initialize(); protected: int VerifyParam(); bool GetData(); };
Asimismo, dividiremos el trabajo entre los dos nuevos métodos de forma obvia:
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; }
Esta separación en dos métodos ofrecerá al desarrollador otra función útil: actualizar algunos parámetros según sea necesario. Basta con añadir un método público Update():
class CInputManager: public CInputParam { public: ... bool Update() {return GetData(); } ... };
No podemos decir que mezclar en una clase (CInputParam) los parámetros de entrada definidos por el usuario y los datos recibidos del terminal sea una solución ideal. El problema reside en una cierta incoherencia en los principios. Este desajuste se encuentra en los diferentes grados de modificabilidad del código. El desarrollador cambia los parámetros de entrada fácilmente y con frecuencia. El nombre de un parámetro individual, su tipo, etc. También puede eliminar un parámetro y añadir varios nuevos. Este estilo de trabajo es una de las razones por las que ponemos los parámetros de entrada en un módulo aparte. Las llamadas a la función SymbolInfo XXXXX() no son las mismas: el desarrollador es mucho menos proclive a hacer cambios aquí. La siguiente razón son las diversas fuentes. En el primer caso es el usuario, en el segundo, el terminal.
Resulta sencillo eliminar las observaciones descritas. Basta con dividir todos los datos de entrada en dos submódulos. Uno de ellos trabajará con los parámetros de entrada, y el segundo, con los datos del terminal. ¿Y si necesitamos un tercero, por ejemplo, para trabajar con un archivo de configuración que contenga XML o JSON? Vamos a escribir y añadir el tercer submódulo. Después crearemos una composición en la clase CInputParam, y dejaremos la clase CInputManager como está. Por supuesto, esto supondrá una complicación del código y no lo implementaremos aquí, ya que en nuestro en nuestro indicador de prueba debe primar la simplicidad. Pero para los scripts más complejos, este enfoque puede resultar razonable.
Vamos a considerar otro punto más. ¿Por qué necesitamos la segunda clase CInputManager? Todos los métodos que tenemos en esta clase se pueden trasladar con seguridad a la clase básica CInputParam., pero hay una razón para este enfoque. No debemos permitir que todos los componentes llamen a los métodos Initialize(), Update() y similares de la clase CInputManager. Por lo tanto, crearemos en el Controlador un objeto de tipo CInputManager, mientras que los otros componentes tendrán acceso a su clase básica CInputParam. De esta forma, el desarrollador estará protegido de la reinicialización y las llamadas inesperadas a Update(...) de otros componentes.
2.2. La clase CController
Vamos a crear el archivo Controller.mqh en la carpeta Controller. Para ello, incluiremos directamente el archivo con el módulo de datos de origen y crearemos la clase CController en este archivo. Después, añadimos un campo cerrado a la clase:
CInputManager pInput;
Ahora necesitamos inicializar este módulo, implementar la capacidad de actualizar los datos en él, y tal vez de llamar a otros métodos aún no implementados. Al menos el método Release(), que podría efectuar la limpieza, libera algunos recursos captados en los datos de origen. Esto no resulta necesario en este caso, pero dicha necesidad podría surgir en el futuro.
Vamos a añadir los métodos Initialize() y Update() a la clase. Ahora tiene este aspecto:
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 el módulo con los datos de entrada en el método Initialize() de la clase Controller y, en caso de obtener un resultado insatisfactorio, interrumpimos la inicialización. Obviamente, no resulta posible continuar si obtenemos un error en los datos iniciales.
Al actualizar los datos de origen, también pueden producirse errores. El método Update() nos indica esto retornando false.
La siguiente tarea del Controlador consiste en asegurar que los otros componentes tengan acceso a su módulo de datos de origen. Esta tarea se resuelve fácilmente si el Controlador es dueño de los otros componentes e incluye el Modelo y la Vista:
class CController { public: ... private: CInputManager* pInput; CModel* pModel; CView* pView; } ... CController::CController() { pInput = new CInputManager(); pModel = new CModel(); pView = new CView(); }
Entonces el Controlador tiene la tarea adicional de inicializar, actualizar y mantener el ciclo de vida de todos los componentes, lo cual puede ser fácilmente gestionado por el Controlador, si el desarrollador añade los métodos Initialize() y Update() (y otros necesarios) a los componentes Modelo y Vista.
En este caso, el archivo principal del indicador WPR.mq5 empezará a adquirir el aspecto siguiente:
... CController* pController; int OnInit() { pController = new CController(); return pController.Initialize(); } ... void OnDeinit(const int reason) { if (CheckPointer(pController) != POINTER_INVALID) delete pController; }
El manejador OnInit() crea un Controlador y llama a su método Initialize(). A continuación, el Controlador llama a su vez a los métodos de Modelo y Vista correspondientes. Por ejemplo, para el manejador del indicador OnCalculate(...) vamos a crear el método Tick(...) en el Controlador y llamarlo en el manejador OnCalculate(...) del archivo principal del 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); }
Volveremos al método Tick(...) del Controlador un poco más tarde. Por ahora, prestaremos atención al hecho de que:
- Para cada manejador de eventos del indicador, podemos crear el método correspondiente en el 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; } ...
- El archivo principal del indicador WPR.mq5 resulta muy breve y sencillo.
3. Modelo
Vamos a pasar a la parte principal de nuestro indicador, el Modelo. Recordemos que el Modelo es el componente en el que se toman las decisiones. El Controlador proporciona al Modelo los datos para el cálculo, y el Modelo obtiene el resultado. Pero ¿de qué tipo de datos hablamos? En primer lugar, son los datos de origen. Acabamos de crear el módulo para trabajar con ellos. En segundo lugar, son los datos obtenidos en el manejador OnCalculate(...) y transmitidos al Controlador. También pueden ser otros datos de otros manejadores: OnTick(), OnChartEvent(), etc., que no necesitaremos en este caso.
Vamos a crear en la carpeta Model.mqh un archivo con la clase CModel y un campo cerrado en el Controlador de tipo CModel. Ahora tenemos que permitir que el Modelo acceda a los datos de origen. Esto puede hacerse de dos maneras. La primera consiste en duplicar en el Modelo todos los datos de origen que este necesita y luego inicializarlo usando métodos como SetXXX(...):
#include "..\Controller\Input.mqh" class CModel { public: ... void SetPeriod(int value) {iWprPeriod = value;} ... private: int iWprPeriod; ... };
Si hay muchos datos de origen, se producirán muchas funciones SetXXX(), cosa que querríamos evitar.
La segunda forma consiste en transmitir al Modelo un puntero al objeto de la clase CInputParam desde el Controlador:
#include "..\Controller\Input.mqh" class CModel { public: int Initialize(CInputParam* pI){ pInput = pI; return INIT_SUCCEEDED; } private: CInputParam* pInput; };
El modelo ahora puede recuperar los datos de origen usando múltiples funciones GetXXX():
pInput.GetPeriod();
Pero este método tampoco es bueno. ¿Y qué hace el Modelo? Toma decisiones. Realiza cálculos básicos. Produce el resultado final. Además, concentra la lógica empresarial, que es la menos sujeta a cambios. Por ejemplo, si el desarrollador escribe un asesor experto basado en el cruce de dos medias móviles, el hecho de dicho cruce y la necesidad de entrar en el mercado serán determinados en el Modelo. El desarrollador puede cambiar el conjunto de parámetros de entrada, los métodos de muestra, la adición / eliminación de trails, pero nada de esto afectará al Modelo. El cruce de las dos medias se mantendría. Sin embargo, al realizar una entrada de este tipo en el archivo con la clase del Modelo:
#include "..\Controller\Input.mqh"
De este modo, hacemos que el Modelo dependa del módulo del Controlador con los datos de origen. El Controlador le dice al modelo: "estos son los datos en bruto que tengo. Tómalos. Y si hago cambios, tendrás que tenerlo en cuenta y cambiar tú mismo". Es decir, el componente más importante; el componente central y que rara vez se modifica, se hace depender de un módulo que modificamos con frecuencia y facilidad. Debería ser al revés. El modelo debería decirle al Controlador: "tú realizas la inicialización. Estos son los datos con los que tengo que trabajar. Ten la amabilidad de dármelos".
Para implementar esta condición, la línea que incluye Input.mqh (y las líneas similares) debe ser eliminada del archivo con la clase CModel. A continuación, deberemos determinar de alguna forma cómo quiere exactamente el Modelo recibir los datos de origen. Realizaremos esta tarea creando un archivo llamado InputBase.mqh en la carpeta con el Modelo. En este archivo, crearemos la siguiente interfaz:
interface IInputBase { const int GetPeriod() const; };
y añadiremos el código necesario a la clase del Modelo:
class CModel { public: ... int Initialize(IInputBase* pI){ pInput = pI; return INIT_SUCCEEDED; } ... private: IInputBase* pInput; };
Después, haremos un último cambio en la clase CInputParam. Este implementará la interfaz que acabamos de escribir:
class CInputParam: public IInputBase
Podríamos deshacernos de nuevo de la clase CInputManage y transferir toda su funcionalidad a CInputParam, pero no lo haremos. Todavía tenemos pendiente la tarea de prevenir la llamada no supervisada de Ininialize() y Update(). Por consiguiente, la posibilidad de usar un puntero a CInputParam en lugar de un puntero a IInputBase podría resultar necesaria para aquellos módulos en los que no queremos establecer la dependencia incluyendo el archivo InputBase.mqh con la definición de la interfaz.
- NO hemos creado una nueva dependencia en el Modelo: la interfaz añadida es una parte de él.
- Como tenemos un ejemplo muy simple, todos los métodos GetXXX() podrían haber sido añadidos a esta interfaz, incluyendo los que no pertenecen al Modelo (GetBckColor() y GetDistance()).
Vamos a pasar a los cálculos básicos que realiza el Modelo. En nuestro caso, a partir de los datos obtenidos del Controlador, el Modelo calculará los valores de los indicadores. Para ello, escribiremos el método Tick(...) igual que en el Controlador. A continuación, transferiremos el código del indicador WPR original a este método y añadimos los métodos auxiliares. Es decir, en nuestro caso, el Modelo es casi idéntico al código del manejador OnCalculate del indicador original.
Sin embargo, tenemos un problema: el búfer de indicador. Debemos escribir los datos directamente allí, pero no es correcto poner un búfer de indicador en el Modelo, deberíamos colocarlo en la Vista. Por consiguiente, actuaremos de la forma conocida. Vamos a crear el archivo IOutputBase.mqh en la carpeta donde se encuentra el Modelo. En este archivo escribiremos la interfaz:
interface IOutputBase { void SetValue(int shift, double value); const double GetValue(int shift) const; };
El primer método guarda los valores según el índice especificado; el segundo método, los retorna. Como consecuencia, la Vista implementará esta interfaz, pero, mientras tanto, cambiaremos el método de inicialización del Modelo para obtener un puntero a la nueva interfaz y añadir un campo cerrado:
int Initialize(IInputBase* pI, IOutputBase* pO){ pInput = pI; pOutput = pO; ... } ... private: IInputBase* pInput; IOutputBase* pOutput;
Y en los cálculos, sustituiremos la referencia al búfer de indicador por una llamada al método:
pOutput.SetValue(...);
Como resultado, el método Tick(...) del Modelo adquirirá la siguiente forma (podemos compararlo con el manejador 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); }
Con esto concluye nuestro trabajo con el Modelo.
4. Vista
El último componente del indicador que vamos a crear es la Vista. Esta se encarga de mostrar los datos ofrecidos por el Modelo. Además, la Vista, junto con el módulo de datos de origen, es un componente que se modifica y actualiza con frecuencia. Añadir otro búfer, cambiar el estilo, el color por defecto, etcétera: todos estos cambios de uso frecuente se realizan en la Vista. Y una circunstancia más a la que merece la pena prestar atención: los cambios en la Vista suelen ser consecuencia de los cambios en el módulo de datos de origen y viceversa. Esta sería otra razón de peso para mantener la Vista y el módulo de datos de origen lejos del Modelo.
Comenzaremos a trabajar de la forma conocida. En la carpeta View, crearemos la clase CView, y después incluiremos el archivo IOutputBase.mqh. En la clase View, creamos el método Initialize(...) que tan bien conocemos. Preste atención: no hemos creado los métodos Update(...) o Release(...) en el Modelo o en la Vista. Hasta ahora, nuestro indicador no requiere su presencia.
A continuación, añadimos el búfer de indicador como un simple campo cerrado, implementamos el contrato IOutputBase, y ocultamos todas las llamadas de IndicatorSetXXX, PlotIndexSetXXX etc., eliminando así la mayoría de las macros del archivo principal del 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; }
Eso es todo; el indicador ha sido creado y funciona, la captura de pantalla muestra ambos, tanto el WPR original como el que acabamos de crear, que se encuentra en el archivo adjunto:
Resulta bastante obvio que sus lecturas son las mismas. No obstante, vamos a intentar complicar el indicador introduciendo una funcionalidad adicional según nuestras propias reglas desarrolladas.
5. Trabajando con el nuevo indicador
Vamos a suponer, por ejemplo, que sea necesario cambiar dinámicamente la forma de dibujar el indicador de una línea a un histograma. Así, añadimos esta funcionalidad en primer lugar para comprobar si ahora resulta más cómodo o, por el contrario, más difícil de hacer.
En primer lugar, necesitaremos una forma de señalización. Supongamos que tenemos un objeto gráfico, y que, al clicar sobre él, la línea se transforma en un histograma o viceversa. Crearemos este objeto como un botón en la subventana del indicador:
Para crear, inicializar, almacenar y eliminar el objeto gráfico llamado "botón", crearemos la clase CButtonObj. El código de esta clase no es complicado y no lo ofreceremos aquí. El Controlador gestionará el objeto de esta clase, y por ende, el propio botón, lo cual resulta comprensible: el botón es un elemento de interacción con el usuario, y esta es gestionada por el Controlador.
Ahora tenemos que añadir el manejador OnChartEvent al archivo del programa principal y el método correspondiente al Controlador:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { pController.ChartEvent(id, lparam, dparam, sparam); }
El mayor número de cambios se producirá en la Vista. Aquí, todo también se reduce a añadir una enumeración para la señalización, así como unos cuantos 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(); }
El método Controller resultante, que se llama en el manejador OnChartEvent del archivo principal del indicador, tiene el siguiente aspecto:
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) }
El método comprueba si se ha clicado en el objeto gráfico deseado y cambia el método de representación en la Vista:
Todas las adiciones se han hecho muy rápidamente. Si tuviéramos que realizar las mismas acciones un año después, no se tardaría mucho más. El desarrollador recuerda la estructura del script y de qué es responsable cada componente, por lo que también podemos navegar si perdemos la documentación u olvidamos los principios de funcionamiento.
7. Comentarios sobre el código
Ahora que el trabajo está terminado, hagamos un análisis.
- Nuestro modelo no tiene casi ninguna dependencia. El Controlador, por el contrario, depende de casi todos los demás módulos, a juzgar por el conjunto #include al principio del archivo. ¿Es así? Técnicamente, la respuesta es sí. Junto con la inclusión del archivo, el desarrollador establece la dependencia. Lo específico de nuestro Controlador es que crea módulos, controla su ciclo de vida y les transmite los eventos. El Controlador sirve de "motor", aportando dinámica, lo cual encaja de forma bastante lógica con su tarea original de interactuar con el usuario.
- Todos los Componentes contienen métodos muy similares — Initialize, Update, Release. Resulta lógico plantearse la creación de alguna clase básica con un conjunto de métodos virtuales. Sí, la signatura del propio método Initialize es distinta para diferentes componentes, pero podemos intentar superar esto.
- Quizás una opción más atractiva (aunque más compleja) sería una variante de CInputManager que retornara los punteros a las interfaces de esta manera:
class CInputManager { ... public: InputBase* GetInput(); ... };
De implementarse, este esquema permitiría que los componentes individuales tuvieran acceso solo a un conjunto limitado de parámetros de entrada. No vamos a implementar esto aquí, pero debemos tener en cuenta que la gran atención que hemos prestadado al módulo de parámetros de entrada a lo largo de este artículo se debe al deseo de mostrar posibles enfoques para construir también otros módulos necesarios en el trabajo. Por ejemplo, el componente CView no tiene que implementar necesariamente la interfaz IOutputBase, como se hace en el artículo, mediante relaciones jerárquicas, sino eligiendo alguna forma de composición, como acabamos de sugerir.
8. Conclusión
Con esto, podemos dar por terminado el tema que nos ocupa. En el primer artículo, abarcamos principalmente la plantilla MVC, pero en este hemos profundizado en el tema, explorando para ello las posibles interacciones entre los componentes individuales de la plantilla. Obviamente, el tema no es fácil. Pero el efecto, si se aplica correctamente, puede resultar bastante útil.
Programas utilizados en el artículo:
# | Nombre | Tipo | Descripción |
---|---|---|---|
1 | WPR_MVC.zip | Fichero | Indicador WPR revisado. |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/10249
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
Artículo publicado Patrón de diseño MVC y posibilidad de utilizarlo (Parte 2): esquema de interacción entre tres componentes:
Autor: Andrei Novichkov