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 clsPrueba
Set 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 clsPrueba
CPrueba.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:

  • Cuando la variable sale de ámbito (al finalizar el procedimiento o el módulo en que está declarada, o al finalizar el programa).
     
  • Haciendo referencia a Nothing:
    Set CPrueba = Nothing

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 Explicit
Private CContar As clsContar
Private Sub cmdContar_Click()
    CContar.Sumar
End Sub
Private 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 Explicit
Private CContar As New clsContar
Private 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 Sub
Public 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.Sumar
I = 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 Integer
Public Property Let NumeroInicio(ByVal NuevoNumero As Integer)
    iMNumeroInicio = NuevoNumero
    iMNumero = iMNumeroInicio
End Property
Public 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 Form
Public Property Set Ventana(ByVal NuevaVentana As Form)
    Set FMVentana = NuevaVentana
End Property
Public 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 Sub
Private 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 Explicit
Private CSuma() As New clsSumar
Private Sub Form_Load()
    ReDim CSuma(cmdRascar.UBound)
End Sub
Private 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 Explicit
Private iMSuma As Integer
Public Function Sumar(Sumando As Integer) As Integer
    iMSuma = iMSuma + Sumando
    Sumar = iMSuma
End Function
'modSuma
Option Explicit
Private iMSuma As Integer
Public 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 Explicit
Private iMSuma(1) As Integer
Public 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 Explicit
Public 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 clsCaja
Private cCaja2 As New clsCaja
Private 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 Explicit
Private CajaCambiado As Boolean
Public Sub Actualizar(Cambio As Boolean)
    CajaCambiado = Cambio
End Sub
Public Function Cambiado(hwnd As Long) As Boolean
    Cambiado = CajaCambiado
End Function
'frmCambio
Option Explicit
Private Sub Text1_Change()
    Releer
End Sub
Private 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

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 Explicit
Private Sub Text1_Change(Index As Integer)
    Actualizar Text1.hwnd, True
    Releer
End Sub
Private Sub Text2_Change()
    Actualizar Text2.hwnd, True
    Releer
End Sub
Private 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 Explicit
Private 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 Sub
Public 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 Function
Private 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:

  • No se ha incluído ninguna rutina para "eliminar" controles: cada vez que descargamos un formulario, se guardan los controles "antiguos" (y si se recarga el formulario, como variará el hwnd, no se reutilizarán).
  • No podemos acceder a valores antiguos "guardados", ya que no podemos encontrarlos (el hwnd habrá variado).
  • Si los controles tienen que interactuar entre ellos, puede ser necesaria una rutina que indique que "cambiado" no debe utilizarse. Con este sistema, es muy difícil implementarlo.
  • Para leer los valores de "Cambiado", es necesario recorrer la colección Controls del formulario (y automáticamente añade los controles no usados, que tal vez no interesen).

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 Explicit
Public 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 Explicit
Private 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 Sub
Private Sub Text1_Change(Index As Integer)
    cText(Index).Change
    Releer
End Sub
Private Sub Text2_Change()
    cText(3).Change
    Releer
End Sub
Private 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:

  • Cada instancia de la clase clsCaja se destruye al salir del formulario, y evitamos un gasto inútil de recursos.
  • Es mucho más fácil mejorar el código de la clase. Por ejemplo: podemos añadir el procedimiento Property Let para la propiedad de sólo lectura (para que nos muestre un error interceptable, en lugar de un error al compilar). También podemos añadir nuevos métodos, etc. Como no varían los antiguos, no afectan al programa. También podría haberse hecho con el módulo, pero con mayor dificultad.
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 Explicit
Private CAños As New clsAños
Private Sub Form_Load()
    txtAños.Text = CAños.EdadPersonaAños
End Sub
Private Sub txtAños_Change()
    CAños.EdadPersonaAños = Val(txtAños)
End Sub 

Y en la clase clsAños

Option Explicit
Private 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 Property
Public Property Get EdadPersonaAños() As Integer
    EdadPersonaAños = iMEdadPersonaAños
End Property
Private 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 = 18
Private 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 String
Public Apellido1() As String
Public Apellido2() As String
Public Edad() As Integer
Public 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 Explicit
Public Type TipoAmigo
    Nombre As String
    Apellido1 As String
    Apellido2 As String
    Edad As Integer
    SexoVaron As Boolean
End Type
Public 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:

  • Requiere la utilización de varias variables públicas (una variable TipoAmigo, y dos variables booleanas "indicadoras de estado". Aunque funcione, no es una buena práctica de programación: ¿Qué pasaría si tuviésemos ocho formularios de introducción de datos? Cada uno de ellos requeriría sus propias variables públicas, y si por error un formulario modifica una variable que no sea la suya (por un error de nomenclatura), o nos olvidamos de "poner a cero" algún indicador, se producirían errores muy difíciles de depurar.
  • La matriz de TDUs dónde se almacenan los datos en memoria, al ser pública, es accesible desde cualquier punto del programa. Y podemos modificarla por error. Por ejemplo, podríamos haber hecho una rutina que comprobase que las edades fuesen correctas. Pero podemos acceder a la matriz de TDUs sin pasar por esa rutina. Esos "atajos" son una fuente potencial de errores.
  • No se filtran los valores introducidos. La validación de los datos debe hacerse a nivel de formulario, con todos los problemas que ello implica (ver "Variables con límites").

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

ir al índice

Pulsa este link para ir a la página de clases en Visual Basic

Este otro te llevará al la segunda entrega