Search code examples
vbaoopexcelfactoryfactory-pattern

How Can I Initialize an object with help of properties of initializing object in VBA


I have two objects that need to interact with each other one is called Collateral the other is called Model. Model is an abstract Class is implemented by Model_A, Model_B, Model_AB. Each Collateral object has a collection of models as one of its properties. In order to initialize each Model I will need to use information from Collateral(and still another object lets call it User_Input), that information will vary with implementation of Model.

My question is it possible to use a constructor that will be aware of what object is creating it(in this case Model Constructor that knows what Collateral instantiated it)? If not I assume that someone will suggest for me to use abstract factory pattern, if so is so how would it look like(I'm afraid I'm still green when it comes to OOP)?

For Simplicity's sake assume following:

  • Collateral has properties A, B, C , Models_Collection
  • Collateral Calls procedure Run for Each of Models it created( has in Models_Collection)
  • Model has a public Sub called Run which is implemented in all classes bellow
  • Procedure Run Manipulates Collateral
  • Model_A requires property A to initialize
  • Model_B requires property B to initialize
  • Model_AB requires property A, B to initialize

Here is a Simplified Code of how I assume this should look like:

Collateral

Dim A, B, C as Variant
Dim Model_Collection as Collection
Sub New_Model( Model_Type as String)
    Model_Collection.Add(Model_Implementation)
End Sub
Sub Execute_Models()
    For Each Model in Model_Collection
        Model.Run(Me)
    Next Model
End Sub

Model

    Sub Run()
    End

Model_A

Implements Model
Sub Class_Initialize()
    'Some code that takes property A from Collateral that Created this object
Sub Run(Collateral as Collateral)
    'Some Code
End Sub

Model_B

Implements Model
Sub Class_Initialize()
    'Some code that takes property B from Collateral that Created this object
Sub Run(Collateral as Collateral)
    'Some Code
End Sub

Model_AB

Implements Model
Sub Class_Initialize()
    'Some code that takes property A, and B from Collateral that Created this object
Sub Run(Collateral as Collateral)
    'Some Code
End Sub

Solution

  • First, lets answer your question. How can you dynamically create instances of different class that all implement the same interface? As was pointed out, VBA doesn't have any constructors, so you're correct. A Factory Pattern is called for here.

    How I tend to go about this is define a public enum in the Interface class that keeps track of what classes have been implemented. Any time you implement a new one, you'll need to add it to your enum and Factory. It's a bit more maintenance then I like, but without proper reflection, there's not much we can do about that.

    So, the IModel interface:

    Public Enum EModel
        ModelA
        ModelB
        ModelC
    End Enum
    
    Public Sub Run
    End Sub
    

    Your models themselves remain unchanged. Then back in your Collateral implement your New_Model like this.

    private models as Collection
    
    Public Sub New_Model(ByVal type As EModel) As IModel
        dim model As IModel
        Select Case type
            Case EModel.ModelA: Set model = New ModelA
            Case EModel.ModelB: Set model = New ModelB
            Case EModel.ModelC: Set model = New ModelC
        End Select
    
        models.Add model
    End Sub
    

    Note that it's better to use the enum than a string as in your example so it gets compile time checked for errors instead of runtime. (This removes the chances of misspelling something.)


    If it was me implementing this, I would create an actual separate class ModelFactory. Then Collateral would call on the model factory to get what it needs. It makes a nice separation of concerns I think.

    An implementation would look something like this, based on your requirements.

     Public Function CreateModel(Optional A As Variant, Optional B As Variant, Optional C As Variant)
         If Not A Is Nothing Then
             If B Is Nothing Then
                 Set CreateModel = New ModelA
                 Exit Function
             Else
                 Set CreateModel = New ModelC
                 Exit Function
             End If
         End If
    
         If Not B Is Nothing Then
             Set CreateModel = New ModelB
             Exit Function
         End If
     End Function
    

    Note that this entirely does away with the enum and the need to specify the type. The factory knows what to create based on which arguments are available to it.

    Then your Collateral class simply calls on the factory and gives it whatever it has.

    Private A,B,C
    Private models As Collection
    Private factory As ModelFactory
    
    Private Sub Class_Initialize()
        Set factory = New ModelFactory
    End Sub
    
    Public Sub New_Model()
        models.Add factory.CreateModel(A,B,C)
    End Sub
    

    Now, I'm going to pre-emptively answer your next question, because I feel like you're on the verge of asking it already.

    How can I tell exactly what type of model I have?

    Well, for that you have a few options that are detailed a bit in this code review Q & A. It depends on your use case, but here they are.

    • TypeName(arg) - Returns the string name of the object. For example:

      Dim model As IModel
      Set model = New ModelA
      
      Debug.Print TypeName(model) '=> "ModelA"
      
    • TypeOf and Is - Checks the type of a variable a bit more strongly. Details are in the question I linked to, but here is an example.

      Dim model as IModel
      Set model = SomeFunctionThatReturnsAnIModel()
      
      If TypeOf model Is ModelA Then
          ' take some specific action for ModelA types
      Else If TypeOf model Is ModelB Then
          ' ModelB type specific action
      Else If ...