Goals for the class
- 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
Newkeyword 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
Newkeyword 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
Newkeyword, and does not prevent access to the (potentially invalid) state of the predeclared instance.
- 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
Newkeyword is not allowed.
- An attempt to access the state of the Factory Instance returns a runtime error.
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
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
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.