2013-03-05 4 views
49

Come si possono costruire oggetti che passano argomenti direttamente alle proprie classi?Passare argomenti a Constructor in VBA

Qualcosa di simile a questo:

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

Non essendo in grado di fare questo è molto fastidioso, e si finisce con soluzioni sporchi di lavorare in questo giro.

+1

http://codereview.stackexchange.com/questions/67825/parameterised-constructors –

risposta

84

Ecco un piccolo trucco che sto usando ultimamente e porta buoni risultati. Mi piacerebbe condividere con chi deve combattere spesso con VBA.

1.- Implementare una subroutine di iniziazione pubblica in ciascuna delle classi personalizzate. Lo chiamo InitiateProperties in tutte le mie classi. Questo metodo deve accettare gli argomenti che vorresti inviare al costruttore.

2.- Creare un modulo denominato factory e creare una funzione pubblica con la parola "Create" più lo stesso nome della classe e gli stessi argomenti in entrata richiesti dal costruttore. Questa funzione deve istanziare la tua classe e chiamare la subroutine di iniziazione spiegata al punto (1), passando gli argomenti ricevuti. Finalmente restituito il metodo istanziato e avviato.

Esempio:

Diciamo che abbiamo il dipendente classe personalizzata. Come l'esempio precedente, è necessario creare un'istanza con nome ed età.

Questo è il metodo InitiateProperties. m_name e m_age sono le nostre proprietà private da impostare.

Public Sub InitiateProperties(name as String, age as Integer) 

    m_name = name 
    m_age = age 

End Sub 

E ora nel modulo di fabbrica:

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 

E infine quando si desidera creare un'istanza di un dipendente

Dim this_employee as Employee 
Set this_employee = factory.CreateEmployee(name:="Johnny", age:=89) 

Particolarmente utile quando si dispone di diverse classi. Basta inserire una funzione per ogni modulo in fabbrica e istanziare solo chiamando factory.CreateClassA (argomenti), factory.CreateClassB (other_arguments), ecc

EDIT

Come stenci sottolineato, si può fare la stessa cosa con una sintassi del terser evitando di creare una variabile locale nelle funzioni del costruttore. Ad esempio la funzione CreateEmployee potrebbe essere scritta in questo modo:

Public Function CreateEmployee(name as String, age as Integer) as Employee 

    Set CreateEmployee = new Employee 
    CreateEmployee.InitiateProperties name:=name, age:=age 

End Function 

Quale è più bello.

+1

Bella soluzione! Anche se probabilmente lo rinominerei 'factory.CreateEmployee' per ridurre l'ambiguità ... –

+0

Sì hai ragione, sarebbe meno ambiguo. Anche se mi sono abituato a leggere "fabbrica". come se fosse "nuovo". Lo cambierò. – bgusach

+1

Qual è il vantaggio di un modulo factory su, ad esempio, un metodo Construct in ogni classe. Quindi chiameresti 'Set employee_obj = New Employee' quindi' employee_obj.Construct "Johnny", 89' e le cose della costruzione accadono all'interno della classe. Solo curioso. –

23

I utilizzare uno Factory modulo che contiene uno (o più) costruttore per classe che chiama il Init membro di ogni classe.

Per esempio una classe Point:

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

Una 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 

E un modulo 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 aspetto di questo approccio è che rende facile utilizzare le funzioni di fabbrica all'interno di espressioni. Ad esempio è possibile fare qualcosa di simile:

