How do I pass class names as arguments?

Viewed 394

I want to make a function that accepts a class exemplar/class name and returns a collection of a certain number of exemplars of said class, initalized and populated. So far I have come up with this solution:

Function getCollectionObj(className as string) As Collection

    Dim obj As Variant
    Dim result As Collection
    Dim objType As String
    Dim I 
    
    For I = 1 To 5
    
    'selecting the type of a class based on string name
    Select Case className
    Case "Cls1"
        Set obj = New Cls1
    Case "Cls2"
        Set obj = New Cls2
    Case "DocumentStart"
        Set obj = New Cls3    
    End Select
    'some code handling the exemplar of the object.

    result.add obj
    Next I
    Set getCollectionObj = result
End Function

The problem with this is I have to explicitly put every class into select case and that the code has to select and declare the cerain class every time, even though all object in the collection should be the exemplars of the same class. How do I improve it?

Edit: my testing code (gives "object required error")

Main module:

Private Function getCollectionObj(ByVal factory As IObjectFactory) As Collection
Dim I
    For I = 1 To 5
    result.Add factory.Create
    Next I
End Function

Sub test()
Dim var As Object
Set var = getCollectionObj(New Class1ObjectFactory)
End Sub

IObjectFactory class:

Option Explicit
Public Function Create() As Object

End Function

Cls1Factory class (the right dropdown shows "create" when the function is selected):

Option Explicit
Implements IObjectFactory

Private Function IObjectFactory_Create() As Object
     ' "Object required" error here
     Set IObjectFactory_Create = New Cls1
End Function

Cls1 class:

Option Explicit
Public I As String
2 Answers

Ideally, you don't (to answer the question as titled) - that way your code doesn't silently break when a class gets renamed, for example.

There's a better way, one that gives you an immediate compile-time error as soon as something breaks, and that works nicely with static code analysis and refactoring tools, such as Rubberduck - full disclosure, that's my website, and I manage the Rubberduck open-source project.

You could formalize the task of creating an instance of a class by defining an abstract factory interface. Add a new class module, call it IObjectFactory:

Option Explicit
Public Function Create() As Object
End Function

Now add a new class module, call it Cls1Factory and make it implement the abstract factory interface:

Option Explicit
Implements IObjectFactory

Private Function IObjectFactory_Create() As Object
    Set IObjectFactory_Create = New Cls1
End Function

Add a new class module, call it Cls2Factory and make it also implement the abstract factory interface:

Option Explicit
Implements IObjectFactory

Private Function IObjectFactory_Create() As Object
    Set IObjectFactory_Create = New Cls2
End Function

Add another implementation that can create Cls3 instances, call it Cls3Factory.

Then change your function's signature as follows:

Public Function getCollectionObj(ByVal factory As IObjectFactory) As Collection

Now the entire Select Case block becomes this single statement:

result.Add factory.Create

The calling code that used to do this:

Set things1 = getCollectionObj("Cls1")
Set things2 = getCollectionObj("Cls2")
Set things3 = getCollectionObj("Cls3")

Now needs to do this:

Set things1 = getCollectionObj(New Cls1Factory)
Set things2 = getCollectionObj(New Cls2Factory)
Set things3 = getCollectionObj(New Cls3Factory)

The function can now create a collection of 5 instances of any class you want, without modifying it at all: if you need to support a new class, simply implement a new factory for it, and then pass it as a parameter to the function.


RE: EDIT

Rubberduck's static code analysis can help you avoid a ton of traps, exactly like those in your edited snippet: the function return value isn't assigned, result variable is accessed before it's initialized, etc.:

OP's revision 2 code under ducky scrutiny

The "object required" error is signaled by its underlying causes, here in cascading order leading to this specific error:

  • 'Option Explicit' is not specified in 'Module1'.
  • Local variable 'result' is not declared.
  • Variable 'result' is used but not assigned.

Option Explicit not being specified has allowed the code to execute with result not being declared. Since it's not declared, it's allocated as a Variant/Empty until it's assigned... except it never is, and when a member call (.Add) is tentatively being made against that variant, that's when VBA screams at run-time and says "object required", because an object is required for that member call to be valid. But all VBA is seeing is a Variant/Empty pointer it just created on-the-spot.

The run-time error is raised after the .Create factory method call returns, but before the returned object is passed as a parameter to an accidentally late-bound .Add member call. Execution hasn't entered the .Add method, VBA hasn't successfully dereferenced a Collection object from the result variable; but because the debugger doesn't let us step through operators, when we click "DEBUG" we're taken back on the failing statement, and the entire result.Add factory.Create statement is highlighted (as opposed to just the result.Add call that's the actually-failing operation here).

Because there are two statements on the same line, the debugger is not quite in the exact state it should be in right now. If we hit F8, we're taken back into the factory method, and if we don't realize what's going on then it's easy to let the debugger lead us astray into investigating a completely sane execution path.

Separate the statements:

Dim o As Object
Set o = factory.Create '<~ no problem here!

result.Add o '<~ object required

Now the debugger enters break mode with the correct failing statement highlighted.

The working code, for completeness' sake:

Option Explicit

Public Sub test()
    Dim var As Collection
    Set var = getCollectionObj(New Class1ObjectFactory)
    Debug.Print var.Count
End Sub

Private Function getCollectionObj(ByVal factory As IObjectFactory) As Collection
    Dim result As Collection
    Set result = New Collection
    Dim I As Long
    For I = 1 To 5
        result.Add factory.Create
    Next I
    Set getCollectionObj = result
End Function

FWIW, if you already have a license or are willing to buy a license for vbWatchDog Error Handler, one of the perk is the ability to create objects from strings:

Set myClass1 = ErrEx.LiveCallstack.ProjectCreateClassInstance("Class1")

Introduced in version 3.8.6

Related