web-dev-qa-db-fra.com

Passer les arguments au constructeur dans VBA

Comment pouvez-vous construire des objets en passant des arguments directement à vos propres classes?

Quelque chose comme ça:

Dim this_employee as Employee
Set this_employee = new Employee(name:="Johnny", age:=69)

Ne pas être capable de faire cela est très ennuyant, et vous vous retrouvez avec des solutions sales pour résoudre ce problème.

71
bgusach

Voici un petit truc que j'utilise dernièrement et qui donne de bons résultats. Je voudrais partager avec ceux qui doivent souvent se battre 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 "Create", ainsi que le même nom que la classe et les mêmes arguments entrants que le constructeur en a besoin. Cette fonction doit instancier votre classe et appeler le sous-programme d'initiation expliqué au point (1) en transmettant les arguments reçus. Enfin retourné la méthode instanciée et initiée.

Exemple:

Disons que nous avons la classe personnalisée Employee. Comme dans l'exemple précédent, il doit être instancié avec le nom et l'âge.

C'est la méthode InitiateProperties. m_name et m_age sont nos propriétés privées à 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 cours. Il suffit de placer une fonction pour chacun dans la fabrique de modules et d’instancier simplement en appelant factory.CreateClassA (arguments), factory.CreateClassB (other_arguments), etc.

MODIFIER

Comme l'a souligné Stenci, vous pouvez faire la même chose avec une syntaxe de testeur en évitant de créer une variable locale dans les fonctions du constructeur. Par exemple, la fonction CreateEmployee pourrait être écrite comme suit:

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 gentil.

107
bgusach

J'utilise un module Factory contenant un (ou plus) constructeur par classe qui appelle le membre Init 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

Une classe 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

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 bel aspect de cette approche est qu’il est facile d’utiliser les fonctions 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 elle le fait de manière cohérente pour tous les objets, juste la création et un appel Init à chaque créateur .

Et c'est plutôt orienté objet: les fonctions Init sont définies à l'intérieur des objets.

EDIT

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. Toute variable statique sera donc perdue après l'exécution. La collection de lignes et toute autre variable statique utilisée dans cette méthode pseudo-statique doivent être définies dans un module.

32
stenci

Lorsque vous exportez un module de classe et ouvrez le fichier dans le Bloc-notes, vous remarquerez presque en haut un ensemble d'attributs cachés (le VBE ne les affiche pas et n'expose pas non plus de fonctionnalités à Tweak). 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 PredeclaredId ont une "instance globale" que vous obtenez gratuitement - exactement comme UserForm modules (exportez un formulaire utilisateur, vous verrez que son attribut predeclaredId 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 d'une instance dans une classe statique!

Au lieu de cela, vous utilisez cette instance par défaut pour implémenter votre méthode factory:

[Employee class]

'@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 ça, vous pouvez faire ceci:

Dim empl As Employee
Set empl = Employee.Create("Johnny", 69)

Employee.Create travaille sur le instance par défaut, c’est-à-dire qu’il est considéré comme un membre du type et n’est appelé que par l’instance par défaut.

Le problème, 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 déroutante. Vous pouvez utiliser '@Description annotations/VB_Description attributs de documenter l'utilisation, mais sans Rubberduck il n'y a rien dans l'éditeur qui vous montre cette information sur les sites d'appels.

En outre, le Property Let membres sont accessibles, votre instance Employee est donc mutable:

empl.Name = "Booba" ' Johnny no more!

L'astuce consiste à faire en sorte que votre classe implémente un interface qui expose uniquement ce qui doit être exposé:

[IEmployee class]

Option Explicit

Public Property Get Name() As String : End Property
Public Property Get Age() As Integer : End Property

Et maintenant vous faites EmployeeimplémenterIEmployee - la classe finale pourrait ressembler à ceci:

[Employee class]

'@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

Notez que la méthode Create renvoie maintenant l'interface, et l'interface ne le fait pas expose le Property Let membres? Le code appelant peut ressembler à ceci:

Dim empl As IEmployee
Set empl = Employee.Create("Immutable", 42)

Et puisque le code client est écrit sur l'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 Self getter, ni aucun des Property Let mutateurs: au lieu de travailler avec la classe "concrete" Employee, le reste du code peut fonctionner avec l'interface "abstraite" IEmployee et profiter d'un objet polymorphe immuable.

20
Mathieu Guindon

En utilisant le truc

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 constater, le constructeur New_ est appelé pour créer et définir les membres privés de la classe (comme init). Le seul problème est que, si elle est appelée sur une instance non statique, elle réinitialise le membre privé. mais cela peut être évité en plaçant un drapeau.

2
Tomasz