Thursday, 6 September 2018

VBA - Inheritance ... or as close as you will get ... better in fact.

So far, this blog has not opined on VBA inheritance or more accurately the simulation of inheritance; time to rectify that here. We could use composition along with a standard naming convention to mimic inheritance as found in languages such as C++ and C#. But we can use the default member trick to make the syntax even tighter.

C# and C++ Inheritance and the Fragile Base Class Problem

Before people complain that follows is not like the true inheritance found in C# and C++ I would say that what follows has some superior benefits. C++ and C# inheritance breaks encapsulation because derived classes can gain access to the base class's members. Also, once derived classes are written it becomes difficult to re-engineer the base class without breaking the derived classes, this is known as the fragile base class problem. Indeed, languages such as C# have to invent keywords such sealed to prevent derived classes inspecting a base class's private variables.

VBA can use Composition and the Default Member Trick to simulate Inheritance

What follows is VBA composition dressed up as inheritance by using the default member trick to tighten the syntax. So it has all the benefits of composition over inheritance

Code Listings

In the following listings, to effect Class1 and Class2 as intended it is not sufficient to cut and paste into VBA environment, it is also required to export to disk, load into editor, edit the file, save and then re-import; details are given in the code.

Class1 Listing

Option Explicit

Private moBase As Class2

'* To do the default member trick
'* 1) Export this module to disk;
'* 2) load into text editor;
'* 3) uncomment line with text Attribute Item.VB_UserMemId = 0 ;
'* 4) save the file back to disk
'* 5) remove or rename original file from VBA project to make room
'* 6) Re-import saved file


Private Sub Class_Initialize()
    Set moBase = New Class2
End Sub

Public Function Base() As Class2
    'Attribute Item.VB_UserMemId = 0
    Set Base = moBase
End Function

Public Function Foo() As String
    Foo = "Class1.Foo:" & 23
End Function

Public Function Common() As String
    Common = "Class1.Common"
End Function

Class2 Listing

Option Explicit

Private moBase As Class3

'* To do the default member trick
'* 1) Export this module to disk;
'* 2) load into text editor;
'* 3) uncomment line with text Attribute Item.VB_UserMemId = 0 ;
'* 4) save the file back to disk
'* 5) remove or rename original file from VBA project to make room
'* 6) Re-import saved file

Private Sub Class_Initialize()
    Set moBase = New Class3
End Sub

Public Function Base() As Class3
    'Attribute Item.VB_UserMemId = 0
    Set Base = moBase
End Function


Public Function Bar() As String
    Bar = "Class2.Bar:" & 42
End Function

Public Function Common() As String
    Common = "Class2.Common"
End Function

Class3 Listing

Option Explicit

Public Function Baz() As String
    Baz = "Class3.Baz:" & -5
End Function

Public Function Common() As String
    Common = "Class3.Common"
End Function

Test Module

So hopefully you have imported the above classes and edited Class1 and Class2 necessarily to effect the default member trick. So now one can see how this works using the test code below. One can access a class's base class simply by adding .Base qualifier but we can tighten the syntax to just a pair of round brackets using the default member trick.

Option Explicit

Sub Test()
    Dim oClass1 As Class1
    Set oClass1 = New Class1

    '* without using default member trick
    '* access common method method
    Debug.Print oClass1.Common              '* prints Class1.Common
    Debug.Print oClass1.Base.Common         '* prints Class2.Common
    Debug.Print oClass1.Base.Base.Common    '* prints Class3.Common

    '* using default member trick (Attribute Item.VB_UserMemId = 0)
    '* access common method method
    Debug.Print oClass1.Common      '* prints Class1.Common
    Debug.Print oClass1().Common    '* prints Class2.Common
    Debug.Print oClass1()().Common  '* prints Class3.Common

    '* access a base class's unique method (using default member trick)
    Debug.Print oClass1.Foo      '* prints Class1.Foo:23
    Debug.Print oClass1().Bar    '* prints Class2.Bar:42
    Debug.Print oClass1()().Baz  '* prints Class3.Baz:-5

End Sub

No need for Virtual or Overides keywords

Of note is the test code above that calls the Common method. So you see how without the virtual and overrides keywords found in other languages we can in fact easily determine which implementation of Common in the 'inheritance' chain to call by changing the number of bracket pairs.

Final Thoughts

I felt the need to post this because whilst investigating COM Type libraries, I thought I had found a secret way to aggregate a VBA class. (Aggregation is COM's reuse feature.) That turned our to be a mirage (details to follow in a separate post, maybe). I wanted somewhere on this blog to demonstrate how VBA developers can use something very conceptually close to inheritance in their designs.

1 comment:

  1. Must replace Item in Attribute Item.VB_UserMemId = 0, with Base.

    ReplyDelete