Esta entrada se publicó originalmente en Harbour Magazine, mi publicación sobre el lenguaje de programación 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