Alexander Platonov
Alexander Platonov

Reputation: 87

VBA inheritance

I heard a lot of that VBA is lacking inheritance. I did a little workaround and now it seems to me that it is exactly what inheritance does. I'm far from pro =) and may be missing something. So I would really appreciate your thoughts about possible downsides.

I was very surprised when found that you still can make a full realization of a function in an interface class (not only a signature), which led me to the following below. I saw that some people did similar with help of composition, but they used only a signature in the interface.

IBird - class

Public Sub SayFromInterface()
    Debug.Print "Tweet from IBird"
End Sub

Public Sub SayFromInstance()
End Sub

Crow - class

Implements IBird

Private pBird As IBird

Private Sub Class_Initialize()
    Set pBird = New IBird
End Sub

'let you use already implemented code from "abstract class", to avoid
'duplicating your code, which is the main point of inheritance
'in my opinion
Public Sub IBird_SayFromInterface()
    pBird.SayFromInterface
End Sub

'you can override the IBird function (common use)
Public Sub IBird_SayFromInstance()
    Debug.Print "Tweet from Crow"
End Sub

Test - module

Sub testIBird()

 Dim Bird As IBird

 Set Bird = New Crow

 Bird.SayFromInterface
 Bird.SayFromInstance
 Debug.Print TypeName(Bird)
 Debug.Print TypeOf Bird Is IBird

End Sub

Output:

Tweet from IBird
Tweet from Crow
Crow
True

Upvotes: 2

Views: 830

Answers (1)

Mathieu Guindon
Mathieu Guindon

Reputation: 71167

That's composition, not inheritance - and yes, with composition you can essentially emulate inheritance. And if the class implements the interface of the encapsulated object, then things start looking like some kind of a decorator pattern.

Except you wouldn't be having any implementation code in IBird. An interface should be purely abstract. Making a New instance of what's supposed to be an interface, makes the class no longer be an interface: now it's just another class exposing a default interface that any other class can implement, and the I prefix becomes rather confusing:

Set pBird = New IBird

It's rather weird that the client code now needs to wonder whether they want that bird to chirp FromInstance or FromInterface - these are very "meta" identifiers that make things not work like inheritance.

If we had a Crow : Bird where Bird had this implementation for IBird.Chirp:

public virtual string Chirp() => "Chirp!";

...and then Crow had this:

public override string Chirp() => "Craaaw!";

Then which method is invoked depends on what the runtime type is - this should feel pretty obvious:

IBird bird1 = new Bird();
bird1.Chirp(); // "Chirp!"

IBird bird2 = new Crow();
bird2.Chirp(); // "Craaaw!"

However picture a method that receives an IBird parameter:

public void DoSomething(IBird bird)
{
    Debug.Print(bird.Chirp());
}

If bird is a Bird, it prints "Chirp!"; if bird is a Crow, it prints "Craaaw!": the method that gets to run, is the most derived override, which isn't necessarily defined on the most derived type.

Inheritance would allow a GiantCrow : Crow to inherit the Chirp method from Crow and not necessarily override it. And that is what you can't simulate with VBA classes: you are forced to write the equivalent of...

public override string Chirp() => base.Chirp();

...which is technically redundant, and gets very repetitive if you have to do it every time just to get the "base" members visible on your default interface.

Instead of inheriting base members, we actually wrap calls to the encapsulated object. The decorator pattern does exactly that, and gives you a non-intrusive way of extending a VBA class or interface.

A decorator implements the interface it's extending, and encapsulates a private instance of that type. So basically with a decorator the "crow inheritance hierarchy" setup looks like this:

Dim bird As IBird
Set bird = Crow.Create(New BaseBird)

A perhaps more suitable decorator pattern example might be:

Dim repository As IRepository
Set repository = LoggingRepository.Create(BirdRepository.Create(connectionString), New DebugLogger)

Where a BirdRepository is responsible for abstracting the database operations relating to some Birds table (BirdRepository implements IRepository), and where LoggingRepository is a decorator that also implements IRepository, but also wraps an IRepository instance (in this case a BirdRepository) to add its own functionality - something that might look like this:

'@PredeclaredId
Implements IRepository
Private loggerInternal As ILogger
Private wrappedInternal As IRepository

Public Function Create(ByVal internal As IRepository, ByVal logger As ILogger) As IRepository
    Dim result As LoggingRepository
    Set result.Wrapped = internal
    Set result.Log = logger
    Set Create = result
End Function 

Public Property Get Wrapped() As IRepository
    Set Wrapped = wrappedInternal
End Property

Public Property Set Wrapped(ByVal value As IRepository)
    If Not wrappedInternal Is Nothing Then Err.Raise 5, TypeName(Me), "Instance is already initialized."
    Set wrappedInternal = value
End Property

Public Property Get Log() As ILogger
    Set Log = loggerInternal
End Property

Public Property Set Log(ByVal value As ILogger)
    If Not loggerInternal Is Nothing Then Err.Raise 5, TypeName(Me), "Instance is already initialized."
    Set loggerInternal = value
End Property

Private Function IRepository_SelectAll() As Object
    Log.Info "Starting IRepository.SelectAll"
    Dim t As Double
    t = Timer

    Set IRepository_SelectAll = wrappedInternal.SelectAll

    Log.Info "IRepository.SelectAll completed in " & Timer - t & " seconds."
End Function

Private Sub IRepository_Delete(ByVal id As Long)
    Log.Info "Starting IRepository.Delete(" & id & ")"
    Dim t As Double
    t = Timer

    wrappedInternal.Delete id

    Log.Info "IRepository.Delete completed in " & Timer - t & " seconds."
End Sub

Private Sub IRepository_Save(ByVal entity As Object)
    '...
    wrappedInternal.Save entity
    '...
End Sub

'...

A method that is given a IRepository object cannot (and absolutely should not) know whether it's given a plain BirdRepository, a LoggingRepository wrapping a BirdRepository, or a FakeRepository that encapsulates a Collection instead of access to a database table - and this polymorphism is the entire point.

It's one way to extend a type without using inheritance, that VBA can absolutely leverage without bastardizing the pattern too much. But it's not inheritance.

Upvotes: 6

Related Questions