D = Distance(NewPoint(10, 10), NewPoint(20, 20) 

o:

D = NewPoint(10, 10).Distance(NewPoint(20, 20)) 

E 'pulito: la fabbrica fa molto poco e lo fa in modo coerente in tutti gli oggetti, solo la creazione e uno Init chiamata su ogni creatore.

Ed è abbastanza orientato agli oggetti: le funzioni Init sono definite all'interno degli oggetti.

EDIT

Ho dimenticato di aggiungere che questo mi permette di creare metodi statici. Per esempio io posso fare qualcosa di simile (dopo aver effettuato i parametri facoltativi):

NewLine.DeleteAllLinesShorterThan 10 

Purtroppo una nuova istanza l'oggetto viene creato ogni volta, in modo che qualsiasi variabile statica verranno persi dopo l'esecuzione. La raccolta di linee e qualsiasi altra variabile statica utilizzata in questo metodo pseudo-statico deve essere definita in un modulo.

+1

È più pulito della risposta selezionata. –

+0

È passato molto tempo dall'ultima volta che ho giocato con VBA, ma ... ** 1 **: come si ottengono oggetti costruiti dalle subroutine di 'Factory'? la definizione di "Sotto" non comporta alcun valore di ritorno. ** 2 **: anche con il punto che mi manca, il tuo 'Factory' fa praticamente la stessa cosa del mio: crea un oggetto (lo faccio in due passaggi, la tua sintassi è chiaramente più corta), chiama un' Init'/'InitiateProperties' metodo, e nel mio caso, restituire esplicitamente. – bgusach

+1

@ ikaros45 Dovevano essere 'Function', non' Sub', ho modificato il post, grazie. Sì, è uguale al tuo, è solo organizzato in un modo più facile da gestire (secondo me) poiché il numero di classi e il numero di "costruttori" per ogni classe cresce. – stenci

-1

Un altro approccio

Dire si crea una classe clsBitcoinPublicKey

Nel modulo di classe creare una subroutine COMPLEMENTARI, che si comporta come si vorrebbe il costruttore vero e proprio comportarsi. Sotto ho chiamato 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(...) 

L'unica pena è la chiamata in più, ma il vantaggio è che si può tenere tutto nel modulo di classe, e il debugging diventa più facile.

+1

A meno che non trascuri qualcosa, è proprio come chiamare manualmente "InitiateProperties" ogni volta che istanziate qualsiasi oggetto, che è ciò che volevo evitare in primo luogo. – bgusach

3

Quando esporti un modulo di classe e apri il file in Blocco note, noterai, in alto, una serie di attributi nascosti (il VBE non li visualizza e non espone la funzionalità per ottimizzare la maggior parte di entrambi). Uno di loro è VB_PredeclaredId:

Attribute VB_PredeclaredId = False 

Set a True, salvare e reimportare il modulo nel progetto VBA.

In alternativa, se si sta utilizzando Rubberduck, si può semplicemente indicare un'annotazione speciale nella parte superiore del modulo:

'@PredeclaredId 
Option Explicit 

Rubberduck emette un risultato di controllo dicendo che gli attributi del modulo non sono sincronizzati con il modulo annotazioni e con un clic è possibile impostare l'attributo VB_PredeclaredId senza uscire dal VBE (nota: al momento della stesura questa è ancora una funzionalità sperimentale).

Le classi con un PredeclaredId hanno una "istanza globale" che si ottiene gratuitamente - esattamente come i moduli UserForm (esportare un modulo utente, vedrete che il suo attributo predeclaredId è impostato su true).

Un sacco di persone utilizzano semplicemente l'istanza predeclared per memorizzare lo stato. È sbagliato: è come memorizzare lo stato dell'istanza in una classe statica!

Invece, si leva l'istanza predefinita per implementare il metodo factory:

[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 

Con questo, si può fare questo:

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

Employee.Create sta lavorando dall'istanza predefinita , ovvero è considerato un membro del tipo e invocato solo dall'istanza predefinita.

Il problema è che questo è anche perfettamente legale:

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

E questo fa schifo, perché ora si dispone di un'API di confusione. È possibile utilizzare le annotazioni '@Description/VB_Description per documentare l'utilizzo, ma senza Rubberduck non c'è nulla nell'editor che mostri tali informazioni nei siti di chiamata.

Inoltre, i Property Let membri sono accessibili, quindi l'istanza Employee è mutabile:

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

Il trucco è quello di rendere la vostra classe attuare un un'interfaccia che espone solo ciò che deve essere esposto:

[IEmployee classe]

Option Explicit 

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

E ora fate EmployeeimplementareIEmployee - classe finale potrebbe essere simile a questo:

[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 

Avviso il metodo Create ora restituisce l'interfaccia, e l'interfaccia non espone i membri Property Let?Ora il codice chiamante può assomigliare a questo:

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

E dal momento che il codice del client è scritto contro l'interfaccia, gli unici membri empl espone sono i membri definiti dall'interfaccia IEmployee, il che significa che non vede il metodo Create , né il getter Self, né alcuno dei mutanti Property Let: così invece di lavorare con la classe "calcestruzzo" Employee, il resto del codice può funzionare con l'interfaccia "astratta" IEmployee e godere di un oggetto polimorfico immutabile.