patternMinor
Immutable Object class in VBA – Creatable only through constructor and not via “New” keyword
Viewed 0 times
newthroughkeywordcreatableimmutableconstructorviavbaandobject
Problem
Goals for the class
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
-
Creating a Factory class for each class Provides more encapsulation, less dependancy and narrower responsibility, but again does not prevent use of
-
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
Proposed Solution
Implementation
IMaker.cls - Interface
```
Option Explicit
Pub
- Create Immutable objects – i.e. only Getters – no Setters
- Object creation only possible through a constructor, not via
Newkeyword, 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 usingClassName.Make, or on another instance of the class usingObjectName.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
Newkeyword 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:
As opposed to:
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
But you have them the standard way, like this:
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
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
Error Handling
I like what I'm seeing: private procedures dedicated to raising specific runtime errors. The
However the calling code has no easy way of telling
That way you can raise them without resorting to magic numbers:
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:
..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
Immutability
In VB6 you would set the
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
If having a "framework/toolbox add-in" project is an option (an immutable
The
And then any VBA project that uses this type will never know about a
You could also force the client code to work with an interface for the type - say
```
Option Explicit
Public Property Get X() As Double
End Property
Public Property
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 SubAs opposed to:
Public Sub DoSomething()
'instructions
End SubSeems 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 IfBut you have them the standard way, like this:
If condition Then
'instructions
End IfYour 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 EnumThat 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 SubNote 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 failsImmutability
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 PropertyAnd 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 SubPublic Sub DoSomething()
'instructions
End SubIf condition Then
'instructions
End IfIf condition Then
'instructions
End IfPublic Enum PointFactoryError
ERR_ClassIsNotCreatable = VBA.vbObjectError + 513
ERR_InstanceStateNotAvailable 'next value has to be VBA.vbObjectError + 514
End EnumContext
StackExchange Code Review Q#157425, answer score: 9
Revisions (0)
No revisions yet.