Skip to main content
Code Review

Return to Revisions

2 of 7
Added Error Throwing Code
Mark.R
  • 231
  • 2
  • 7

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

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
Public Property Get IsMaking() As Boolean
 End Property

Provides an interface for objects to query the Factory Instance to check if their creation was requested. Put in a separate interface so that it does not show up in the public members of the class. Same interface can be used by all classes that use this pattern.

Point.cls - Edited in Notepad so that Attribute VB_PredeclaredId = True

Example class with two properties: X and Y. Verbose comments added for illustrative purposes

Option Explicit
Private Const CLASS_NAME As String = "Point"
Private Maker As IMaker
Private IsMaking As Boolean
Private X_ As Double
Private Y_ As Double
Implements IMaker
Private Sub Class_Initialize()
 ' Check Instance isn't the Factory Instance (which can be created without checks)
 If Not Me Is Point Then
 ' Create a reference to the Factory Instance, cast as type IMaker
 Set Maker = Point
 ' Throw Runtime error if creation wasn't requested by Factory Instance
 If Not Maker.IsMaking Then ThrowError_AttemptToCreateInstanceOutsideOfConstructor
 End If
 End Sub
Public Function Make(ByVal X As Double, ByVal Y As Double) As Point
 If Me Is Point Then ' Object is Factory Instance
 ' Allow new instances to be created
 IsMaking = True
 ' Create a new instance
 With New Point
 ' Pass parameters to its constructor and return new object
 Set Make = .Make(X, Y)
 End With
 ' Disallow new instances to be created
 IsMaking = False
 ElseIf Maker.IsMaking Then ' Object is new instance being created by the Factory Instance
 ' Set state and return self
 X_ = X
 Y_ = Y
 Set Make = Me
 Else
 ' Delegate creation of new object to Factory Instance and return new object
 Set Make = Point.Make(X, Y)
 End If
 End Function
Public Property Get X() As Double
 ' Disallow access to state of Factory Instance
 If Me Is Point Then ThrowError_AttemptToAccessPredeclaredInstance
 ' Return state
 X = X_
 End Property
 
Public Property Get Y() As Double
 ' Disallow access to state of Factory Instance
 If Me Is Point Then ThrowError_AttemptToAccessPredeclaredInstance
 ' Return state
 Y = Y_
 End Property
 
Private Property Get IMaker_IsMaking() As Boolean
 ' Indicate whether new instances of class can be created or not - only ever set to True by Factory Instance
 IMaker_IsMaking = IsMaking
 End Property
Private Sub ThrowError_AttemptToCreateInstanceOutsideOfConstructor()
 Err.Raise VBA.vbObjectError + 513, CLASS_NAME, "Cannot create instance of " & CLASS_NAME & " via New"
 End Sub
Private Sub ThrowError_AttemptToAccessPredeclaredInstance()
 Err.Raise VBA.vbObjectError + 514, CLASS_NAME, "Cannot access state of predeclared instance of " & CLASS_NAME
 End Sub

Example Usage

Sub ExampleUsage()
 Dim Point1 As Point, Point2 As Point
 Set Point1 = Point.Make(1, 2) ' Creates new object via Factory Instance
 Set Point2 = Point1.Make(3, 4) ' Creates new object via existing object
 Set Point1 = Point1.Make(5, 6) ' Creates new object and assigns to Point1 - does not change state of previous object
 
 Set Point1 = New Point ' Throws Runtime Error
 
 Dim Point3 As New Point ' Does not throw Error - Instantiation only occurs on first use
 Debug.Print ObjPtr(Point3) ' Throws Runtime Error
 
 Debug.Print Point.X ' Throws Runtime Error - cannot access state of Predeclared instance
 
 Point1.X = 5 ' Throws Compiler Error - only GET methods provided
 End Sub

Drawbacks

This is obviously quite a bit of "boilerplate" for a simple object, but VBA’s lack of features seems to require it. I would welcome suggestions to streamline it. Putting a check in each method that the object isn’t the Factory Instance obviously adds overhead and verbosity – can be omitted if you don’t care too much about code accessing the state of the predeclared instance.

Mark.R
  • 231
  • 2
  • 7
lang-vb

AltStyle によって変換されたページ (->オリジナル) /