Note: this article was updated 2021-04-13 with screenshots from the latest v2.5.1.x pre-release build; the extract interface enhancements shown will green-release with v2.5.2.
We’ve seen how to leverage the default instance of a class module to define a stateless interface that’s perfect for a factory method. At the right abstraction level, most objects will not require more than just a few parameters. Often, parameters are related and can be abstracted/regrouped into their own object. Sometimes that makes things expressive enough. Other times, there’s just nothing we can do to work around the fact that we need to initialize a class with a dozen or more values.
The example code for this article can be found in our Examples repository.
A class with many properties
Such classes are actually pretty common; any entity object representing a database record would fit the bill. Let’s make a
User class. We’re using Rubberduck, so this will be quick!
We start with a public field for each property we want:
Option Explicit Public Id As String Public UserName As String Public FirstName As String Public LastName As String Public Email As String Public EmailVerified As Boolean Public TwoFactorEnabled As Boolean Public PhoneNumber As String Public PhoneNumberVerified As Boolean Public AvatarUrl As String
Now we hit Ctrl+` to trigger a parse, right-click any of the variables and select Encapsulate Field from the Refactor menu (or Ctrl+Shift+F if you haven’t tweaked the default hotkeys):
Check the wrap fields in private type box, then click the Select all button and hit OK.
Now the module looks like this, and all you had to do was to declare a bunch of public fields:
Option Explicit Private Type TUser Id As String UserName As String FirstName As String LastName As String Email As String EmailVerified As Boolean TwoFactorEnabled As Boolean PhoneNumber As String PhoneNumberVerified As Boolean AvatarUrl As String End Type Private this As TUser Public Property Get Id() As String Id = this.Id End Property Public Property Let Id(ByVal value As String) this.Id = value End Property Public Property Get UserName() As String UserName = this.UserName End Property Public Property Let UserName(ByVal value As String) this.UserName = value End Property Public Property Get FirstName() As String FirstName = this.FirstName End Property Public Property Let FirstName(ByVal value As String) this.FirstName = value End Property Public Property Get LastName() As String LastName = this.LastName End Property Public Property Let LastName(ByVal value As String) this.LastName = value End Property Public Property Get Email() As String Email = this.Email End Property Public Property Let Email(ByVal value As String) this.Email = value End Property Public Property Get EmailVerified() As Boolean EmailVerified = this.EmailVerified End Property Public Property Let EmailVerified(ByVal value As Boolean) this.EmailVerified = value End Property Public Property Get TwoFactorEnabled() As Boolean TwoFactorEnabled = this.TwoFactorEnabled End Property Public Property Let TwoFactorEnabled(ByVal value As Boolean) this.TwoFactorEnabled = value End Property Public Property Get PhoneNumber() As String PhoneNumber = this.PhoneNumber End Property Public Property Let PhoneNumber(ByVal value As String) this.PhoneNumber = value End Property Public Property Get PhoneNumberVerified() As Boolean PhoneNumberVerified = this.PhoneNumberVerified End Property Public Property Let PhoneNumberVerified(ByVal value As Boolean) this.PhoneNumberVerified = value End Property Public Property Get AvatarUrl() As String AvatarUrl = this.AvatarUrl End Property Public Property Let AvatarUrl(ByVal value As String) this.AvatarUrl = value End Property
I love this feature! Rubberduck has already re-parsed the module, so next we right-click anywhere in the module and select the Extract Interface refactoring, and check the box to select all
Property Get accessors (skipping
Having a read-only interface for client code that doesn’t need the
Property Let accessors makes an objectively cleaner API: assignments are recognized as invalid at compile time.
We get a read-only
IUser interface for our efforts (!), and now the
User class has an
Implements IUser instruction at the top, …and these new members at the bottom:
Private Property Get IUser_ThingId() As String IUser_ThingId = ThingId End Property Private Property Get IUser_UserName() As String IUser_UserName = UserName End Property Private Property Get IUser_FirstName() As String IUser_FirstName = FirstName End Property Private Property Get IUser_LastName() As String IUser_LastName = LastName End Property Private Property Get IUser_Email() As String IUser_Email = Email End Property Private Property Get IUser_EmailVerified() As Boolean IUser_EmailVerified = EmailVerified End Property Private Property Get IUser_TwoFactorEnabled() As Boolean IUser_TwoFactorEnabled = TwoFactorEnabled End Property Private Property Get IUser_PhoneNumber() As String IUser_PhoneNumber = PhoneNumber End Property Private Property Get IUser_PhoneNumberVerified() As Boolean IUser_PhoneNumberVerified = PhoneNumberVerified End Property Private Property Get IUser_AvatarUrl() As String IUser_AvatarUrl = AvatarUrl End Property
The scary part is that it feels as though if Extract Interface accounted for the presence of a Update: automagic implementation completed!
Private Type in a similar way Encapsulate Field does, then even the
TODO placeholder bits could be fully automated. Might be something to explore there…
Now we have our read-only interface worked out, if we go by previous posts’ teachings, , that is where we make our
User class have a predeclared instance, and expose a factory method that I’d typically name
'@Description "Creates and returns a new user instance with the specified property values." Public Function Create(ByVal Id As String, ByVal UserName As String, ...) As IUser '... End Function
Without Rubberduck, in order to have a predeclared instance of your class you would have to export+remove the class module, locate the exported .cls file, open it in Notepad++, edit the
VB_PredeclaredId attribute value to
True, save+close the file, then re-import it back into your VBA project.
With Rubberduck, there’s an annotation for that: simply add
'@PredeclaredId at the top of the class module, parse, and there will be a result for the AttributeValueOutOfSync inspection informing you that the class’
VB_PredeclaredId attribute value disagrees with the
@PredeclaredId annotation, and then you apply the quick-fix you want, and you just might have synchronized hidden attributes across the with a single click.
'@PredeclaredId Option Explicit
When it’s a factory method for a service class that takes in dependencies, 2-3 parameters is great, 5+ is suspicious. But here we’re taking in values, pure data – not some
IFileWriter or other abstraction. And we need quite a lot of them (here 10, but who knows how many that can be!), and that’s a problem, because this is very ugly:
Set identity = User.Create("01234", "Rubberduck", "firstname.lastname@example.org", False, ...)
Using named parameters can help:
Set identity = User.Create( _ Id:="01234", _ UserName:="Rubberduck", _ Email:="email@example.com", _ EmailVerified:=False, _ Phone:="555-555-5555", _ PhoneVerified:=False, _ ...)
But the resulting code still feels pretty loaded, and that’s with consistent line breaks. Problem is, that limits the number of factory method parameters to 20-ish (if we’re nice and stick to one per line), since that’s how many line continuations the compiler will handle for a single logical line of code.
Surely there’s a better way.
Building the Builder
I wrote about this pattern in OOP Design Patterns: The Builder, but in retrospect that article was really just a quick overview. Let’s explore the builder pattern.
I like to design objects from the point of view of the code that will be consuming them. In this case what we want to end up with, is something like this:
Set identity = UserBuilder.Create("01234", "Rubberduck") _ .WithEmail("firstname.lastname@example.org", Verified:=False) _ .WithPhone("555-555-5555", Verified:=False) _ .Build
This solves a few problems that the factory method doesn’t:
- Optional arguments become explicitly optional member calls; long argument lists are basically eliminated.
UserNameare required, i.e. a
Userobject would be invalid without these values; the builder’s own
Createfactory method can take these required values as arguments, and that way any
Userinstance that was built with a
UserBuilderis guaranteed to at least have these values.
- If we can provide a value for
EmailVerifiedbut not for
PhoneVerifiedbut not for
Phone, and neither are required… then with individual properties the best we can do is raise some validation error after the fact. With a
UserBuilder, we can have
WithPhonemethods that take a
VerifiedBoolean parameter along with the email/phone, and guarantee that if
EmailVerifiedis supplied, then
I like to start from abstractions, so let’s add a new class module – but don’t rename it just yet, otherwise Rubberduck will parse it right away. Instead, copy the
IUser interface into the new
Class1 module, select all, and Ctrl+H to replace “Property Get ” (with the trailing space) with “Function With” (without the trailing space). Still with the whole module selected, we replace “String” and “Boolean” with “IUserBuilder”. The result should look like this:
'@Interface Option Explicit Public Function WithId() As IUserBuilder End Function Public Function WithUserName() As IUserBuilder End Function Public Function WithFirstName() As IUserBuilder End Function Public Function WithLastName() As IUserBuilder End Function Public Function WithEmail() As IUserBuilder End Function Public Function WithEmailVerified() As IUserBuilder End Function Public Function WithTwoFactorEnabled() As IUserBuilder End Function Public Function WithPhoneNumber() As IUserBuilder End Function Public Function WithPhoneNumberVerified() As IUserBuilder End Function Public Function WithAvatarUrl() As IUserBuilder End Function
We’re missing a
Build method that returns the
IUser we’re building:
Public Function Build() As IUser End Function
Now we add the parameters and remove the members we don’t want, merge the related ones into single functions – this is where we define the shape of our builder API: if we want to make it hard to create a
User with a
LastName but without a
FirstName, or one with
PhoneNumberVerified set to
True but without a
PhoneNumber value… then with a well-crafted builder interface we can make it do exactly that.
Once we’re done, we can rename the class module to
IUserBuilder, and that should trigger a parse. The interface might look like this now:
'@Interface '@ModuleDescription("Incrementally builds a User instance.") Option Explicit '@Description("Returns the current object.") Public Function Build() As IUser End Function '@Description("Builds a user with a first and last name.") Public Function WithName(ByVal FirstName As String, ByVal LastName As String) As IUserBuilder End Function '@Description("Builds a user with an email address.") Public Function WithEmail(ByVal Email As String, Optional ByVal Verified As Boolean = False) As IUserBuilder End Function '@Description("Builds a user with SMS-based 2FA enabled.") Public Function WithTwoFactorAuthentication(ByVal PhoneNumber As String, Optional ByVal Verified As Boolean = False) As IUserBuilder End Function '@Description("Builds a user with an avatar at the specified URL.") Public Function WithAvatar(ByVal Url As String) As IUserBuilder End Function
Then we can add another class module, and type
Implements IUserBuilder under
Option Explicit, then hit Ctrl+` to parse. Unless you disabled the “check if code compiles before parsing” setting (it’s enabled by default), you should be seeing this warning:
Click Yes to parse anyway (normally we only want compilable code, but in this case we know what we’re doing, I promise), then right-click somewhere in the
Implements IUserBuilder statement, and select the Implement Interface refactoring:
The result is as follows, and makes a good starting point:
Option Explicit Implements IUserBuilder Private Function IUserBuilder_Build() As IUser Err.Raise 5 'TODO implement interface member End Function Private Function IUserBuilder_WithName(ByVal FirstName As String, ByVal LastName As String) As IUserBuilder Err.Raise 5 'TODO implement interface member End Function Private Function IUserBuilder_WithEmail(ByVal Email As String, Optional ByVal Verified As Boolean = False) As IUserBuilder Err.Raise 5 'TODO implement interface member End Function Private Function IUserBuilder_WithTwoFactorAuthentication(ByVal PhoneNumber As String, Optional ByVal Verified As Boolean = False) As IUserBuilder Err.Raise 5 'TODO implement interface member End Function Private Function IUserBuilder_WithAvatar(ByVal Url As String) As IUserBuilder Err.Raise 5 'TODO implement interface member End Function
We’re “building” an
IUser object. So we have a module-level
User object (we need the class’ default interface here, so that we can access the
Property Let members), and each
With method sets one property or more and then returns the current object (
Me). That last part is critical, it’s what makes the builder methods chainable. We’ll need a
Build method to return an encapsulated
IUser object. So the next step will be to add a
@PredeclaredId annotation and implement a
Create factory method that takes the required values and injects the
IUser object into the
IUserBuilder instance we’re returning; then we can remove the members for these required values, leaving only builder methods for the optional ones. We will also add a
value parameter of the correct type to each builder method, and make them all return the current object (
Me). Once the class module looks like this, we can rename it to
UserBuilder, and Rubberduck parses the code changes – note the
@PredeclaredId annotation (needs to be synchronized to set the hidden
VB_PredeclaredId attribute to
'@PredeclaredId '@ModuleDescription("Builds a User object.") Option Explicit Implements IUserBuilder Private internal As User '@Description("Creates a new UserBuilder instance.") Public Function Create(ByVal Id As String, ByVal UserName As String) As IUserBuilder Dim result As UserBuilder Set result = New UserBuilder '@Ignore UserMeaningfulName FIXME Dim obj As User Set obj = New User obj.Id = Id obj.UserName = UserName Set result.User = internal Set Create = result End Function '@Ignore WriteOnlyProperty '@Description("For property injection of the internal IUser object; only the Create method should be invoking this member.") Friend Property Set User(ByVal value As IUser) If Me Is UserBuilder Then Err.Raise 5, TypeName(Me), "Member call is illegal from default instance." If value Is Nothing Then Err.Raise 5, TypeName(Me), "'value' argument cannot be a null reference." Set internal = value End Property Private Function IUserBuilder_Build() As IUser If internal Is Nothing Then Err.Raise 91, TypeName(Me), "Builder initialization error: use UserBuilder.Create to create a UserBuilder." Set IUserBuilder_Build = internal End Function Private Function IUserBuilder_WithName(ByVal FirstName As String, ByVal LastName As String) As IUserBuilder internal.FirstName = FirstName internal.LastName = LastName Set IUserBuilder_WithName = Me End Function Private Function IUserBuilder_WithEmail(ByVal Email As String, Optional ByVal Verified As Boolean = False) As IUserBuilder internal.Email = Email internal.EmailVerified = Verified Set IUserBuilder_WithEmail = Me End Function Private Function IUserBuilder_WithTwoFactorAuthentication(ByVal PhoneNumber As String, Optional ByVal Verified As Boolean = False) As IUserBuilder internal.TwoFactorEnabled = True internal.PhoneNumber = PhoneNumber internal.PhoneNumberVerified = Verified Set IUserBuilder_WithTwoFactorAuthentication = Me End Function Private Function IUserBuilder_WithAvatar(ByVal Url As String) As IUserBuilder internal.AvatarUrl = Url Set IUserBuilder_WithAvatar = Me End Function
Now, when I said default instances and factory methods (here too) are some kind of fundamental building block, I mean we’re going to be building on top of that, starting with this builder pattern; the
Create method is intended to be invoked off the class’ default instance, like this:
Set builder = UserBuilder.Create(internalId, uniqueName)
The advantages are numerous, starting with the possibility to initialize the builder with everything it needs (all the required values), so that the client code can call
Build and consume a valid
User object right away.
Side note about this
FIXME comment – there’s more to it than it being a signpost for the reader/maintainer:
'@Ignore UserMeaningfulName FIXME Dim obj As User
By default only
NOTE markers are picked up, but you can easily configure Rubberduck to find any marker you like in comments, and then the ToDo Explorer lets you easily navigate them all:
Another noteworthy observation:
'@Ignore WriteOnlyProperty '@Description("For property injection of the internal IUser object; only the Create method should be invoking this member.") Friend Property Set User(ByVal value As IUser) If Me Is UserBuilder Then Err.Raise 5, TypeName(Me), "Member call is illegal from default instance." If value Is Nothing Then Err.Raise 5, TypeName(Me), "'value' argument cannot be a null reference." Set internal = value End Property
Me is always the current object, as in, an instance of this class module, presenting the default interface of this class module: the
If Me Is UserBuilder condition evaluates whether
Me is the object known as
UserBuilder – and right now there’s no such thing and the code doesn’t compile.
Synchronizing Attributes & Annotations
Rubberduck knows we mean that class to have a
VB_PredeclaredId attribute value of
True because of the
@PredeclaredId annotation, but it’s still just a comment at this point. Bring up the inspection results toolwindow, and find the results for the MissingAttribute inspection under Rubberduck Opportunities:
That didn’t fix the
VB_PredeclaredId attributes! Why?! The reason is that the attribute isn’t missing, only its value is out of sync. We’ll have to change this (pull requests welcome!), but for now you’ll find the AttributeValueOutOfSync inspection results under the Code Quality Issues group. If you group results by inspection, its miscategorization doesn’t matter though:
Adjust the attribute value accordingly (right-click the inspection result, or select “adjust attribute value(s)” from the “Fix” dropdown menu), and now your
UserBuilder is ready to use:
Dim identity As IUser Set identity = UserBuilder.Create(uniqueId, uniqueName) _ .WithName(first, last) _ .WithEmail(emailAddress) _ .Build
Set UserBuilder.User = New User '<~ runtime error, illegal from default instance Debug.Print UserBuilder.User.AvatarUrl '<~ compile error, invalid use of property Set builder = New UserBuilder Set identity = builder.Build '<~ runtime error 91, builder state was not initialized Set builder = New UserBuilder Set builder = builder.WithEmail(emailAddress) '<~ runtime error 91
Model classes with many properties are annoying to write, and annoying to initialize. Sometimes properties are required, other times properties are optional, others are only valid if another property has such or such value. This article has shown how effortlessly such classes can be created with Rubberduck, and how temporal coupling and other state issues can be solved using the builder creational pattern.
Using this pattern as a building block in the same toolbox as factory methods and other creational patterns previously discussed, we can now craft lovely fluent APIs that can chain optional member calls to build complex objects with many properties without needing to take a gazillion parameters anywhere.
17 thoughts on “Builder Walkthrough”
Thank you Mathieu, I haven’t understand that part how you turned multiple todos of extracted inteface
Err.Raise 5 ‘TODO implement interface member
IUser_Id = this.Id
Automatically with rubberduck or mannually?
LikeLiked by 1 person
That was done manually, but this was my first time actually laying out the steps like this, and that part really feels like Rubberduck already has all the information it needs to fill the blanks… the *implement interface* refactoring would need a few tweaks, but the result would be pretty cool!
Absolutely wonderful reading and explanation. This is very timely for me. I have finally gotten very comfortable with factory pattern; a short few months ago the factory pattern was anything but clear. This article takes things to the next level. As always it will take me some time to fully understand and get comfortable with implementing this next layer.
Thank you so much for the time and effort you put into these articles and, of course, Rubberduck. I don’t know how you have time for a day job and your family.
LikeLiked by 2 people
Hi, thanks for the article, clear and thorough as always! 2 quick things; First, shouldn’t there be a colon before FIXME to indicate it’s a comment? What if one of your TODO explorer identifiers clashed with a code inspection identifier? Also I wonder is there ever an argument for `builder.WithBlah()` being valid if say you wanted to set an (overridable?) default for different places in your code; a bit like subclassing the generic UserBuilder with an AdminUserBuilder which specifies an automatic 2 factor authentication phone number to your company’s security team, so all Admins get that as a minimum, or a GuestUserBuilder with a default avatar that is different to a RegisteredUserBuilder?
The fixme parsed fine, but you’re probably correct that a separator is cleaner – presumably that FIXME looks like an argument to the annotation in the AST. But the TODO markers could be whatever you like, doesn’t matter if they match an inspection name, a keyword, or whatever =)
As for the other question, …the problem is that the abstract builder interface would have to be the same regardless of the implementation, in adherence to LSP.
Builder isn’t a very commonly used pattern, TBH the one time I *really* needed a builder was for setting up unit testing in Rubberduck; the MockVbeBuilder allows us to configure a mock IDE to run our tests in, and the setup can get very intricate, so a builder makes complete sense: the pattern works well for building *complex objects*, but basically twists into a pretzel for simple ones =)
Thank you for your hard work on the Rubberduck add-in. I use it almost every day. It could be me but there appears to be a small error in the ‘Create’ Function of the UserBuilder class. it passes the ‘internal’ User instance to the User property.
Same here (with the “Thank You for the hard work […]” as well): Using example-codeline 29 in the UserBuilder-Class, i get the err.raise 5 – cannot be null reference. I assume, instead of “internal” it should be:
Set result.User = obj
At least, it seems to work then. “internal” is “nothing” at that point and the “obj”-object-variable despite giving it two values is not used anywhere else.
For rubberduck Version 126.96.36.19957 (german translation): It should be “Bibliothek” (not Biblo…).
Again thanks for sharing Your knowledge and all the related work.
LikeLiked by 2 people
Hello, first of all thanks a lot for the “New Vision” you provide regarding VBA programming, I’m using it for more a decade and to be honest I never read a so clear presentation of possible advantages of OOP applied to VBA applications, simply I was thinking not almost possible. Now I changed my idea !! Thank you.
Trying supporting your hard work on such amazing tool, I would provide my modest 1 cent contribution…
1) My development platform is based on Asus Z390-A – Intel I7 8700 3.20GHz – 16Gb RAM – Samsung 970 Pro NvMe 512Mb M2 disk. Software Win10 (64bit) – Office 365-MsAccess 2016 (32 bit) – MySQL.
I have an application counting about 44.000 loc (comments and blank lines excluded) with 152 forms+reports (27.000 loc), 32 modules (13.000 loc) and 11 class modules (4.000 loc).
I installed RubberdDuck 2.5.0 plugin in Access without problems (and this i very good).
With my application the first aspect emerged is a very low speed, mainly during reparsing .
By looking at Win10 performance data, I see an average 12-13% cpu (34% peak) and 600Mb RAM usage by MsAccess during reparsing tasks, but parsing time is about 60-62 sec each time, I mean not only the first reparsing after first application startup is so long. During this 60 secs MsAccess interface is actually not responding, even if the Rubberduck command bar in VBE reports Ready status.
IMHO it is very good to know some work is in progress for performance point, as I read in your posts (and for sure I agree on Functionality come before Performance).
2) Trying to implement the very intriguing Builder pattern I reach the point, at the very beginning of this post, where, if I well understand, after Encapsulate Field menu choice I should “Check the wrap fields in private type box, then click the Select all button and hit OK.” but I cannot see where the “wrap” box is…
The Encapsulate Field form appears showing to the current Public attribute in the module where the cursor is located and I can only change the Property Name and disable the Let checkbox, no Wrap Fields box nor Select All button appear. I tried both 2.5.0 stable and 188.8.131.5244 release, same behaviour.
I missing something about the Encapsulate Field procedure ?
Thank you for sharing inspiring ideas !!
…sorry for point 2) in my previous post…
It has been my misunderstanding, I installed latest RubberDuck rel. 184.108.40.20624 and it works perfectly !
Very nice feature, thank you.
LikeLiked by 1 person
[…] This particular refactoring has seen a terrific enhancement that makes it very easy to cleanly and quickly turn a set of public fields into Property Get/Let members, with a Private Type TClassName and a module-scope Private this As TClassName instance variable – and all properties automatically reading/writing from it. You can see this feature in action in the previous article. […]
First of all tyvm for this nice pattern.
But I don’t really understand how to use .WithXYZ after i already created an IUser object.
For instance your code:
“Dim identity As IUser
Set identity = UserBuilder.Create(uniqueId, uniqueName) _
.WithName(first, last) _
Now identity is “created” with uniqueId, uniqueName, first, ……
And for example I want to add ‘WithTwoFactorEnabled(“+49xxx”)’ later in my code. Is this even possible?
Like: identity.WithTwoFactorEnabled(“+49xxx”, true)
“The advantages are numerous, starting with the possibility to initialize the builder with everything it needs (all the required values), so that the client code can call Build and consume a valid User object right away.”
So I thought it should actually be possible.
It’s possible, as long as it’s the builder interface you’re passing around: once the object is “built”, you’re not looking at a builder anymore.
One way I can think of, would be to have a “FromInstance” factory method that can spawn/initialize a new builder from a given instance of an object, such that a builder can be created off an already-built “user” object.
LikeLiked by 1 person
Thx, your “CreateFromInstnace(ByVal value As IUser)” idea does the trick for now. This saves me a couple of If statements. Builder Pattern is probably not necessary but I want to learn as much as possible.
Very likely not necessary! I’m finding this pattern is most useful when designing an API / object model, like the validation adorners in the MVVM infrastructure code… and even then, I didn’t need an explicit abstract builder interface for it!
It does make a very nice way to dig into objects and properties though!
Yeah I will watch that In the near future. But for now it’s to complicated for me 😀
As a non-programmer whoended up writing a ton of VBA to automate stuff, I appreciate the OOP overview. If I understand correctly, the IUser and IUserBuilder classes are never directly invoked in the object creation. Rather these are used as ‘structures/patterns’ for the User / UserBuilder class objects.
Also, one issue/error I found in the example code — In the UserBuilder class, the code example never initializes the ‘internal’ object as a new user. The code gives me an error until I add this statement in the Public Create function.
The more I read and think about this approach, the more I find it appealing as my collection of parameters grows with each month! But in my application, I have more than 5 classes for distinct objects which share the same set of parameters but with different implementation details. I would like to create and build them incrementally with this Builder approach. Is there a way to refactor the Builder responsibility into a single Factory class, instead of having the Builder residing within each class? Could you please guide me on how this could be done?