Ponga una clase en su vida
Fecha: 24/Ago/98 (18/Ago/98)
Autor: Luis Sanz, Hospital "Reina Sof�a" [email protected]
Visual Basic incorpor�, en su versi�n 4, un nuevo tipo de archivo: las clases. Pero �Qu� son? �Para qu� sirven?
�Qu� son las clases?
Realmente, cualquier objeto de Visual Basic es realmente una clase. Una clase es una "cosa" que tiene sus propios m�todos, eventos y propiedades. Algunas de ellas tienen una interfaz gr�fica (como, por ejemplo, una caja de texto, o un formulario). Por lo tanto, Visual Basic ha tenido clases desde siempre. Pero desde la versi�n 4, se pueden usar clases definidas por el usuario: los m�dulos de clase.
Las clases de VB tienen m�todos, eventos y propiedades. Las clases "hechas" (como los formularios) los tienen por defecto (por ejemplo, el m�todo Show, la propiedad Visible, y el evento Click). A ellos les podemos a�adir los propios. Sin embargo, los m�dulos de clase, hasta que los "rellenemos", s�lo tienen dos eventos (Class_Initialize y Class_Terminate). Hay propiedades ocultas, como Name, o Instancing, pero en principio no podemos acceder a ellas salvo desde la ventana de propiedades.
�C�mo usarlos?
Las clases "predise�adas" de VB, como los formularios, tienen una instancia por defecto. Por ejemplo, si tenemos un proyecto con el formulario frmPrueba, podemos poner:
frmPrueba.Show
Lo que hacemos en realidad es referirnos a la instancia por defecto del objeto "frmPrueba". Si queremos, podemos declarar nosotros mismos la instancia (lo cual suele ser recomendable) pero no es imprescindible:
Dim Fprueba As New frmPrueba FPrueba.Show
Sin embargo, las clases no tienen una instancia "por defecto": para acceder a ellas, debemos declararlas. Si en el ejemplo anterior tenemos una clase llamada clsPrueba, con un m�todo p�blico "Pitar"
Public Sub Pitar() Dim i As Integer For i = 0 To 10 Beep Next End Sub
Si ponemos en el c�digo:
Private Sub Form_Click clsPrueba.Pitar End Sub
Obtendremos un error del tipo "variable no definida". Para evitarlo, hay que poner:
Private Sub Form_Click() Dim CPrueba As clsPrueba Set CPrueba = New clsPrueba CPrueba.Pitar End Sub
�:
Private Sub Form_Click() Dim CPrueba As New clsPrueba CPrueba.Pitar End Sub
La variable CPrueba que hemos declarado tendr�, como cualquier otra variable, un �mbito, y un "ciclo de vida". Podremos declararla como local (dentro de un procedimiento), de m�dulo (privada o p�blica), o global, igual que hacemos con cualquier otra variable.
Cuando creamos una instancia de la variable, se crea una nueva copia del objeto en memoria. En ese momento, las variables de dicho m�dulo se inicializan con sus valores por defecto (es decir: vac�as, o cero). Pero la instancia se crea en un momento u otro, seg�n como se haya declarado la variable. Si se declara:
Dim CPrueba As clsPruebaSet CPrueba = New clsPrueba Se crea la instancia
la instancia se crea cuando establecemos la referencia (con Set ). Debemos recordar establecer en el c�digo la nueva referencia. Si la declaramos
Dim CPrueba As New clsPruebaCPrueba.Pitar Se crea la instancia
la referencia se crea en tiempo de compilaci�n, y la instancia se crea la primera vez que llamamos a la clase (con un m�todo, una propiedad ). Si descargamos la clase (con Set CPrueba = Nothing) y volvemos a llamar a la clase, se crea una nueva instancia.
Entre uno u otro m�todo apenas hay diferencias de velocidad. El primer m�todo da m�s control sobre la creaci�n de instancias, pero debemos recordar que tenemos que crearlas. El segundo m�todo es m�s c�modo, ya que llamaremos a la clase cuando queramos, pero est� m�s expuesto a una programaci�n descuidada. Hay que tener en cuenta el c�digo que se ejecute en Class_Initialize, ya que nos puede interesar que se ejecute en un momento u otro. Habitualmente, y por comodidad, usar� el segundo sistema.
El m�dulo de clases se descarga:
|
Vamos a mostrar como se cargan y descargan las clases (el listado puede verse en el proyecto PContar).
Vamos a incluir una nueva clase (clsContar), que lo que hace es indicarnos cuantas veces se ejecuta su m�todo p�blico (Sumar), para lo cual usa una variable privada. Como es una clase muy educada, nos indica cuando se carga y cuando se descarga (cuando se producen los eventos Initialize y Terminate).
'Clase clsContar Option Explicit Private iMNumero As Integer Public Sub Sumar() MNumero = iMNumero + 1 MsgBox "Se han sumado " & CStr(iMNumero) End Sub Private Sub Class_Initialize() MsgBox "Hola" End Sub Private Sub Class_Terminate() MsgBox "Adi�s" End Sub
Ahora, llamaremos a la clase desde un formulario. Como la clase debe contar, no debe salir de �mbito al acabar el procedimiento: la declararemos en la secci�n "Declaraciones" del formulario:
Option ExplicitPrivate CContar As clsContarPrivate Sub cmdContar_Click() CContar.Sumar End SubPrivate Sub Form_Load() Set CContar = New clsContar End Sub
Si ejecutamos el programa, veremos c�mo se crea la instancia de la clase en el evento Load del formulario, y se destruye tras el evento Unload de �ste. Cada vez que se pulsa el bot�n "Sumar", se suma uno.
Ahora, modificaremos el evento cmdContar_Click
Private Sub cmdContar_Click() CContar.Sumar Set CContar = Nothing End Sub
La primera vez que se pulsa el bot�n, se ejecuta el m�todo sumar, y la instancia se despide y se destruye. La siguiente vez, se produce un error. Si hacemos de nuevo referencia a la clase:
Private Sub cmdContar_Click() CContar.Sumar Set CContar = Nothing Set CContar = New clsContar End Sub
Veremos c�mo la clase se carga y se descarga, y el valor de las variables internas no se conserva.
Si ahora declaramos la variable de la otra forma (con As New):
Option ExplicitPrivate CContar As New clsContarPrivate Sub cmdContar_Click() CContar.Sumar End Sub
Veremos que el comportamiento es diferente. La instancia de la clase se crea la primera vez que se hace referencia a �sta. Si ahora eliminamos la referencia:
Private Sub cmdContar_Click() CContar.Sumar Set CContar = Nothing End Sub
Veremos c�mo no se produce un error, sino que se crea una nueva instancia de la clase cada vez que se hace referencia a �sta, pero, al ser una nueva "copia", no se conserva el valor de las variables internas. Por ello hay que ser m�s cuidadoso si se usa el segundo m�todo de declarar las clases.
�C�mo a�adir c�digo?
Una clase puede tener m�todos, propiedades y eventos, p�blicos o privados. Los m�todos, etc�tera, privados son internos a la clase, y ahora no nos interesan. Los p�blicos constituyen la interfaz p�blica de la clase, es decir, lo que la clase "expone" al resto del programa. Para implementarlos, podemos a�adirlos manualmente, o usar el asistente de VB5 "Class Builder Utility". La ventaja del asistente es que es sencillo de usar, y evita errores. Pero la nomenclatura de las variables no es mu all�, que digamos.
A�adir un m�todo a una clase
Basta con declarar un procedimiento (Sub o Function) como Public:
Public Sub Sumar() iMNumero = iMNumero + 1 MsgBox "Se han sumado " & CStr(iMNumero) End SubPublic Function Total() As Integer Total = iMNumero End Function
Para acceder a ellas, se hace como para acceder a un m�todo p�blico de un formulario:
CContar.SumarI = CContar.Total
A�adir una propiedad a una clase
La forma m�s sencilla es a�adir una variable p�blica:
Public NumeroInicio As Integer
Y se accede:
If 0 = CContar.NumeroInicio Then CContar.NumeroInicio = 3
Pero, aunque funciona, es una forma peligrosa: los valores que establezcamos no estar�n "filtrados". Adem�s, en ocasiones querremos que se ejecutan acciones cuando se modifique un valor (igual que en un formulario cambia el color de fondo al modificar la propiedad BackColor). Para ello, usaremos los procedimientos Property Let y Property Get:
'En la clase clsContar Private iMNumeroInicio As IntegerPublic Property Let NumeroInicio(ByVal NuevoNumero As Integer) iMNumeroInicio = NuevoNumero iMNumero = iMNumeroInicio End PropertyPublic Property Get NumeroInicio() As Integer NumeroInicio = iMNumeroInicio End Property'En frmPrueba Private Sub txtContar_Change() CContar.NumeroInicio = Val(txtContar) End Sub
S queremos establecer la referencia a un objeto, debemos hacerlo con Property Set:
Private FMVentana As FormPublic Property Set Ventana(ByVal NuevaVentana As Form) Set FMVentana = NuevaVentana End PropertyPublic Property Get Ventana() As Form Set Ventana = FMVentana End Property
A�adir eventos a una clase
Esta es una de las nuevas caracter�sticas de las clases que tiene VB5.
En ocasiones, querremos que la clase nos "avise" de que ha pasado algo, de la misma forma que una caja de texto nos avisa que ha cambiado su contenido. Es decir, que ha pasado algo: que ha ocurrido un evento. Pero implementar un nuevo evento no es tan sencillo como a�adir una propiedad:
En primer luger, debemos a�adir el evento a la clase:
Public Event CambioNumero(ByVal Numero As Integer)
Para disparar los eventos, debe usarse RaiseEvent
Public Sub Sumar() iMNumero = iMNumero + 1 MsgBox "Se han sumado " & CStr(iMNumero) RaiseEvent CambioNumero(iMNumero) End Sub
Ahora es preciso "recibir" los eventos. Eso m�s complejo. En primer lugar, debemos declarar la clase con la palabra clave WithEvents. No puede hacerse ni en un procedimiento (variable local) ni en un m�dulo, sino en la secci�n "Declaraciones" de un formulario (o de una clase). No puede usarse la palabra clave New
Private WithEvents CContarEventos As clsContar
Ahora, si en el visor del c�digo miramos en la ventana de controles, veremos que hay una nueva entrada: CContarEventos. Es como si hubi�semos a�adido un nuevo control al formulario. Este nuevo "control" s�lo tendr� disponible un evento por ahora (CambioNumero), pero podemos crear los que necesitemos.
Para recibir el evento, todav�a hemos de crear la instancia:
Private Sub Form_Load() Set CContarEventos = New clsContar End Sub
Ahora podemos incluir c�digo en el nuevo evento.
Private Sub cmdEventos_Click() CContarEventos.Sumar End SubPrivate Sub CContarEventos_CambioNumero(ByVal Numero As Integer) MsgBox "El nuevo n�mero es " & CStr(Numero) End Sub
Como con cualquier otro procedimiento, podemos editar sus caracter�sticas. Para ello seleccionaremos en el men� de VB Herramientas|Atributos del procedimiento. Podemos seleccionar un procedimiento y cambiar sus atributos (pulsando "Avanzadas" podemos hacer que sea el procedimiento por defecto, que est� oculto, etc.
�Para qu� sirve una clase?
Con todo esto, ya sabemos crear una clase que funcione. Pero si una clase s�lo va a servir para adornar un programa, no va a tener mucha utilidad. En la programaci�n habitual �para qu� podemos usarlas?
Ante todo, las clases no son imprescindibles. Los programadores de BASIC de siempre, de Visual Basic (hasta la versi�n 4), etc�tera, se las han arreglado para hacer buenos programas sin complicarse con las clases. Pero facilitan mucho determinadas tareas.
Lo que viene luego no es una lista exclusiva. Las clases, al ser objetos hechos "a medida" pueden usarse para las cosas m�s estramb�ticas". S�lo voy a se�alar algunas de sus posibilidades m�s sencillas.
Procedimientos blindados (encapsular procedimientos)
Una de las principales ventajas de las clases es la encapsulaci�n. Cuando creamos una instancia de una clase, se crea un espacio de memoria reservado para esta, con sus propias variables, que no interfieren entre s�. Si creamos un m�dulo est�ndar, s�lo hay en memoria una "copia" de �ste, con un �nico "juego" de variables. Por ello es mucho m�s sencillo que unos procedimientos interfieran con los otros.
Para demostrarlo, vamos a tener un programa que nos va a indicar cuantas veces nos rascamos. Cada vez que lo hagamos, pulsaremos el bot�n correspondiente. Y tendremos dos ventanas que nos dir�n cuantas veces lo hemos hecho, pero una usar� un m�dulo est�ndar, y la otra una clase. Vamos a tener el mismo c�digo en el m�dulo y en la clase.
'frmSumar Option ExplicitPrivate CSuma() As New clsSumarPrivate Sub Form_Load() ReDim CSuma(cmdRascar.UBound) End SubPrivate Sub cmdRascar_Click(Index As Integer) lblClase(Index).Caption = CStr(CSuma(Index).Sumar(1)) lblModulo(Index).Caption = CStr(modSumar.Sumar(1)) End Sub'clsSumar Option ExplicitPrivate iMSuma As IntegerPublic Function Sumar(Sumando As Integer) As Integer iMSuma = iMSuma + Sumando Sumar = iMSuma End Function'modSuma Option ExplicitPrivate iMSuma As IntegerPublic Function Sumar(Sumando As Integer) As Integer iMSuma = iMSuma + Sumando Sumar = iMSuma End Function
En apariencia, el c�digo es el mismo. Y el funcionamiento, igual. Hasta ahora.
Pero como somos muy nerviosos, no s�lo nos rascamos la nariz, tambi�n lo hacemos con la oreja. Y vamos a aprovechar el proyecto anterior. Como �ramos previsores, ya hab�amos creado una matriz de controles. Ahora, simplemente a�adimos otro bot�n y dos ventanas. Dejamos el c�digo igual
�Qu� ha pasado? Ya no suman igual. Cuando sumamos con el m�dulo, la suma es incorrecta. La causa es que el m�dulo usa la misma variable para almacenar, mientras que cada clase usa su propia variable.
Esto podr�a haberse evitado con una programaci�n m�s cuidadosa del m�dulo est�ndar:
'en frmSumar Private Sub cmdRascar_Click(Index As Integer) lblClase(Index).Caption = CStr(CSuma(Index).Sumar(1)) lblModulo(Index).Caption = CStr(modSumar.Sumar(1, Index)) End Sub'en modSuma Option ExplicitPrivate iMSuma(1) As IntegerPublic Function Sumar(Sumando As Integer, Indice As Integer) As Integer iMSuma(Indice) = iMSuma(Indice) + Sumando Sumar = iMSuma(Indice) End Function
Ahora funciona bien. Pero a costa de tener que modificar el c�digo de frmSumar y de modSuma, que por lo visto est�n estrechamente relacionados.
Mientras tanto �que ha pasado con clsSumar? Pues nada. Como estaba bien dise�ada desde el principio, no ha requerido ninguna modificaci�n. Se usa una instancia para cada bot�n, y cada una de ellas tiene su propio juego de variables, que no interfieren entre s�. Los cambios de frmSumar no le han afectado, y ha funcionado bien las dos veces.
Resumiendo: en muchas ocasiones, el uso de clases permite escribir procedimientos m�s sencillos y menos dependientes del "medio externo".
C�digo perenne (reutilizar c�digo)
La ventaja del c�digo encapsulado es que es mucho m�s sencillo incluirlo en otros proyectos. En el ejemplo anterior, hemos visto como hab�a que modificar el m�dulo est�ndar para a�adir un bot�n. Pero �y si queremos a�adir otro m�s?
Ahora, supongamos que estamos acatarrados, y empezamos a estornudar
Si pulsamos "Estornudo", se produce un nuevo error. Hemos de modificar de muevo modSuma para que funcione. Tal como funcionan hasta ahora, frmSumar y modSuma, est�n interrelacionados, y un cambio en uno de ellos requiere una modificaci�n en el otro.
Desde luego, este problema se podr�a haber evitado. Se podr�a haber puesto un procedimiento especial, que modificase la dimensi�n de la matriz de variable "almac�n", que se llamase durante Form_Load, etc. Pero entonces nos estar�amos complicando la vida, y el formulario y el m�dulo seguir�an dependiendo uno del otro.
Sin embargo, clsSumar sigue funcionando igual de bien que antes. No hemos cambiado nada, y el c�digo es el mismo en los tres proyectos. Es m�s, podr�amos tener todas las copias que necesit�semos, funcionando a la vez, sin problemas. Incluso podemos cambiar su c�digo interno:
Option ExplicitPublic Function Sumar(Sumando As Integer) As Integer Static iLStSuma As Integer iLStSuma = iLStSuma + Sumando Sumar = iLStSuma End Function
y sigue funcionando bien. Es porque su interfaz p�blica no se ha modificado. Esto es importante: si una clase est� bien dise�ada, mientras no se modifique su interfaz p�blica, podemos hacer todas las modificaciones internas que queramos, y el resto del programa no se afectar�.
M�s a�n: podemos incluir la misma clase en varios proyectos. No una copia del archivo .CLS en cada uno, sino el mismo archivo en varios proyectos. Funcionar� bien. Y si un d�a detectamos un error, o hacemos una mejora, al modificar la clase se actualizar�n todos los proyectos que la usen �qui�n se atreve a hacer lo mismo con un m�dulo?
Y todav�a m�s. Ni siquiera tenemos que escribir la clase nosotros. Podr�a ponerme de acuerdo con un compa�ero:
- "Nacho, necesito una clase que me vaya sumando de uno en uno"
- "No te preocupes, Luis, que te la hago. Tendr� un m�todo p�blico Sumar, que recibir� como par�metro el sumando, y devolver� el total"
- "Justo lo que necesitaba. Date prisa, por favor"
Ahora que estamos de acuerdo, Nacho puede escribir lo que quiera, ya que su c�digo se "adaptar�" al m�o. Pero esto trae consigo una exigencia: si queremos usar clases y reutilizarlas (o compartirlas), el c�digo debe estar encapsulado (nada de variables globales, por favor) y se debe mantener la interfaz p�blica sin modificaciones. De hecho, VB suministra herramientas para poder reutilizar la interfaz p�blica de las clases (es eso del polimorfismo; paciencia, ya llegaremos).
En resumen: usar clases, aunque requiere ser "disciplinado" al programar, acaba facilitando la vida al permitirnos reutilizar el c�digo, sobre todo si trabajamos en grupo.
Clases y controles
Como hemos visto hasta ahora, una de las caracter�sticas de las clases es la posibilidad de usar "copias m�ltiples" de ellas. Por eso no interfieren entre s�, al usar su propio "juego" de variables. Por ejemplo, si tenemos una clase clsCaja, con una variable p�blica "Texto", podemos poner:
Private cCaja1 As New clsCajaPrivate cCaja2 As New clsCajaPrivate cCaja3(1) As New clsCaja
Y luego:
cCaja1.Texto = "Carlos"If cCaja2.Texto = "Pepe" Then ...For i = 0 To UBound(cCaja3) cCaja3(i).Texto = CStr(i) Next
Esto �no suena de nada? Si resulta que en el formulario tenemos cajas de texto Text1, Text2, Text3(0) y Text3(1), podr�amos haber escrito un c�digo muy similar. Se debe a que realmente, una caja de texto es una clase (como hemos visto antes). Text1, Text2, etc. son instancias de la clase TextBox. Y cada una de ellas tiene su propio "juego" de variables, para cada una de ellas. Y si una tiene la propiedad Text = "Carlos", y otra "Pepe", no nos devolver�n nunca valores "cambiados".
Pero �y si quisi�semos a�adir una propiedad a un control? Por ejemplo, queremos que la caja de texto que vamos a utilizar tenga una propiedad de s�lo lectura "Cambiado", que indique si el texto se ha modificado, y un m�todo "Actualizar", que "pone a cero" el valor de cambiado �C�mo podr�amos hacerlo?
La forma l�gica de hacerlo ser�a usar una de las nuevas posibilidades de VB5: hacer un control de usuario que herede las caracter�sticas de un TextBox, y a�adirle las propiedades y m�todos nuevos. Con ello conseguiremos una nueva caja "a medida". Pero es algo latosa de desarrollar, y consume bastante recursos. Y total, para una s�la propiedad... �Se puede hacer de otra forma?
Podr�a hacerse con un m�dulo est�ndar. Es f�cil hacerlo si en el formulario hay un solo control (ver el c�digo de Pcambio0):
'modCaja Option ExplicitPrivate CajaCambiado As BooleanPublic Sub Actualizar(Cambio As Boolean) CajaCambiado = Cambio End SubPublic Function Cambiado(hwnd As Long) As Boolean Cambiado = CajaCambiado End Function'frmCambio Option ExplicitPrivate Sub Text1_Change() Releer End SubPrivate Sub Releer() Dim i As IntegerFor i = 0 To UBound(cText) If cText(i).Cambiado Then lblCambio(i).Caption = "Cambiado" Else lblCambio(i).Caption = "No cambiado" End If Next End Sub
Pero, como es evidente, esto no funcionar� con varios controles, ya que interferir�n con el valor de CajaCambiado en el m�dulo, al haber una s�la copia del procedimiento. Necesitamos alg�n "marcador" que nos indique que caja es. Para eso usaremos la propiedad hwnd (es el manejador de la ventana, o sea, la forma que Windows tiene de identificar la "instancia" de la ventana, que ser� �nica para esta). Luego, en el m�dulo est�ndar, guardaremos el valor del manejador, y si se ha cambiado. Es necesario a�adir un procedimiento para "incluir" nuevos controles (ver el c�digo de Pcambio):
'frmCambio Option ExplicitPrivate Sub Text1_Change(Index As Integer) Actualizar Text1.hwnd, True Releer End SubPrivate Sub Text2_Change() Actualizar Text2.hwnd, True Releer End SubPrivate Sub Releer() Dim C As Control Dim i As Integer For Each C In Controls If TypeOf C Is TextBox Then If Cambiado(C.hwnd) Then lblCambio(3 - i).Caption = "Cambiado" Else lblCambio(3 - i).Caption = "No cambiado" End If i = i + 1 End If Next End Sub'modCaja Option ExplicitPrivate Cajahwnd() As Long Private CajaCambiado() As Boolean Public Sub Actualizar(hwnd As Long, Cambio As Boolean) Dim iLMax As Integer Dim bLHay As Boolean Dim i As Integer On Error Resume Next iLMax = UBound(Cajahwnd) If Err = 0 Then For i = 0 To iLMax If Cajahwnd(i) = hwnd Then CajaCambiado(i) = Cambio bLHay = True End If Next End If If Not bLHay Or Err Then A�adir hwnd, Cambio End SubPublic Function Cambiado(hwnd As Long) As Boolean Dim iLMax As Integer Dim bLHay As Boolean Dim i As Integer On Error Resume Next iLMax = UBound(Cajahwnd) If Err = 0 Then For i = 0 To iLMax If Cajahwnd(i) = hwnd Then Cambiado = CajaCambiado(i) bLHay = True End If Next End If If Not bLHay Or Err Then A�adir hwnd End FunctionPrivate Sub A�adir(hwnd As Long, Optional Cambio As Boolean) Dim iLMax As Integer On Error Resume Next iLMax = UBound(Cajahwnd) If Err Then iLMax = -1 ReDim Preserve Cajahwnd(iLMax + 1) ReDim Preserve CajaCambiado(iLMax + 1) Cajahwnd(iLMax + 1) = hwnd CajaCambiado(iLMax + 1) = Cambio End Sub
Con todo esto, funciona, independientemente del n�mero de controles que incorporemos. Pero a base de "chapuzas": no es ni el�stico, ni robusto. Vamos a ver problemas:
|
En resumen: funciona, pero a base de un c�digo complejo, dif�cil de adaptar, y con un mayor consumo de recursos. Tal vez sea m�s sencillo hacerlo con clases. Veremos como queda el c�digo (Pcambio2):
'clsCaja Option ExplicitPublic Caja As Object Private bMCambiado As Boolean 'local copy Public Sub Actualizar(Cambio As Boolean) bMCambiado = Cambio End Sub Public Property Get Cambiado() As Boolean Cambiado = bMCambiado End Property'frmCambio Option ExplicitPrivate cText(3) As New clsCaja Private Sub Form_Load() Set cText(0).Caja = Text1(0) Set cText(1).Caja = Text1(1) Set cText(2).Caja = Text1(2) Set cText(3).Caja = Text2 End SubPrivate Sub Text1_Change(Index As Integer) cText(Index).Change Releer End SubPrivate Sub Text2_Change() cText(3).Change Releer End SubPrivate Sub Releer() Dim i As Integer For i = 0 To UBound(cText) If cText(i).Cambiado Then lblCambio(i).Caption = "Cambiado" Else lblCambio(i).Caption = "No cambiado" End If Next End Sub
El c�digo es mucho m�s sencillo usando clases. Tanto dentro de la misma clase (como el acceso es "�nico", nos olvidamos de matrices, etc.) como en el formulario (es m�s sencillo recorrer la matriz de clases). Pero hay otras ventajas:
|
Public Property Let Cambiado(ByVal Cambio As Boolean) Err.Raise 12001, , "Esta propiedad es s�lo lectura" End Property
En el ejemplo, de todas formas, hemos podido substituir la clase por un m�dulo, con un poco de trabajo. Pero no siempre es tan sencillo: podr�amos tener procedimientos que se llamasen recursivamente, o que desde unos controles se act�e en otros. En esos casos, es m�s f�cil controlar el valor de las variables si hay una copia para cada control, que si tenemos que identificar cada control para evitar que uno interfiera en la �nica copia de la variable..
En resumen: es mucho m�s sencillo "asociar" un procedimiento a un control si est� en un m�dulo de clases que si est� en un m�dulo est�ndar, ya que podremos tener una instancia para cada control, y no habr� riesgo de interferencias entre las variables.
Variables con filtro (clases en lugar de variables)
Algunas variables s�lo deber�an admitir valores en unos rangos determinados. Por ejemplo: supongamos que en un programa necesitamos guardar la edad de una amiga. Para ello, tenemos una variable "EdadPersonaA�os". Para esta variable ser�a aceptable un valor entre 0 y 120, pero no lo ser�an valores negativos ni enormes. Inadvertidamente podr�amos incluir un valor absurdo �C�mo evitarlo? Desde luego, es f�cil hacer comprobaciones en el c�digo, pero �no ser�a mejor que fuese la variable la que se encargase de ello? Para eso podemos usar una clase, la clase clsA�os:
En el formulario pondremos:
Option ExplicitPrivate CA�os As New clsA�osPrivate Sub Form_Load() txtA�os.Text = CA�os.EdadPersonaA�os End SubPrivate Sub txtA�os_Change() CA�os.EdadPersonaA�os = Val(txtA�os) End Sub
Y en la clase clsA�os
Option ExplicitPrivate iMEdadPersonaA�os As Integer Private Const ctMEdadMinima As Integer = 0 Private Const ctMEdadMaxima As Integer = 120 Private Const ctMEdadDefecto As Integer = 27 Public Property Let EdadPersonaA�os(ByVal Edad As Integer) If Edad < ctMEdadMinima Or Edad > ctMEdadMaxima Then Err.Raise 11111, "clsA�os", _ "�Seguro que la edad es correcta?" Else iMEdadPersonaA�os = Edad End If End PropertyPublic Property Get EdadPersonaA�os() As Integer EdadPersonaA�os = iMEdadPersonaA�os End PropertyPrivate Sub Class_Initialize() iMEdadPersonaA�os = ctMEdadDefecto End Sub
Ahora tenemos, en lugar de una variable "tonta" tenemos una clase que filtra los datos introducidos, y que incluso se inicializa con un valor por defecto. Ello tiene una segunda ventaja, menos evidente, y es que facilita el mantenimiento del c�digo. Si de repente queremos ser estricto, y no queremos saber nada de ni�as ni de viejas, es muy sencillo cambiar los l�mites sin tocar el c�digo. Basta por modificar los l�mites:
Private Const ctMEdadMinima As Integer = 18Private Const ctMEdadMaxima As Integer = 40
En plan chulo, podr�amos haber puesto los l�mites y el valor por defecto, no como constantes, sino como propiedades. En ese caso podr�amos reutilizar la clase: para nuestras amigas, para un colegio, para un programa de gesti�n de empleados (ver el c�digo de Pa�os).
Por desgracia, no es oro todo lo que reluce. Las clases son lentas y consumen recursos. En el ejemplo anterior, usando la funci�n del API GetTickCount (en las p�ginas del Guille encontrareis un ejemplo) para comparar la velocidad de modificar el valor de una variable Long, o de la clase clsA�os, se puede ver que esta �ltima es unas 100 veces m�s lenta (no os asusteis; aun as� es rapid�sima), y el consumo de recursos mucho mayor. Y tampoco aconsejar�a a nadie empezar a sustituir todas sus variables por clases. Ni lo recomendar�a para operaciones donde la velocidad es cr�tica (como las operaciones gr�ficas). Ahora bien, cuando la seguridad sea cr�tica (por ejemplo, en los valores que se guardar�n en una base de datos), es una opci�n.
De todas formas, no parece muy eficiente usar un mont�n de clases en cada proyeco �se podr�an agrupar?
Usar Clases en lugar de Tipos Definidos por el Usuario
Los tipos definidos por el usuario (TDU) son un tipo especial de variables "agrupadas" (no es realmente as�; es como si fuese una variable "dividida en trozos", pero para el caso nos vale). En ocasiones pueden resultar muy �tiles. Por ejemplo, en el caso anterior: supongamos que estamos haciendo un programa que lleva la gesti�n de nuestros amigos, y tenemos varios datos para cada uno de ellos: el nombre y los apellidos, la edad y el sexo (lo realmente importante). Podr�amos hacerlo as�:
Public Nombre() As StringPublic Apellido1() As StringPublic Apellido2() As StringPublic Edad() As IntegerPublic SexoVaron() As Boolean
Pero el problema con este sistema es que va a ser preciso redimensionar cada matriz (con el consiguiente riesgo de errores). Por ello, es mucho m�s sencillo hacerlo as�:
'M�dulo est�ndar modAmigos Option ExplicitPublic Type TipoAmigo Nombre As String Apellido1 As String Apellido2 As String Edad As Integer SexoVaron As Boolean End TypePublic tGAmigo() As TipoAmigo
Ahora hemos creado una matriz de un TDU propio. Ahora, si tenemos veinte amigos, ponemos:
Redim tGAmigo (20)
Y si queremos a�adir un amigo m�s:
Redim Preserve tGAmigo (UBound(tGAmigo) + 1)
Luego, para almacenar los datos, podemos usar un archivo de acceso aleatorio (ver el curso b�sico del Guille) o una base de datos.
Pero los TDUs tienen sus limitaciones. Una es evidente: ni filtran los datos ni operan con ellos. Por eso, en el ejemplo anterior, podr�amos tener un problema con la edad: habr� que modificarla manualmente cada a�o. Tal vez sea mejor usar alg�n otro sistema, como veremos luego.
Pero otra limitaci�n es menos evidente: no podemos usarlos como miembros p�blicos de un formulario, ni pasarlos como argumento de un m�todo o funci�n p�blico, ni tener una funci�n que nos devuelva un TDU. Y eso puede ser fuente de problemas:
En el ejemplo anterior, queremos que el programa que estamos haciendo tenga una pantalla para editar los datos del amigo. Y queremos usar la misma pantalla tanto para introducir un amigo nuevo, como para modificar los datos de una amigo anterior �C�mo hacerlo? Podr�amos tener un m�todo p�blico en frmAmigo que tuviese como par�metro TipoAmigo:
Public Sub EditarAmigo(ByRef Amigo As TipoAmigo)
Pero nos da un error: no se pueden usar tipos definidos por el usuario como miembros p�blicos de un m�dulo de objeto. �C�mo lo pasaremos, pues? Una soluci�n ser�a tener una copia provisional "externa" de TipoAmigo, y un formulario escribe en una y el otro lo lee. Necesitaremos tambi�n alguna variable que indique si se han hecho cambios, etc. Para ello, declaramos en un m�dulo est�ndar:
Public tGAmigoProv As TipoAmigo
El proyecto PAmig0.vbp est� desarrollado con TDUs. Aparentemente funciona, pero tiene varias deficiencias:
|
Otra posibilidad ser�a usar, en lugar de variables p�blicas, un m�todo p�blico en el formulario, cada uno de cuyos par�metros fuese uno de los "componentes" de TipoAmigo:
Public Function EditarAmigo(Nombre As String, Apellido1 As String, ...
Tambi�n funcionar�a. Pero implicar�a una relaci�n demasiado estrecha (incestuosa, vamos) entre TipoAmigo, frmGestion y frmAmigo. Por ejemplo, si modificamos el componente "Edad" a Long, hay que modificar los m�todos p�blicos, etc. Si se a�ade otro "Componente" (Domicilio), lo mismo.
Todos estos problemas se pueden evitar mediante una programaci�n cuidadosa. Pero requiere tiempo y, si encima se est� desarrollando en un grupo, obliga a que los participantes est�n continuamente en contacto, ya que las modificaciones del c�digo de uno afectan a los dem�s (por ejemplo: si se usan variables p�blicas, hay que ponerse de acuerdo con el nombre de las variables, para que no interfieran).
Para evitar todos estos problemas, es conveniente usar otro estilo de programaci�n, lo m�s modular posible, y que evite el uso de variables p�blicas. Lo ideal ser�a que TipoAmigo tuviese sus propias reglas de validaci�n, sus valores por defecto, procesase la edad... Y que pudiese pasarse como un par�metro privado, ya que entonces las modificaciones del tipo no afectar�an al m�todo.
Eso, los TDUs no pueden hacerlo. Pero las clases, s�. Vamos a ver como:
El primer paso va a ser sustituir el tipo p�blico TipoAmigo por una clase p�blica clsAmigo. Tendr� las mismas variables. En alg�n caso (cuando no requieren "procesado, como es el caso del nombre) ser� como variables p�blicas. En otros casos (como con la fecha de nacimiento) ser� como propiedades, para poder incorporar "filtros" para los datos introducidos. En lugar de tener una propiedad "Edad", tendr� una propiedad p�blica "FechaNacimiento" y otra, de s�lo lectura, "EdadActual" (en este caso se ha a�adido un procedimieto Property Let que genera un error interceptable; si simplemente no se hubiera inclu�do el procediiento, se producir�a un error al compilar el m�dulo).
La clase se ha construido mediante el asistente de VB5 "Class Builder Utility", aunque posteriormente se ha modificado el nombre de las variables, y se ha modificado el c�digo
Adem�s, la propia clase tendr� sus reglas de validaci�n, que en este caso ser� s�lo para la fecha de nacimiento: los l�mites de valores, los valores por defecto... Est�n como constantes, pero se podr�an haber escrito como propiedades (o como propiedades ocultas, como luego veremos).
Lo m�s �til es que la clase no s�lo validar� sus datos y se inicializar�, sino que tambi�n la podremos pasar como un argumento, para poder establecer una comunicaci�n "privada" entre formularios. Esta comunicaci�n, adem�s, ser� independiente de la estructura interna de la clase clsAmigo (podemos a�adir propiedades, etc., sin tener que modificar los argumentos del m�dulo).
Cuando se vaya a a�adir o modificar un amigo, se env�a al formulario frmAmigos una copia de la clase clsAmigo. Puede ser ya una copia "rellena" (si se van a modificar los datos) o una copia en blanco (si se va a a�adir). Posteriormente, frmAmigos introduce los datos en la clase. La clase se "encarga" de avisar si los datos son incorrectos. Si todo va bien, frmAmigos informar� a frmGesti�n que no hay errores, y los datos se guardar�n.
La ventaja de pasar los datos por par�metros, en lugar de usar variables p�blicas, es que ya no hay valores "expuestos" que se puedan modificar por error. Los dos formularios y la clase se comunican con m�todos p�blicos, no mediante "atajos" como variables globales. Incluso hay mecanismos de seguridad (en frmAmigos) para evitar que pueda cargarse el formulario de forma an�mala (mediante .Show, sin pasar por su m�todo "PonerDatos". Quedan variables "indicadores de estado", pero ahora est�n dentro de los m�dulos, ocultas al resto del proyecto.
Como ahora cada m�dulo es independiente del funcionamiento y la estructura interna delos otros, se facilita el desarrollar en grupo. Basta con ponerse de acuerdo con la interfaz de clsAmigos, los m�todos, y luego cada uno trabajar (y mejorar) por su cuenta. Si posteriormente el desarrollador de clsAmigos decide modificarlo (por ejemplo, a�adir una nueva propiedad "Telefono") no afectar� al funcionamiento de frmGestion y frmAmigos (aunque no ser�n capaces de utilizar el nuevo dato).
En resumen: las clases tienen varias ventajas sobre los TDUs, pero las principales son que pueden ser miembros p�blicos de un m�dulo, y que pueden encargarse del procesado interno de los datos.
Los proyectos Pamig0 y Pamig1 muestran el c�digo anterior (con TDUs y con clases).
Sin embargo, el programa a�n no funciona bien: sigue habiendo una variable p�blica: la matriz de datos (de amigos). Est� expuesta a modificaciones sin control desde otros puntos del programa. Se puede acceder a los elementos de la matriz alegremente, e incluso a�adir o eliminarlos. Tal vez requiramos la utilizaci�n de alg�n tipo de matriz inteligente, como veremos m�s adelante.
Pulsa este link para bajarte el c�digo de ejemplo: Clases1c.zip 30.8 KB
Pulsa este link para ir a la p�gina de clases en Visual Basic