Modelo Vista Controlador en Harbour
Este es un artículo de Manu Expósito para Harbour Magazine. Manu es un conocido experto en bases de datos y ha desarrollado Harbour Data Objects, una jerarquía de clases para acceder a BB.DD. relacionales con Harbour. Puedes contactar con él en hdoexpoARROBAgmailPUNTOcom
Con este artículo pretendo explicar de una manera muy práctica como programar usando el patrón de diseño MVC en xBase.
Además me gustaría que fuera el punto de inicio para que mediante las reseñas que hagáis, mejorar el propio articulo y el ejemplo que ilustra la explicación.
¡¡¡Sin más dilación empecemos!!!
¿Sabes qué es un patrón de diseño en programación?
Durante muchos años todos los programadores hemos afrontado unos problema que eran comunes y que cada uno resolvía de una manera diferente. Si se comparaban, realmente había muchas similitudes. Alguien se dedicó a estudiar esos retos y sin codificar en un lenguaje concreto hizo unas directrices por escrito donde se indicaba como solucionar el problema. Por lo tanto un patrón de diseño es una técnica definida para afrontar un problema informático. A partir de aquí dejo a cada uno que indague más.
¿En qué consiste el patrón de diseño Modelo Vista Controlador (MVC)?
Es una manera de resolver un problema en la programación que consiste en dividir el problema en tres capas: el modelo, la vista y el controlador. Cada una de ellas está especializada en la solución de una parte del problema. Además se puede usar con otros patrones de diseño.
Sobre todo se seuele utilizar en el desarrollo para internet pero que podemos aprovechar para otros tipos de programación.
¿Qué es el modelo?
Realmente en Wikipedia están muy bien explicados los componentes de este patrón de diseño, pero voy a intentar mostrar qué es de una manera muy práctica.
El modelo representa la información que va a tratar nuestro programa.
Tiene que ser autosuficiente, quiero decir que no pude depender de nada externo. Para obtener la información que lo conforma tiene que acceder a mecanismos internos que podrían ser el uso de otros patrones, por ejemplo usar un servicio DAO para acceder a la información guardada en una base de datos. Pero también podría ser obtenida desde un XML o un JSon o una cadena etc…
El modelo no tiene porqué conocer la existencia de los otros componentes, ni del controlador ni de la vista.
De hecho debería estar completamente desacoplado del resto.
Como regla general se suelen crear clases que representan el modelo.
Podríamos crear una clase abstracta y de ella heredaremos las características generales para crear una clase especializada de la cual instaciaremos los objetos modelo que se necesiten.
El ejemplo va a consistir en un conversor de monedas.
Esta sería la clase abstracta que propongo para la conversión de cualquier moneda:
//----------------------------------------------------------------//
#include "HbClass.ch"
//------------------------------------------------------------------
// Clase para convertir cualquier moneda a otra
CREATE CLASS TConversorModel
PROTECTED:
DATA cambio
DATA resultado
EXPORTED:
METHOD new( valorCambio ) CONSTRUCTOR
METHOD getResultado()
// SET GET
METHOD setCambio( cambio )
METHOD getCambio()
PROTECTED:
METHOD convMul( cantidad )
METHOD convDiv( cantidad )
END CLASS
//------------------------------------------------------------------
// Constructor
METHOD new( valorCambio ) CLASS TConversorModel
if ValType( valorCambio ) == "N"
::cambio := valorCambio
end if
return Self
//------------------------------------------------------------------
// Pasa euros a la moneda
PROCEDURE convMul( cantidad ) CLASS TConversorModel
::resultado := if( ValType( cantidad ) == "N", cantidad * ::cambio, 0 )
return
//------------------------------------------------------------------
// Pasa las monedas a euros
PROCEDURE convDiv( cantidad ) CLASS TConversorModel
::resultado := if( ValType( cantidad ) == "N", cantidad / ::cambio, 0 )
return
//------------------------------------------------------------------
// Asigna el cambio
METHOD setCambio( cambio ) CLASS TConversorModel
local ret := ::cambio
if ValType( cambio ) == "N"
::cambio := cambio
end if
return ret
//------------------------------------------------------------------
// Obtiene el cambio
METHOD getCambio() CLASS TConversorModel
return ::cambio
//------------------------------------------------------------------
METHOD getResultado() CLASS TConversorModel
return ::resultado
//------------------------------------------------------------------
Y esta otra sería la clase especializada para la conversión de euros y pesetas y que hereda de la anterior:
//----------------------------------------------------------------//
#include "HbClass.ch"
//------------------------------------------------------------------
// Clase para convertir pesetas a Euros
CREATE CLASS TConversorModelEurosPesetas FROM TConversorModel
METHOD new() CONSTRUCTOR
METHOD deEurosAPesetas()
METHOD dePesetasAEuros()
END CLASS
//------------------------------------------------------------------
// Constructor
METHOD new() CLASS TConversorModelEurosPesetas
::setCambio( 166.386 )
return Self
//------------------------------------------------------------------
// Convierte un importe a ptas
METHOD deEurosAPesetas( cantidad ) CLASS TConversorModelEurosPesetas
::convMul( cantidad )
return ::getResultado()
//------------------------------------------------------------------
// Convierte un importe a euros
METHOD dePesetasAEuros( cantidad ) CLASS TConversorModelEurosPesetas
::convDiv( cantidad )
return ::getResultado()
//------------------------------------------------------------------
¿Qué es la vista?
Hemos dicho que el modelo representa la información. Eso en sí no vale de nada si no se puede tratar o ver. Para eso está la vista.
La vista puede ser la manera de visualizar uno más modelos por pantalla. Aunque también podría ser un informe que se imprime o un PDF o cualquier mecanismo en el que se representaciónde datos.
Aquí podríamos usar clases o no.
Por ejemplo, en la programación para internet, suelen ser las páginas web y no suelen ser objetos de una clase sino HTML.
Pero también podrían ser clases que contengan por ejemplo una cabecera y un pie.
Aquí van a ser también clases. Una abstracta y otra especializada:
La clase abstracta:
//----------------------------------------------------------------//
#include "HbClass.ch"
//------------------------------------------------------------------
// Clase para Abstracta para la creacion de vistas.
// En las clases derivadas hay que implementar al menos estos metodos VIRTUALES
//------------------------------------------------------------------
CREATE CLASS TConversorVista
PROTECTED:
DATA tipoConversion // "Pesetas a Euros" -> 1 "Euros a Pesetas" -> 2
EXPORTED:
METHOD new() CONSTRUCTOR
// Se implementan en cada vista
METHOD msg( cTxt, cTitulo ) VIRTUAL
METHOD muestraMenu() VIRTUAL
METHOD getCantidad() VIRTUAL
METHOD escribeCambio( s ) VIRTUAL
METHOD acercaDe() VIRTUAL
METHOD muestraFin() VIRTUAL
// SET GET
METHOD setTipoConversion( cTipo )
METHOD getTipoConversion()
END CLASS
//------------------------------------------------------------------
// Constructor
METHOD new() CLASS TConversorVista
return self
//------------------------------------------------------------------
// Asigna el tipo Conversion
METHOD setTipoConversion( cTipo ) CLASS TConversorVista
local ret := ::tipoConversion
if ValType( cTipo ) == 'C' .and. ( cTipo == '1' .or. cTipo == '2' )
::tipoConversion := cTipo
end if
return ret
//----------------------------_-------------------------------------
// Obtiene el tipo Conversion
METHOD getTipoConversion() CLASS TConversorVista
return ::tipoConversion
//------------------------------------------------------------------
Y esta es la clase desde la que vamos a instanciar nuestro objeto vista en formato texto. Como reto, propongo que hagais una clase para un GUI, por ejemplo FWH.
//----------------------------------------------------------------//
#include "HbClass.ch"
//------------------------------------------------------------------
// Definicion de la clase VISTA tipo texto
CREATE CLASS TVistaTXT FROM TConversorVista
METHOD msg( cTxt, cTitulo )
METHOD leeOpcion()
METHOD muestraMenu()
METHOD operacionIncorrecta()
// Implementacion de los metodos de la interfaz vista
METHOD escribeCambio()
METHOD getCantidad()
METHOD acercaDe()
METHOD muestraFin()
END CLASS
//------------------------------------------------------------------
// Saca un mensaje en pantalla
METHOD msg( cMsg, cTitulo ) CLASS TVistaTXT
if ValType( cTitulo ) != 'C'
cTitulo := "Atencion"
endif
cTitulo := ";" + cTitulo + ";;;"
if ValType( cMsg ) != 'C'
cMsg := " "
endif
return Alert( cTitulo + cMsg )
//------------------------------------------------------------------
// Se encarga de sacar por pantalla la informacion
PROCEDURE escribeCambio( s ) CLASS TVistaTXT
::msg( s, "Resultado" )
return
//------------------------------------------------------------------
// Acepta el tipo de opcion por pantalla
METHOD leeOpcion() CLASS TVistaTXT
local cOpcion := " "
local getList := {}
@ 10, 10 SAY "Elige opcion:" GET cOpcion
READ
return AllTrim( cOpcion )
//------------------------------------------------------------------
// Acepta el importe que se va a tratar
METHOD getCantidad() CLASS TVistaTXT
local nCantidad := 0
local getList := {}
@ 15, 10 SAY "Importe:" GET nCantidad
READ
// Limpia pantalla
@ 15, 10 SAY Space( 30 )
return nCantidad
//------------------------------------------------------------------
// Muestra el menu por pantalla
PROCEDURE muestraMenu() CLASS TVistaTXT
cls
? " +----------------------------------------+"
? " | Indica operacion que quieres realizar: |"
? " +----------------------------------------+"
?
? " [1] De pesetas a euros"
? " [2] De euros a pesetas"
? " [3] Acerca de..."
? " [0] Salir"
return
//------------------------------------------------------------------
// Mensaje de error
PROCEDURE operacionIncorrecta() CLASS TVistaTXT
::msg( "Opcion incorrecta..." )
return
//------------------------------------------------------------------
// Informacion del sistema
PROCEDURE acercaDe() CLASS TVistaTXT
::msg( "Ejemplo en modo Texto;del;Patron de Diseño;MVC", "Acerca del ejemplo" )
return
//------------------------------------------------------------------
// Aqui ira todo lo que se debe hacer al finalizar la ejecucion a nivel de vista
PROCEDURE muestraFin() CLASS TVistaTXT
cls
::msg( "Fin de la ejecucion" )
return
//------------------------------------------------------------------
Como podeis ver en el código de las cuatro clases vistas hasta ahora ninguna conoce la existencia de las demás. El modelo no sabe nada de la vista ni del controlador y la vista no sabe nada del modelo ni del controlador.
¿Qué es el controlador?
Como su nombre indica es el que controla :-)
El controlador se encarga atender a los eventos y de despachar las peticiones del usuario (en algunos sistemas se llama acciones que son un tipo de modelo especial). Para ello tiene que conocer la existencia de las vistas y de los modelos que se tienen que propagar por nuestro programa.
Realmente es un intermediario entre el usuario, el modelo y la vista.
Puede haber diferentes tipos de controladores. Uno de los más difundidos son:
El controlador frontal que es único en el sentido que admite todas las peticiones y las despacha todas.
Y puede haber controladores especializados que sólo admiten peticiones y eventos para los que está concevido o de un controlador frontal.
Para seguir con el mismo criterio vamos poner primero la clase abstracta:
//------------------------------------------------------------------
#include "HbClass.ch"
//------------------------------------------------------------------
// Control 1/1
CREATE CLASS TConversorController
PROTECTED:
DATA vista
DATA modelo
EXPORTED:
METHOD new( vista, modelo ) CONSTRUCTOR
METHOD gestionDeTipoConversion( cTipo ) VIRTUAL
METHOD despachaAcciones()
METHOD fin()
// SET GET
METHOD getVista()
METHOD setVista( vista )
METHOD getModelo()
METHOD setModelo( modelo )
END CLASS
//------------------------------------------------------------------
// Constructor
METHOD new( vista, modelo ) CLASS TConversorController
::vista := vista
::modelo := modelo
return self
//------------------------------------------------------------------
// Gestiona las peticiones
PROCEDURE despachaAcciones() CLASS TConversorController
local cTipo
while .t.
switch cTipo := ::vista:leeOpcion()
case '0'
::vista:muestraFin()
::fin()
exit
case '1'
case '2'
::vista:setTipoConversion( cTipo )
::gestionDeTipoConversion()
exit
case '3'
::vista:acercaDe()
exit
otherwise
::vista:operacionIncorrecta()
end switch
end
return
//------------------------------------------------------------------
// Se ejecuta al final
PROCEDURE fin() CLASS TConversorController
// Se haria todo lo del final
break
return
//------------------------------------------------------------------
// Obtiene la vista
METHOD getVista() CLASS TConversorController
return ::vista
//------------------------------------------------------------------
// Asigna la vista
PROCEDURE setVista( vista ) CLASS TConversorController
::vista := vista
return
//------------------------------------------------------------------
// Obtiene el modelo
METHOD getModelo() CLASS TConversorController
return ::modelo
//------------------------------------------------------------------
// Asigna el modelo
PROCEDURE setModelo( modelo ) CLASS TConversorController
::modelo := modelo
return
//------------------------------------------------------------------
Y ahora nuestro controlador especializado para la conversión entre pesetas y euros:
//----------------------------------------------------------------//
/*
El CONTROLADOR
Desde aqui se reciben las peticiones del usuario y se trasladan al MODELO
*/
#include "HbClass.ch"
//------------------------------------------------------------------
// Controlador
CREATE CLASS TConversorEurosPesetasController FROM TConversorController
METHOD gestionDeTipoConversion( cTipo )
END CLASS
//------------------------------------------------------------------
// Control de conversiones
PROCEDURE gestionDeTipoConversion() CLASS TConversorEurosPesetasController
local cantidad := ::vista:getCantidad()
switch ::vista:getTipoConversion()
case '1'
::vista:escribeCambio( hb_ntos( cantidad ) + " pesetas son: " + ;
hb_ntos( ::modelo:dePesetasAEuros( cantidad ) ) + " euros" )
exit
case '2'
::vista:escribeCambio( hb_ntos( cantidad ) + " euros son: " + ;
hb_ntos( ::modelo:deEurosAPesetas( cantidad ) ) + " pesetas" )
exit
otherwise
::vista:msg( "---< Se ha producido un ERROR >---" )
end switch
return
//------------------------------------------------------------------
Si observais el código vereis que ni en el modelo ni en el cotrolador hay salidas a pantalla de eso se encarga la clase especializada TVistaTXT, me encantaría que halguien hiciera una clase TVistaFWH o cualquier otra clase para otro IDE. Ahí queda el reto ;-)
Ahora necesitamos un punto de entrada que será nuestro programa.
Si lo diseñais bien tampoco tiene que estar acoplado al GUI que se use:
//------------------------------------------------------------------
#include "hbclass.ch"
//------------------------------------------------------------------
// Programa principal de la prueba
PROCEDURE main()
local oAp := TAplicacion():new()
oAp:ejecuta()
return
//------------------------------------------------------------------
// Clase principal para el ejemplo de pruebas etc
CLASS TAplicacion
DATA controlador
METHOD new() CONSTRUCTOR
METHOD ejecuta()
END CLASS
//------------------------------------------------------------------
// Constructor
METHOD new() CLASS TAplicacion
local oVista := TVistaTXT():new()
local oModelo := TConversorModelEurosPesetas():new()
::controlador := TConversorEurosPesetasController():new( oVista, oModelo )
return self
//------------------------------------------------------------------
// Metodo que pone en marcha el sistema
PROCEDURE ejecuta() CLASS TAplicacion
::controlador:getVista():muestraMenu()
::controlador:despachaAcciones()
return
//------------------------------------------------------------------
Espero que con este pequeño articulo se cree un debate que promocione a esta gran herramienta que es Harbour Magazine de nuestro gran amigo José Luis, a Harbour y al uso de patrones de diseño en nuestros programas. Si es así podríamos hacer otros artículos con otros patrones de diseño como por ejemplo DAO, Facade, Adapter, Singlenton o Decorator… por poner algunos interesantes.
Espero vuestras reseñas…
Saludos.
Manu Expósito
PD: Todo el código del artículo está disponible en: https://github.com/JoseluisSanchez/MVC_Harbour