Pasar argumentos a Constructor en VBA

¿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 syntax 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.

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.

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 rest del código puede funcionar con la interfaz “abstracta” de IEmployee y disfrutar de un objeto inmóvil y polimórfico.

Usando el truco

 Attribute VB_PredeclaredId = True 

Encontré otra forma más compacta:

 Option Explicit Option Base 0 Option Compare Binary Private v_cBox As ComboBox ' ' Class creaor Public Function New_(ByRef cBox As ComboBox) As ComboBoxExt_c If Me Is ComboBoxExt_c Then Set New_ = New ComboBoxExt_c Call New_.New_(cBox) Else Set v_cBox = cBox End If End Function 

Como puede ver, se llama al constructor New_ para crear y establecer los miembros privados de la clase (como init), pero el único problema es que, si se llama a la instancia no estática, se reinicializará el miembro privado. pero eso se puede evitar estableciendo una bandera.

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.