por - Pasar argumentos a Constructor en VBA
paso de parametros en c++ (4)
¿Cómo puedes construir objetos pasando argumentos directamente a tus propias clases?
Algo como esto:
Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)
No poder hacer esto es muy molesto, y terminas con soluciones sucias para solucionarlo.
Aquí hay un pequeño truco que estoy usando últimamente y trae buenos resultados. Me gustaría compartir con aquellos que tienen que pelear a menudo con VBA.
1.- Implementar una subrutina de iniciación pública en cada una de sus clases personalizadas. Lo llamo InitiateProperties en todas mis clases. Este método tiene que aceptar los argumentos que le gustaría enviar al constructor.
2.- Crea un módulo llamado fábrica, y crea una función pública con la palabra "Crear" más el mismo nombre que la clase, y los mismos argumentos entrantes que el constructor necesita. Esta función tiene que crear una instancia de su clase y llamar a la subrutina de inicio explicada en el punto (1), pasando los argumentos recibidos. Finalmente devolvió el método instanciado e iniciado.
Ejemplo:
Digamos que tenemos la clase personalizada Employee. Como el ejemplo anterior, debe ser instanciado con nombre y edad.
Este es el método InitiateProperties. m_name y m_age son nuestras propiedades privadas que se establecerán.
Public Sub InitiateProperties(name as String, age as Integer)
m_name = name
m_age = age
End Sub
Y ahora en el módulo de fábrica:
Public Function CreateEmployee(name as String, age as Integer) as Employee
Dim employee_obj As Employee
Set employee_obj = new Employee
employee_obj.InitiateProperties name:=name, age:=age
set CreateEmployee = employee_obj
End Function
Y, finalmente, cuando desee crear una instancia de un empleado
Dim this_employee as Employee
Set this_employee = factory.CreateEmployee(name:="Johnny", age:=89)
Especialmente útil cuando tienes varias clases. Simplemente coloque una función para cada uno en la fábrica del módulo y cree una instancia simplemente llamando a factory.CreateClassA (arguments) , factory.CreateClassB (other_arguments) , etc.
EDITAR
Como señaló stenci, puede hacer lo mismo con una sintaxis de terser evitando crear una variable local en las funciones de constructor. Por ejemplo, la función CreateEmployee podría escribirse así:
Public Function CreateEmployee(name as String, age as Integer) as Employee
Set CreateEmployee = new Employee
CreateEmployee.InitiateProperties name:=name, age:=age
End Function
Cuál es más agradable.
Cuando exporta un módulo de clase y abre el archivo en el Bloc de notas, notará, cerca de la parte superior, un grupo de atributos ocultos (el VBE no los muestra y tampoco expone la funcionalidad para ajustar la mayoría de ellos). Uno de ellos es VB_PredeclaredId
:
Attribute VB_PredeclaredId = False
Establézcalo en True
, guarde y vuelva a importar el módulo en su proyecto de VBA.
Las clases con un PredeclaredId
tienen una "instancia global" que se obtiene de forma gratuita, exactamente como los módulos de UserForm
(exportar un formulario de usuario, verá que su atributo predeclaredId se establece en verdadero).
Mucha gente simplemente usa la instancia predeclarada para almacenar el estado. Eso está mal, es como guardar el estado de la instancia en una clase estática.
En cambio, aproveche esa instancia predeterminada para implementar su método de fábrica:
[Clase de Employee
]
''@PredeclaredId
Option Explicit
Private Type TEmployee
Name As String
Age As Integer
End Type
Private this As TEmployee
Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As Employee
With New Employee
.Name = emplName
.Age = emplAge
Set Create = .Self ''returns the newly created instance
End With
End Function
Public Property Get Self() As Employee
Set Self = Me
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Age() As String
Age = this.Age
End Property
Public Property Let Age(ByVal value As String)
this.Age = value
End Property
Con eso, puedes hacer esto:
Dim empl As Employee
Set empl = Employee.Create("Johnny", 69)
Employee.Create
funciona desde la instancia predeterminada , es decir, se considera un miembro del tipo y se invoca solo desde la instancia predeterminada.
El problema es que esto también es perfectamente legal:
Dim emplFactory As New Employee
Dim empl As Employee
Set empl = emplFactory.Create("Johnny", 69)
Y eso apesta, porque ahora tienes una API confusa. Podría usar ''@Description
annotations / VB_Description
para documentar el uso, pero sin Rubberduck no hay nada en el editor que le muestre esa información en los sitios de llamadas.
Además, los miembros de Property Let
son accesibles, por lo que su instancia Employee
es mutable :
empl.Name = "Booba" '' Johnny no more!
El truco es hacer que su clase implemente una interfaz que solo expone lo que debe exponerse:
[Clase de IEmployee
]
Option Explicit
Public Property Get Name() As String : End Property
Public Property Get Age() As Integer : End Property
Y ahora haces que Employee
implemente IEmployee
: la clase final podría verse así:
[Clase de Employee
]
''@PredeclaredId
Option Explicit
Implements IEmployee
Private Type TEmployee
Name As String
Age As Integer
End Type
Private this As TEmployee
Public Function Create(ByVal emplName As String, ByVal emplAge As Integer) As IEmployee
With New Employee
.Name = emplName
.Age = emplAge
Set Create = .Self ''returns the newly created instance
End With
End Function
Public Property Get Self() As IEmployee
Set Self = Me
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Age() As String
Age = this.Age
End Property
Public Property Let Age(ByVal value As String)
this.Age = value
End Property
Private Property Get IEmployee_Name() As String
IEmployee_Name = Name
End Property
Private Property Get IEmployee_Age() As Integer
IEmployee_Age = Age
End Property
Observe que el método Create
ahora devuelve la interfaz y la interfaz no expone los miembros de Property Let
? Ahora el código de llamada puede verse así:
Dim empl As IEmployee
Set empl = Employee.Create("Immutable", 42)
Y dado que el código del cliente se escribe contra la interfaz, los únicos miembros que se exponen son los miembros definidos por la interfaz IEmployee
, lo que significa que no ve el método Create
, ni el Self
getter, ni ninguno de los mutadores Property Let
: en lugar de trabajar con la clase Employee
"concreto", el resto del código puede funcionar con la interfaz "abstracta" de IEmployee
y disfrutar de un objeto inmóvil y polimórfico.
Otro enfoque
Supongamos que crea una clase clsBitcoinPublicKey
En el módulo de clase crea una subrutina ADICIONAL, que actúa como te gustaría que se comporte el verdadero constructor. A continuación lo he llamado ConstructorAdjunct.
Public Sub ConstructorAdjunct(ByVal ...)
...
End Sub
From the calling module, you use an additional statement
Dim loPublicKey AS clsBitcoinPublicKey
Set loPublicKey = New clsBitcoinPublicKey
Call loPublicKey.ConstructorAdjunct(...)
La única penalización es la llamada adicional, pero la ventaja es que puede mantener todo en el módulo de clase, y la depuración se vuelve más fácil.
Utilizo un módulo de Factory
que contiene uno (o más) constructor por clase que llama al miembro Init
de cada clase.
Por ejemplo, una clase Point
:
Class Point
Private X, Y
Sub Init(X, Y)
Me.X = X
Me.Y = Y
End Sub
Una clase de Line
Class Line
Private P1, P2
Sub Init(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
If P1 Is Nothing Then
Set Me.P1 = NewPoint(X1, Y1)
Set Me.P2 = NewPoint(X2, Y2)
Else
Set Me.P1 = P1
Set Me.P2 = P2
End If
End Sub
Y un módulo de Factory
:
Module Factory
Function NewPoint(X, Y)
Set NewPoint = New Point
NewPoint.Init X, Y
End Function
Function NewLine(Optional P1, Optional P2, Optional X1, Optional X2, Optional Y1, Optional Y2)
Set NewLine = New Line
NewLine.Init P1, P2, X1, Y1, X2, Y2
End Function
Function NewLinePt(P1, P2)
Set NewLinePt = New Line
NewLinePt.Init P1:=P1, P2:=P2
End Function
Function NewLineXY(X1, Y1, X2, Y2)
Set NewLineXY = New Line
NewLineXY.Init X1:=X1, Y1:=Y1, X2:=X2, Y2:=Y2
End Function
Un buen aspecto de este enfoque es que facilita el uso de las funciones de fábrica dentro de las expresiones. Por ejemplo, es posible hacer algo como:
D = Distance(NewPoint(10, 10), NewPoint(20, 20)
o:
D = NewPoint(10, 10).Distance(NewPoint(20, 20))
Está limpio: la fábrica hace muy poco y lo hace de forma consistente en todos los objetos, solo la creación y una invocación Init
a cada creador .
Y está bastante orientado a objetos: las funciones Init
se definen dentro de los objetos.
EDITAR
Olvidé agregar que esto me permite crear métodos estáticos. Por ejemplo, puedo hacer algo como (después de hacer los parámetros opcionales):
NewLine.DeleteAllLinesShorterThan 10
Desafortunadamente, se crea una nueva instancia del objeto cada vez, por lo que cualquier variable estática se perderá después de la ejecución. La colección de líneas y cualquier otra variable estática utilizada en este método pseudo-estático debe definirse en un módulo.