Ponga una clase en su vida
Fecha: 24/Ago/98 (18/Ago/98)
Autor: Luis Sanz, Hospital "Reina Sofía" hrst@ctv.es
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