VBA
VBA orientado a objetos
Buscar..
Abstracción
Los niveles de abstracción ayudan a determinar cuándo dividir las cosas.
La abstracción se logra mediante la implementación de la funcionalidad con un código cada vez más detallado. El punto de entrada de una macro debe ser un procedimiento pequeño con un alto nivel de abstracción que facilite comprender de un vistazo lo que está sucediendo:
Public Sub DoSomething()
With New SomeForm
Set .Model = CreateViewModel
.Show vbModal
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
El procedimiento DoSomething
tiene un alto nivel de abstracción : podemos decir que está mostrando un formulario y creando algún modelo, y pasar ese objeto a algún procedimiento ProcessUserData
que sabe qué hacer con él; cómo se crea el modelo es el trabajo de otro procedimiento:
Private Function CreateViewModel() As ISomeModel
Dim result As ISomeModel
Set result = SomeModel.Create(Now, Environ$("UserName"))
result.AvailableItems = GetAvailableItems
Set CreateViewModel = result
End Function
La función CreateViewModel
solo es responsable de crear algunas instancias de ISomeModel
. Parte de esa responsabilidad es adquirir una serie de elementos disponibles ; la forma en que se adquieren estos elementos es un detalle de implementación que se GetAvailableItems
procedimiento GetAvailableItems
:
Private Function GetAvailableItems() As Variant
GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function
Aquí, el procedimiento es leer los valores disponibles de un rango con nombre en una hoja de cálculo DataSheet
. Igualmente, podría ser leerlos desde una base de datos, o los valores podrían estar codificados: es un detalle de la implementación que no es motivo de preocupación para ninguno de los niveles de abstracción más altos.
Encapsulacion
La encapsulación oculta los detalles de implementación del código del cliente.
El ejemplo de Handling QueryClose muestra la encapsulación: el formulario tiene un control de casilla de verificación, pero su código de cliente no funciona directamente con ella: la casilla de verificación es un detalle de implementación , lo que el código de cliente debe saber es si la configuración está habilitada o no.
Cuando el valor de la casilla de verificación cambia, el controlador asigna un miembro de campo privado:
Private Type TView IsCancelled As Boolean SomeOtherSetting As Boolean 'other properties skipped for brievety End Type Private this As TView '... Private Sub SomeOtherSettingInput_Change() this.SomeOtherSetting = CBool(SomeOtherSettingInput.Value) End Sub
Y cuando el código del cliente quiere leer ese valor, no necesita preocuparse por una casilla de verificación; en su lugar, simplemente utiliza la propiedad SomeOtherSetting
:
Public Property Get SomeOtherSetting() As Boolean SomeOtherSetting = this.SomeOtherSetting End Property
La propiedad SomeOtherSetting
encapsula el estado de la casilla de verificación; el código del cliente no necesita saber que hay una casilla de verificación involucrada, solo que hay una configuración con un valor booleano. Al encapsular el valor Boolean
, hemos agregado una capa de abstracción alrededor de la casilla de verificación.
Usando interfaces para imponer la inmutabilidad.
Vayamos un paso más allá encapsulando el modelo del formulario en un módulo de clase dedicado. Pero si hiciéramos una Public Property
para el nombre de UserName
y la Timestamp
, tendríamos que exponer los accesores de Property Let
, haciendo que las propiedades sean mutables, y no queremos que el código del cliente tenga la capacidad de cambiar estos valores una vez que estén establecidos.
La función CreateViewModel
en el ejemplo de Abstraction devuelve una clase de ISomeModel
: esa es nuestra interfaz , y se parece a esto:
Option Explicit
Public Property Get Timestamp() As Date
End Property
Public Property Get UserName() As String
End Property
Public Property Get AvailableItems() As Variant
End Property
Public Property Let AvailableItems(ByRef value As Variant)
End Property
Public Property Get SomeSetting() As String
End Property
Public Property Let SomeSetting(ByVal value As String)
End Property
Public Property Get SomeOtherSetting() As Boolean
End Property
Public Property Let SomeOtherSetting(ByVal value As Boolean)
End Property
Observe que las propiedades de Timestamp
y nombre de UserName
solo exponen una Property Get
acceso. Ahora la clase SomeModel
puede implementar esa interfaz:
Option Explicit
Implements ISomeModel
Private Type TModel
Timestamp As Date
UserName As String
SomeSetting As String
SomeOtherSetting As Boolean
AvailableItems As Variant
End Type
Private this As TModel
Private Property Get ISomeModel_Timestamp() As Date
ISomeModel_Timestamp = this.Timestamp
End Property
Private Property Get ISomeModel_UserName() As String
ISomeModel_UserName = this.UserName
End Property
Private Property Get ISomeModel_AvailableItems() As Variant
ISomeModel_AvailableItems = this.AvailableItems
End Property
Private Property Let ISomeModel_AvailableItems(ByRef value As Variant)
this.AvailableItems = value
End Property
Private Property Get ISomeModel_SomeSetting() As String
ISomeModel_SomeSetting = this.SomeSetting
End Property
Private Property Let ISomeModel_SomeSetting(ByVal value As String)
this.SomeSetting = value
End Property
Private Property Get ISomeModel_SomeOtherSetting() As Boolean
ISomeModel_SomeOtherSetting = this.SomeOtherSetting
End Property
Private Property Let ISomeModel_SomeOtherSetting(ByVal value As Boolean)
this.SomeOtherSetting = value
End Property
Public Property Get Timestamp() As Date
Timestamp = this.Timestamp
End Property
Public Property Let Timestamp(ByVal value As Date)
this.Timestamp = value
End Property
Public Property Get UserName() As String
UserName = this.UserName
End Property
Public Property Let UserName(ByVal value As String)
this.UserName = value
End Property
Public Property Get AvailableItems() As Variant
AvailableItems = this.AvailableItems
End Property
Public Property Let AvailableItems(ByRef value As Variant)
this.AvailableItems = value
End Property
Public Property Get SomeSetting() As String
SomeSetting = this.SomeSetting
End Property
Public Property Let SomeSetting(ByVal value As String)
this.SomeSetting = value
End Property
Public Property Get SomeOtherSetting() As Boolean
SomeOtherSetting = this.SomeOtherSetting
End Property
Public Property Let SomeOtherSetting(ByVal value As Boolean)
this.SomeOtherSetting = value
End Property
Los miembros de la interfaz son todos Private
, y todos los miembros de la interfaz deben implementarse para que el código se compile. Los miembros Public
no forman parte de la interfaz y, por lo tanto, no están expuestos a código escrito contra la interfaz de ISomeModel
.
Usando un método de fábrica para simular un constructor
Usando un atributo VB_PredeclaredId , podemos hacer que la clase SomeModel
tenga una instancia predeterminada y escribir una función que funcione como un miembro de nivel de tipo ( Shared
en VB.NET, static
en C #) al que el código de cliente pueda llamar sin necesidad de crear primero Una instancia, como hicimos aquí:
Private Function CreateViewModel() As ISomeModel Dim result As ISomeModel Set result = SomeModel.Create(Now, Environ$("UserName")) result.AvailableItems = GetAvailableItems Set CreateViewModel = result End Function
Este método de fábrica asigna los valores de propiedad que son de solo lectura cuando se accede desde la interfaz de ISomeModel
, aquí Timestamp
y UserName
:
Public Function Create(ByVal pTimeStamp As Date, ByVal pUserName As String) As ISomeModel
With New SomeModel
.Timestamp = pTimeStamp
.UserName = pUserName
Set Create = .Self
End With
End Function
Public Property Get Self() As ISomeModel
Set Self = Me
End Property
Y ahora podemos codificar en contra de la ISomeModel
interfaz, que expone Timestamp
y UserName
como propiedades de sólo lectura que no se pueden reasignar (siempre y cuando el código está escrito en contra de la interfaz).
Polimorfismo
El polimorfismo es la capacidad de presentar la misma interfaz para diferentes implementaciones subyacentes.
La capacidad de implementar interfaces permite desacoplar completamente la lógica de la aplicación de la interfaz de usuario, o de la base de datos, o de esta o aquella hoja de trabajo.
Supongamos que tiene una interfaz ISomeView
que el formulario implementa:
Option Explicit
Public Property Get IsCancelled() As Boolean
End Property
Public Property Get Model() As ISomeModel
End Property
Public Property Set Model(ByVal value As ISomeModel)
End Property
Public Sub Show()
End Sub
El código subyacente del formulario podría verse así:
Option Explicit
Implements ISomeView
Private Type TView
IsCancelled As Boolean
Model As ISomeModel
End Type
Private this As TView
Private Property Get ISomeView_IsCancelled() As Boolean
ISomeView_IsCancelled = this.IsCancelled
End Property
Private Property Get ISomeView_Model() As ISomeModel
Set ISomeView_Model = this.Model
End Property
Private Property Set ISomeView_Model(ByVal value As ISomeModel)
Set this.Model = value
End Property
Private Sub ISomeView_Show()
Me.Show vbModal
End Sub
Private Sub SomeOtherSettingInput_Change()
this.Model.SomeOtherSetting = CBool(SomeOtherSettingInput.Value)
End Sub
'...other event handlers...
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub CancelButton_Click()
this.IsCancelled = True
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
this.IsCancelled = True
Me.Hide
End If
End Sub
Pero entonces, nada prohíbe la creación de otro módulo de clase que implemente la interfaz ISomeView
sin ser un formulario de usuario ; esto podría ser una clase SomeViewMock
:
Option Explicit
Implements ISomeView
Private Type TView
IsCancelled As Boolean
Model As ISomeModel
End Type
Private this As TView
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Let IsCancelled(ByVal value As Boolean)
this.IsCancelled = value
End Property
Private Property Get ISomeView_IsCancelled() As Boolean
ISomeView_IsCancelled = this.IsCancelled
End Property
Private Property Get ISomeView_Model() As ISomeModel
Set ISomeView_Model = this.Model
End Property
Private Property Set ISomeView_Model(ByVal value As ISomeModel)
Set this.Model = value
End Property
Private Sub ISomeView_Show()
'do nothing
End Sub
Y ahora podemos cambiar el código que funciona con un UserForm
y hacerlo funcionar fuera de la interfaz ISomeView
, por ejemplo, dándole el formulario como un parámetro en lugar de crear una instancia de él:
Public Sub DoSomething(ByVal view As ISomeView)
With view
Set .Model = CreateViewModel
.Show
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Debido a que el DoSomething
método depende de una interfaz (es decir, una abstracción) y no una clase concreta (por ejemplo, un determinado UserForm
), podemos escribir una prueba unitaria automatizado que asegura que ProcessUserData
no se ejecuta cuando view.IsCancelled
es True
, haciendo que nuestra prueba crear una instancia de SomeViewMock
, establecer su propiedad IsCancelled
en True
y pasarla a DoSomething
.
Código verificable depende de abstracciones
Se pueden hacer pruebas unitarias de escritura en VBA, hay complementos que incluso se integran en el IDE. Pero cuando el código se combina estrechamente con una hoja de trabajo, una base de datos, un formulario o el sistema de archivos, entonces la prueba de la unidad comienza a requerir una hoja de trabajo, una base de datos, un formulario o un sistema de archivos reales, y estas dependencias son nuevas fallas fuera de control puntos que el código comprobable debe aislar, por lo que las pruebas unitarias no requieren una hoja de trabajo, base de datos, formulario o sistema de archivos real.
Al escribir código contra interfaces, de una manera que permite que el código de prueba inyecte implementaciones de código auxiliar / simulacro (como el ejemplo anterior de SomeViewMock
), puede escribir pruebas en un "entorno controlado", y simular lo que sucede cuando cada uno de los 42 posibles permutaciones de las interacciones del usuario en los datos del formulario, sin siquiera mostrar una vez el formulario y hacer clic manualmente en un control de formulario.