Passer des arguments au constructeur dans VBA
Comment Pouvez-vous construire des objets en passant des arguments directement à vos propres classes?
Quelque Chose comme ceci:
Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)
Ne pas être capable de le faire est très ennuyeux, et vous vous retrouvez avec des solutions sales pour contourner ce problème.
5 réponses
Voici un petit truc que j'utilise dernièrement et qui apporte de bons résultats. Je voudrais partager avec ceux qui doivent se battre souvent avec VBA.
1.- implémentez un sous-programme d'initiation public dans chacune de vos classes personnalisées. Je l'appelle InitiateProperties dans toutes mes classes. Cette méthode doit accepter les arguments que vous souhaitez envoyer au constructeur.
2.- Créez un module appelé factory, et créez une fonction publique avec le mot "créer" plus le même nom comme la classe, et les mêmes arguments entrants que le constructeur a besoin. Cette fonction doit instancier votre classe, et appeler le sous-programme d'initiation expliqué au point (1), en passant les arguments reçus. Enfin retourné la méthode instanciée et initiée.
Exemple:
Disons que nous avons L'employé de classe personnalisée. Comme l'exemple précédent, is doit être instancié avec name et age.
C'est la méthode InitiateProperties. m_name et m_age sont nos privée propriétés à définir.
Public Sub InitiateProperties(name as String, age as Integer)
m_name = name
m_age = age
End Sub
Et maintenant dans le module d'usine:
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
Et enfin quand vous voulez instancier un employé
Dim this_employee as Employee
Set this_employee = factory.CreateEmployee(name:="Johnny", age:=89)
Particulièrement utile lorsque vous avez plusieurs classes. Il suffit de placer une fonction pour chacun dans la fabrique du module et d'instancier simplement en appelant factory.CreateClassA (arguments), usine.CreateClassB (other_arguments) , etc.
Modifier
Comme l'a souligné stenci, vous pouvez faire la même chose avec une syntaxe terser en éviter de créer une variable locale dans les fonctions du constructeur. Par exemple, la fonction CreateEmployee peut être écrite comme ceci:
Public Function CreateEmployee(name as String, age as Integer) as Employee
Set CreateEmployee = new Employee
CreateEmployee.InitiateProperties name:=name, age:=age
End Function
Ce qui est plus agréable.
, j'utilise un Factory
module qui contient une (ou plusieurs) constructeur par classe qui appelle la Init
membre de chaque classe.
Par exemple une classe Point
:
Class Point
Private X, Y
Sub Init(X, Y)
Me.X = X
Me.Y = Y
End Sub
Un Line
classe
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
Et un module 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 aspect intéressant de cette approche est qu'il est facile d'utiliser les fonctions d'usine dans les expressions. Par exemple, il est possible de faire quelque chose comme:
D = Distance(NewPoint(10, 10), NewPoint(20, 20)
Ou:
D = NewPoint(10, 10).Distance(NewPoint(20, 20))
C'est propre: l'usine fait très peu et il le fait de manière cohérente sur tous les objets, juste la création et un Init
appel à chaque créateur .
Et c'est assez orienté objet: les fonctions Init
sont définies à l'intérieur des objets.
Modifier
J'ai oublié d'ajouter que cela me permet de créer des méthodes statiques. Par exemple, je peux faire quelque chose comme (après avoir rendu les paramètres facultatifs):
NewLine.DeleteAllLinesShorterThan 10
Malheureusement, une nouvelle instance de l'objet est créée à chaque fois, donc tout la variable statique sera perdue après l'exécution. La collection de lignes et toute autre variable statique utilisée dans cette méthode pseudo-statique doit être définie dans un module.
Lorsque vous exportez un module de classe et ouvrez le fichier dans le bloc-notes, vous remarquerez, près du haut, un tas d'attributs cachés (le VBE ne les affiche pas, et n'expose pas la fonctionnalité pour modifier la plupart d'entre eux non plus). L'un d'eux est VB_PredeclaredId
:
Attribute VB_PredeclaredId = False
Définissez-le sur True
, Enregistrez et réimportez le module dans votre projet VBA.
Les Classes avec un PredeclaredId
ont une" instance globale " que vous obtenez gratuitement-exactement comme les modules UserForm
(exportez un formulaire utilisateur, vous verrez son predeclaredId l'attribut est défini sur true).
Beaucoup de gens utilisent simplement l'instance prédéclarée pour stocker l'état. C'est faux - c'est comme stocker l'état de l'instance dans une classe statique!
Au lieu de cela, vous utilisez cette instance par défaut pour implémenter votre méthode d'usine:
[Employee
classe]
'@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
, Avec cela, vous pouvez faire ceci:
Dim empl As Employee
Set empl = Employee.Create("Johnny", 69)
Employee.Create
Fonctionne à partir de l'instance par défaut , c'est-à-dire qu'elle est considérée comme un membre du type , et invoquée depuis l'instance par défaut uniquement.
Le problème est, c'est aussi parfaitement légal:
Dim emplFactory As New Employee
Dim empl As Employee
Set empl = emplFactory.Create("Johnny", 69)
Et ça craint, parce que maintenant vous avez une API confuse. Vous pouvez utiliser '@Description
annotations / VB_Description
attributs pour l'utilisation du document, mais sans Rubberduck, il n'y a rien dans l'éditeur qui vous montre cette information sur les sites d'appel.
En outre, les membres Property Let
sont accessibles, donc votre instance Employee
est mutable :
empl.Name = "Booba" ' Johnny no more!
L'astuce est de faire en sorte que votre classe implémente une interface qui expose uniquement ce qui doit être exposé:
[IEmployee
classe]
Option Explicit
Public Property Get Name() As String : End Property
Public Property Get Age() As Integer : End Property
Et maintenant vous pouvez faire des Employee
mettre en œuvre IEmployee
- la classe finale pourrait ressembler à ceci:
[Employee
classe]
'@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
Avis de la Create
méthode retourne maintenant l'interface, et l'interface n'est pas exposer le Property Let
membres? Maintenant, le code d'appel peut ressembler à ceci:
Dim empl As IEmployee
Set empl = Employee.Create("Immutable", 42)
Et puisque le code client est écrit contre le interface, les seuls membres empl
exposés sont les membres définis par l'interface IEmployee
, ce qui signifie qu'il ne voit pas la méthode Create
, ni le getter Self
, ni aucun des mutateurs Property Let
: donc au lieu de travailler avec la classe" concrète "Employee
, le reste du code peut fonctionner avec l'interface "abstraite" IEmployee
, et profiter d'un objet
Utiliser l'astuce
Attribute VB_PredeclaredId = True
J'ai trouvé un autre moyen plus compact:
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
Comme vous pouvez le voir, le constructeur New_ est appelé à la fois pour créer et définir les membres privés de la classe (comme init). mais cela peut être évité en définissant un drapeau.
Une Autre approche
Disons que vous créez une classe clsBitcoinPublicKey
Dans le module de classe, créez un sous-programme supplémentaire, qui agit comme vous voulez que le constructeur réel se comporte. Ci-dessous, je l'ai nommé Constructoradjoint.
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 seule pénalité est l'appel supplémentaire, mais l'avantage est que vous pouvez tout garder dans le module de classe, et le débogage devient plus facile.