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.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow