92 votes

Passer des arguments au constructeur en VBA

Comment construire des objets 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 pouvoir le faire est très ennuyeux, et vous finissez par trouver des solutions sales pour contourner ce problème.

128voto

ikaros45 Points 2911

Voici une petite astuce que j'utilise ces derniers temps et qui donne de bons résultats. J'aimerais la partager avec ceux qui doivent souvent se battre avec VBA.

1.- Implémentez une sous-routine d'initiation publique 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" plus le même nom que la classe, et les mêmes arguments entrants que le constructeur a besoin. Cette fonction doit instancier votre classe, et appeler la sous-routine d'initiation expliquée au point (1), en passant les arguments reçus. Enfin, la méthode instanciée et initiée est retournée.

Ejemplo:

Disons que nous avons la classe personnalisée Employé. Comme dans l'exemple précédent, elle doit être instanciée 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 de l'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 chacune d'elles dans la fabrique du module et de l'instancier en appelant simplement factory.CreateClassA(arguments) , factory.CreateClassB(other_arguments) etc.

EDIT

Comme l'a souligné stenci, on peut faire la même chose avec une syntaxe plus légère en évitant de créer une variable locale dans les fonctions du constructeur. Par exemple la fonction CreateEmployee pourrait ê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.

42voto

stenci Points 1394

J'en utilise un Factory qui contient un (ou plusieurs) Constructeur par classe qui appelle le Init membre de chaque classe.

Par exemple, un Point classe :

Class Point
Private X, Y
Sub Init(X, Y)
  Me.X = X
  Me.Y = Y
End Sub

A 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 Factory module :

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'elle permet d'utiliser facilement 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 : la fabrique fait très peu de choses et elle le fait de manière cohérente pour tous les objets, juste la création et un Init faire appel à chaque créateur .

Et il est assez orienté objet : le 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, donc toute 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.

39voto

retailcoder Points 3056

Lorsque vous exportez un module de classe et que vous ouvrez le fichier dans le Bloc-notes, vous remarquerez, près du sommet, une série d'attributs cachés (le VBE ne les affiche pas et ne propose pas non plus de fonctionnalité pour modifier la plupart d'entre eux). L'un d'entre eux est VB_PredeclaredId :

Attribute VB_PredeclaredId = False

Réglez-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 UserForm (exportez un formulaire utilisateur, vous verrez que son attribut predeclaredId est défini sur true).

Beaucoup de gens se contentent d'utiliser l'instance prédéclarée pour stocker l'état. C'est faux - cela revient à stocker l'état de l'instance dans une classe statique !

Au lieu de cela, vous vous appuyez sur cette instance par défaut pour mettre en œuvre 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 ça, vous pouvez faire ça :

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

Employee.Create travaille à partir du instance par défaut c'est-à-dire qu'il est considéré comme un membre de l'équipe de l type et invoqué uniquement à partir de l'instance par défaut.

Le problème, c'est que 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 pourriez utiliser '@Description annotations / VB_Description pour documenter l'utilisation, mais sans Rubberduck, il n'y a rien dans l'éditeur qui vous montre cette information aux sites d'appel.

En outre, le Property Let sont accessibles, de sorte que votre Employee L'instance est mutable :

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

L'astuce consiste à faire en sorte que votre classe implémente un interface qui n'expose que 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 faites 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

Remarquez le Create renvoie désormais la méthode l'interface et l'interface n'a 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 du client est écrit contre l'interface, les seuls membres empl expose sont les membres définis par le IEmployee ce qui signifie qu'elle ne voit pas l'interface Create ni la méthode Self ni aucun des éléments suivants Property Let mutateurs : ainsi, au lieu de travailler avec les mutateurs "concrets" de l Employee le reste du code peut travailler avec la classe "abstraite". IEmployee et profiter d'un objet immuable et polymorphe.

2voto

Tomasz Points 154

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é pour créer et définir les membres privés de la classe (comme init). Le seul problème est que s'il est appelé sur une instance non statique, il réinitialisera le membre privé, mais cela peut être évité en mettant un drapeau.

-1voto

Mark E. Points 130

Tout d'abord, voici un résumé/une comparaison très rapide de l'approche de base et des trois premières réponses.

L'approche de base : Ce sont les moyens de base pour construire de nouvelles instances d'une classe :

Dim newEmployee as Employee
Dim newLunch as Lunch

'==Very basic==
Set newEmployee = new Employee
newEmployee.Name = "Cam"
newEmployee.Age = 42

'==Use a method==
Set newLunch = new Lunch
newLunch.Construct employeeName:= "Cam" food:="Salad", drink:="Tea"

Ci-dessus, Construct serait un sous de la classe Lunch qui assigne les valeurs des paramètres à un objet.

Le problème est que, même avec une méthode, il fallait deux lignes, la première pour définir le nouvel objet, et la seconde pour remplir les paramètres. Ce serait bien de faire les deux en une seule ligne.

1) La classe Factory (bgusach) : Créez une classe distincte ("Factory"), avec des méthodes permettant de créer des instances de toute autre classe souhaitée, y compris des paramètres de configuration.

Utilisation possible :

Dim f as Factory 'a general Factory object
Dim newEmployee as Employee
Dim newLunch as Lunch

Set f = new Factory
Set newEmployee = f.CreateEmployee("Bob", 25) 
Set newLunch = f.CreateLunch("Bob", "Sandwich", "Soda")

Quand vous tapez "f." dans la fenêtre de code, après que vous ayez Dim f as Factory vous voyez un menu de ce qu'il peut créer via Intellisense.

2) Le module d'usine (stenci) : Idem, mais au lieu d'une classe, Factory peut être un module standard.

Utilisation possible :

Dim newEmployee as Employee
Dim newLunch as Lunch

Set newEmployee = CreateEmployee("Jan", 31) 'a function
Set newLunch = CreateLunch("Jan", "Pizza", "JuiceBox")

En d'autres termes, nous créons simplement une fonction en dehors de la classe pour créer de nouveaux objets avec des paramètres. De cette façon, vous n'avez pas besoin de créer ou de faire référence à un objet Factory. Vous ne bénéficiez pas non plus de l'intellisense as-you-type de la classe factory générale.

3) L'instance globale (Mathieu Guindon) : Nous revenons ici à l'utilisation d'objets pour créer des classes, mais en nous en tenant à la classe à créer. Si vous modifiez le module de classe dans un éditeur de texte externe, vous pouvez appeler les méthodes de la classe avant de créer un objet.

Utilisation possible :

Dim newEmployee as Employee
Dim newLunch as Lunch

Set newEmployee = newEmployee.MakeNew("Ace" 50)
Set newLunch = newLunch.MakeNew("Ace", "Burrito", "Water")

Ici MakeNew est une fonction comme CreateEmployee o CreateLunch dans la classe générale de l'usine, sauf qu'ici elle est dans la classe à fabriquer, et donc nous n'avons pas à spécifier quelle classe elle va fabriquer.

Cette troisième approche a un aspect fascinant de "création à partir d'elle-même", permis par l'instance globale.

D'autres idées : Auto-instauration, méthode Clone ou classe de collection parentale. Avec l'instanciation automatique ( Dim NewEmployee as new Employee (notez le mot "nouveau"), vous pouvez obtenir quelque chose de similaire à l'instance globale sans le processus de configuration :

Dim NewEmployee as new Employee
NewEmployee.Construct("Sam", 21)

Avec "nouveau" dans le Dim l'objet NewEmployee est créé comme une étape préalable implicite à l'appel de sa méthode. Construct est un Sub dans la classe Employee, comme dans l'approche de base. [1]

L'instanciation automatique pose quelques problèmes ; certains la détestent, d'autres la défendent. 2 Pour limiter l'auto-instauration à un seul proto-objet, vous pouvez ajouter une balise MakeNew à la classe, comme je l'ai fait avec l'approche de l'instance globale, ou modifiez-la légèrement comme suit Clone :

Dim protoEmployee as new Employee 'with "new", if you like

'Add some new employees to a collection
Dim someNames() as Variant, someAges() as Variant
Dim someEmployees as Collection
someNames = array("Cam", "Bob", "Jan", "Ace")
someAges = array(23, 45, 30, 38)
set someEmployees = new Collection

for i = 0 to 3
    someEmployees.Add protoEmployee.Clone(someNames(i), someAges(i))
next

Ici, le Clone peut être configurée avec des paramètres facultatifs Function Clone(optional employeeName, optional employeeAge) et utiliser les propriétés de l'objet appelant si aucune n'est fournie.

Même sans auto-instantation, une MakeNew o Clone au sein de la classe elle-même permet de créer de nouveaux objets en une seule ligne, une fois que vous avez créé le proto-objet. Vous pourriez utiliser l'auto-instauration pour un objet de fabrique générale de la même manière, pour économiser une ligne, ou pas.

Enfin, vous pourriez vouloir une classe parent. Une classe parent pourrait avoir des méthodes pour créer de nouveaux enfants avec des paramètres (par exemple, avec Employees comme collection personnalisée, set newEmployee = Employees.AddNew(Tom, 38) ). Pour de nombreux objets dans Excel, c'est la norme : vous ne pouvez pas créer une feuille de calcul ou un classeur si ce n'est à partir de sa collection parente.

[1] Un autre ajustement concerne la question de savoir si le <code>Construct</code> est un Sub ou une Fonction. Si <code>Construct</code> est appelé par un objet pour remplir ses propres propriétés, il peut être un Sub sans valeur de retour. Cependant, si <code>Construct</code> renvoie à <code>Me</code> après avoir rempli les paramètres, alors les méthodes/fonctions Factory dans les 2 premières réponses pourraient laisser les paramètres à <code>Construct</code> . Par exemple, l'utilisation d'une classe d'usine avec cet ajustement pourrait aller : <code>Set Sue = Factory.NewEmployee.Construct("Sue", "50")</code> donde <code>NewEmployee</code> est une méthode de <code>Factory</code> qui renvoie un nouvel employé vierge, mais <code>Construct</code> est une méthode de <code>Employee</code> qui assigne les paramètres en interne et retourne <code>Me</code> .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X