HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Immutable Object class in VBA – Creatable only through constructor and not via “New” keyword

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
newthroughkeywordcreatableimmutableconstructorviavbaandobject

Problem

Goals for the class

  • Create Immutable objects – i.e. only Getters – no Setters



  • Object creation only possible through a constructor, not via New keyword, to ensure that no objects are created without a valid state.



  • Keep the constructor method in the same code module as the class itself.



Other common solutions for VBA constructors

-
Creating a global Factory class/module that provides constructors for all creatable objects, as suggested in this post. This can be arduous to maintain, creates a dependency between the modules and arguably violates encapsulation/single responsibility principal. Does not prevent use of New keyword to create objects.

-
Creating a Factory class for each class Provides more encapsulation, less dependancy and narrower responsibility, but again does not prevent use of New keyword and soon results in a proliferation of types.

-
Providing a constructor in the class itself and make it available through a predeclared instance using the VB_PredeclaredId attribute, as discussed in this post. Better, but still allows use of New keyword, and does not prevent access to the (potentially invalid) state of the predeclared instance.

Proposed Solution

  • Use the predeclared instance of a class as the “Factory Instance". Only this predeclared instance may create other instances of the class.



  • Provide a constructor method (I use the name Make) in the class module itself. It can be called on the Factory Instance using ClassName.Make, or on another instance of the class using ObjectName.Make. Other instances delegate creation to the Factory Instance and return a new object – it doesn’t alter their own state.



  • Each time a new instance is initialised it checks if it is being made by the Factory Instance, otherwise throws a runtime error – i.e. use of New keyword is not allowed.



  • An attempt to access the state of the Factory Instance returns a runtime error.



Implementation


IMaker.cls - Interface

```
Option Explicit
Pub

Solution

Indentation

The non-standard end-of-block token indentation is off-putting; this is the first time I see VB code that doesn't align the start and end token columns of code blocks:

Public Sub DoSomething()
    'instructions
    End Sub


As opposed to:

Public Sub DoSomething()
    'instructions
End Sub


Seems you go out of your way to fight the IDE to consistently apply that style - I appreciate the consistency, but I find it awkward when If...End If blocks don't seem to follow the same rule - I would expect this:

If condition Then
    'instructions
    End If


But you have them the standard way, like this:

If condition Then
    'instructions
End If


Your indenting style is your own preference, sure, but there's also commonly established conventions. Smart Indenter is an automatic indentation add-in that has been around for almost two decades (ported to .net and supporting x64 hosts through Rubberduck, an open-source project I'm working on - kudos to @Comintern for the work on the indenter); it offers a TON of indenting options, and none of them support that style - I'd take it as a sign.

Readability

The state handling and instance checks work, but it takes a very attentioned reader to follow; the back-and-forth between the default instance and the new object being created, the assignment of Me pointers, the IsMaking flag... the result is a rather convoluted execution path.

It works, but it's hard to follow.

IMaker

The interface is superfluous; while it's nice that you're formalizing the "I'm making something" state, the only calls to IsMaking are, as a comment explains, made from the default instance of the very same class that implements the interface - because no code is ever working off an IMaker instance (and/or accessing that instance from its IMaker interface), the interface is ultimately useless.

Error Handling

I like what I'm seeing: private procedures dedicated to raising specific runtime errors. The CLASS_NAME constant could be easily eliminated with a call to TypeName(Me) though.

However the calling code has no easy way of telling VBA.vbObjectError + 513 from VBA.vbObjectError + 514 without hard-coding these magic values; the class could expose a public enum for this:

Public Enum PointFactoryError
    ERR_ClassIsNotCreatable = VBA.vbObjectError + 513
    ERR_InstanceStateNotAvailable 'next value has to be VBA.vbObjectError + 514
End Enum


That way you can raise them without resorting to magic numbers:

Private Sub ThrowError_AttemptToCreateInstanceOutsideOfConstructor()
    Err.Raise ERR_ClassIsNotCreatable, TypeName(Me), "Cannot create instance of " & TypeName(Me) & " via New"
End Sub


Note that this message is very similar to the compile-time error Invalid use of 'New' keyword; I'd probably word the error message in a similar fashion.

And now callers can handle that error, also without resorting to magic numbers:

ErrHandler:
    If Err.Number = ERR_ClassIsNotCreatable Then
        '...
    End If


..which feels pretty weird, given that run-time error should really be a compile-time error, which means that error would only ever be raised in the case of a bug introduced by the programmer.

Perhaps Debug.Assert calls would be a better alternative then:

Debug.Assert Not Me Is Point 'breaks on-the-spot if assertion fails


Immutability

In VB6 you would set the VB_Creatable attribute to False and you had a class that you couldn't New up, but while this attribute appears in an exported class module, it has no effect whatsoever in VBA.

I like how your class is aware of its default instance, and uses this knowledge to forbid access to that instance's state. It's a bit combersome though, but if you're in a VBA project without other VBA project references, it's pretty much the only way to go.

VBA classes can only ever be New'd up within the project they're in. This means you get the Attribute VB_Creatable = False behavior for free, if you put such immutable types in their own "framework" VBA project.

If having a "framework/toolbox add-in" project is an option (an immutable Point type seems a pretty useful type to have in one's toolbox), then there's another, much simpler solution: access modifiers.

The Friend modifier means a member can only be accessed from within the project it's in. This means you can very well have this:

Public Property Get X() As Double
    X = X_
End Property

Friend Property Let X(ByVal value As Double)
    X_ = value
End Property


And then any VBA project that uses this type will never know about a Property Let member. Combined with the fact that the class can't be New'd up because it's declared in another referenced project, the result is effectively an immutable instance.

You could also force the client code to work with an interface for the type - say IPoint:

```
Option Explicit

Public Property Get X() As Double
End Property

Public Property

Code Snippets

Public Sub DoSomething()
    'instructions
    End Sub
Public Sub DoSomething()
    'instructions
End Sub
If condition Then
    'instructions
    End If
If condition Then
    'instructions
End If
Public Enum PointFactoryError
    ERR_ClassIsNotCreatable = VBA.vbObjectError + 513
    ERR_InstanceStateNotAvailable 'next value has to be VBA.vbObjectError + 514
End Enum

Context

StackExchange Code Review Q#157425, answer score: 9

Revisions (0)

No revisions yet.