When writing OOP code in VBA, it’s important to keep a number of things in mind:
- A class can be given a default instance, which makes all its public members usable without needing to create a new instance.
- An interface can very well expose only public
property getaccessors, but no mutator.
- A class
Implementsas many interfaces as needed.
- Events cannot be exposed by an interface.
If you ever exported a class module and examined it in your favorite text editor, you probably noticed these:
Attribute VB_Name = "Class1" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = False Attribute VB_Exposed = False
VB_Name attribute determines the identifier the class will be referred to in code;
VB_GlobalNameSpace makes its members global which is kinda anti-OOP..
VB_Creatable can only be
False in VBA projects, and means that other VBA projects cannot directly create a new instance of that class.
VB_Exposed determines whether other VBA projects can see this class or not.
The one we’re interested in, is
VB_PredeclaredId. If you export a UserForm module, you’ll notice it’s predeclaredId attribute is
True. This is what allows you to work against a form without creating an instance – you’re using the default instance when you do that.. and you shouldn’t.
Finding the cheapest hotel
Here’s a little problem that I’m going to solve in VBA, with full-blown OOP:
A hotel chain operating in Goa wishes to offer room reservation services. They have three hotels in Goa: GreenValley, RedRiver and BlueHills. Each hotel has separate weekday and weekend (Saturday and Sunday) rates. There are special rates for rewards customer as a part of loyalty program.Each hotel has a rating assigned to it.
- GreenValley with a rating of 3 has weekday rates as Rs1100 for regular customer and Rs800 for rewards customer. The weekend rates are 900 for regular customer and 800 for a rewards customer.
- RedRiver with a rating of 4 has weekday rates as Rs1600 for regular customer and Rs1100 for rewards customer. The weekend rates are 600 for regular customer and 500 for a rewards customer.
- BlueHills with a rating of 5 has weekday rates as Rs2200 for regular customer and Rs1000 for rewards customer. The weekend rates are 1500 for regular customer and 400 for a rewards customer.
IMPORTANT: Before you read any further
This exercise isn’t about solving the problem. The problem is rather easy to solve. It’s about managing changes, writing code that can survive changes. Specifications are never carved in stone, they change all the time. Today the hotel chain has 3 hotels, tomorrow they might have 3,500. Today the hotel chain has two types of customers. Tomorrow they might have three; eventually the chain acquires another chain in another country, and then the prices need to be converted between USD and EUR before they can be compared. The foreign hotels might have different statutory holidays, and it wouldn’t matter until the CEO decided that July 4th reservations would be 25% off, but only in the US hotels.
This solution isn’t the one OOP way to do things. It’s a solution; your mileage may vary. There are many, many ways to do this – but a monolithic block of procedural code wouldn’t survive very long with the hectic reality depicted above, would it? Or it would, but then bugs would start appearing, and more changes would have to be made, perhaps introducing new bugs, too. Sounds familiar? Keep reading.
So, let’s say I want to store information about some pricing rule, based on some
DateType and some
CustomerType. I could describe this type as follows (the enums don’t belong to the interface, they’re just public types that were convenient to define there):
Option Explicit Public Enum CustomerType Regular Premium End Enum Public Enum DateType WkDay WkEnd End Enum Public Property Get DateType() As DateType End Property Public Property Get CustomerType() As CustomerType End Property Public Function ToString() As String End Function
Let’s call this interface
In well-designed OOP, one doesn’t design an interface to change. This IPricingRuleInfo interface will change, as soon as the requirements change and we need to expose a new property. But we’re going to use VBA interfaces differently here… just bear with me.
What we’re going to do with this interface, is a façade that the program will be written against, while we hide the implementation details.
The implementation would look like this:
Option Explicit Private Type TInfo DateType As DateType CustomerType As CustomerType End Type Private this As TInfo Implements IPricingRuleInfo Public Property Get CustomerType() As CustomerType CustomerType = this.CustomerType End Property Public Property Let CustomerType(ByVal value As CustomerType) this.CustomerType = value End Property Public Property Get DateType() As DateType DateType = this.DateType End Property Public Property Let DateType(ByVal value As DateType) this.DateType = value End Property Public Property Get Self() As IPricingRuleInfo Set Self = Me End Property Public Function Create(ByVal dtType As DateType, ByVal custType As CustomerType) As IPricingRuleInfo With New PricingRuleInfo .DateType = dtType .CustomerType = custType Set Create = .Self End With End Function Private Property Get IPricingRuleInfo_CustomerType() As CustomerType IPricingRuleInfo_CustomerType = this.CustomerType End Property Private Property Get IPricingRuleInfo_DateType() As DateType IPricingRuleInfo_DateType = this.DateType End Property Private Function IPricingRuleInfo_ToString() As String IPricingRuleInfo_ToString = CStr(this.CustomerType) & ";" & CStr(this.DateType) End Function
Create method: that’s the Factory Method, intended to be used from the default instance. The properties are instance members that really belong to an instance of the class; the implementation also exposes
Property Let accessors, so that
Create can assign the proprerty values of the instance to create and return.
Self getter is a little trick that enables this neat
With New syntax.
The private type helps remove awkward prefixes by legalizing identical field and property names, and if the class’ state ever needs to be serialized, it’s child play.
Any code that works with a PricingRuleInfo instance will have access to its setters and default instance. But the client code wouldn’t do that: the client code works with the IPricingRuleInfo interface, and know nothing of a default instance, a factory method, or
Property Let members: it only sees CustomerType and DateType read-only values, and a ToString method that concatenates them into a string.
And now we can have an
IPricingRule interface like this:
Option Explicit Public Property Get RuleInfo() As IPricingRuleInfo End Property Public Function Evaluate(ByVal info As IPricingRuleInfo) As Currency End Function
And then we can have as many implementations as we like – here, a simple one called FixedAmountPricingRule, that takes an amount at creation, encapsulates it, and then uses it to return a fixed amount when evaluating the rule:
Option Explicit Private Type TRule RuleInfo As IPricingRuleInfo Amount As Currency End Type Private this As TRule Implements IPricingRule Private Property Get IPricingRule_RuleInfo() As IPricingRuleInfo Set IPricingRule_RuleInfo = this.RuleInfo End Property Private Function IPricingRule_Evaluate(ByVal info As IPricingRuleInfo) As Currency IPricingRule_Evaluate = this.Amount End Function Public Property Get RuleInfo() As IPricingRuleInfo Set RuleInfo = this.RuleInfo End Property Public Property Set RuleInfo(ByVal value As IPricingRuleInfo) Set this.RuleInfo = value End Property Public Property Get Amount() As Currency Amount = this.Amount End Property Public Property Let Amount(ByVal value As Currency) this.Amount = value End Property Public Property Get Self() As IPricingRule Set Self = Me End Property Public Function Create(ByVal info As IPricingRuleInfo, ByVal value As Currency) As IPricingRule With New FixedAmountPricingRule Set .RuleInfo = info .Amount = value Set Create = .Self End With End Function
Again, we give this class a default instance by setting its
VB_PredeclaredId attribute to
True and re-importing the module into the project.
Next we’ll need an abstraction for hotels – enter
Option Explicit Public Property Get Name() As String End Property Public Property Get Rating() As Byte End Property Public Function CalculatePricing(ByVal info As IPricingRuleInfo) As Currency End Function Public Function GetDateType(ByVal value As Date) As DateType End Function
Notice how the interface exposes nothing of
IPricingRule. The implementation has a dependency on IPricingRule and IPricingRuleInfo, but knows nothing of the concrete types. Here’s the code:
Option Explicit Private Type THotel PricingRules As New Scripting.Dictionary Name As String Rating As Byte End Type Private this As THotel Implements IHotel Public Property Get Name() As String Name = this.Name End Property Public Property Let Name(ByVal value As String) this.Name = value End Property Public Property Get Rating() As Byte Rating = this.Rating End Property Public Property Let Rating(ByVal value As Byte) this.Rating = value End Property Public Property Get Self() As IHotel Set Self = Me End Property Public Function Create(ByVal hotelName As String, ByVal stars As Byte, Optional ByVal rules As Collection = Nothing) As StandardHotel Dim rule As IPricingRule With New StandardHotel .Name = hotelName .Rating = stars If Not rules Is Nothing Then For Each rule In rules .AddPricingRule rule Next End If Set Create = .Self End With End Function Public Sub AddPricingRule(ByVal rule As IPricingRule) this.PricingRules.Add rule.RuleInfo.ToString, rule End Sub Private Function IHotel_CalculatePricing(ByVal info As IPricingRuleInfo) As Currency Dim rule As IPricingRule Set rule = this.PricingRules(info.ToString) IHotel_CalculatePricing = rule.Evaluate(info) End Function Private Function IHotel_GetDateType(ByVal value As Date) As DateType IHotel_GetDateType = IIf(Weekday(value, vbMonday) <= 5, WkDay, WkEnd) End Function Private Property Get IHotel_Name() As String IHotel_Name = this.Name End Property Private Property Get IHotel_Rating() As Byte IHotel_Rating = this.Rating End Property
GetDateType function: it allows a given
IHotel implementation to come up with funky creative ways to determine the
DateType for a given date value.
Also interesting, the
AddPricingRule procedure, which isn’t exposed by the
IHotel interface, but that adds pricing rules to the encapsulated dictionary of pricing rules; given an
IPricingRuleInfo instance, we can now calculate the price by evaluating the rule.
HotelFinder class is just an object that encapsulates the logic to find the cheapest hotel, given two dates and a
Option Explicit Private Type TFinder Hotels As Collection End Type Private this As TFinder Public Property Get Hotels() As Collection Set Hotels = this.Hotels End Property Public Function FindCheapestHotel(ByVal fromDate As Date, ByVal toDate As Date, ByVal custType As CustomerType) As String Dim place As IHotel Dim checkedDate As Date Dim cheapestAmount As Currency Dim cheapestHotel As IHotel Dim hotelTotal As Currency For Each place In this.Hotels hotelTotal = 0 For checkedDate = fromDate To toDate Dim info As IPricingRuleInfo Set info = PricingRuleInfo.Create(place.GetDateType(checkedDate), custType) hotelTotal = hotelTotal + place.CalculatePricing(info) Next If cheapestAmount = 0 Or hotelTotal < cheapestAmount Then cheapestAmount = hotelTotal Set cheapestHotel = place ElseIf hotelTotal = cheapestAmount And cheapestHotel.Rating > place.Rating Then 'same price, but higher rating; higher rating gets precedence Set cheapestHotel = place End If Debug.Print place.Name, Format(hotelTotal, "$#,##0.00") Next FindCheapestHotel = cheapestHotel.Name End Function Private Sub Class_Initialize() Set this.Hotels = New Collection End Sub Private Sub Class_Terminate() Set this.Hotels = Nothing End Sub
So, we iterate a collection of hotels, evaluate the stay at each one (output the amount to the debug pane), and return the name of the cheapest hotel.
At the top of the call stack lies a procedure that creates an instance of that
HotelFinder, populates its
Hotels collection, and ouputs the result of the
FindCheapestHotel function. This is where we reap the benefits of OOP: initializing the hotels reads pretty much exactly like reading the specs.
Option Explicit Public Sub Test(ByVal checkin As Date, ByVal checkout As Date, ByVal custType As CustomerType) Dim finder As New HotelFinder InitializeHotels finder Debug.Print finder.FindCheapestHotel(checkin, checkout, custType) End Sub Private Sub InitializeHotels(ByVal finder As HotelFinder) With StandardHotel.Create("Green Valley", 3) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, Premium), 800) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, Premium), 800) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, Regular), 1100) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, Regular), 900) finder.Hotels.Add .Self End With With StandardHotel.Create("Red River", 4) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, Premium), 1100) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, Premium), 500) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, Regular), 1600) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, Regular), 600) finder.Hotels.Add .Self End With With StandardHotel.Create("Blue Hills", 5) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, Premium), 1000) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, Premium), 400) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, Regular), 2200) .AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, Regular), 1500) finder.Hotels.Add .Self End With End Sub
And we get output:
Test Now, Now + 3, Premium Green Valley $3,200.00 Red River $4,400.00 Blue Hills $4,000.00 Green Valley
Is that over-engineered? As I said above, most definitely. But then, how would a Java, C#, or VB.NET solution look like? Not much different, save a PricingStrategyFactoryFactory class for the Java code of course! The point, again, was an exercise in writing code resistant to change, not just solving a problem. Now when the specs change and we need a new pricing rule that grants 20% off on the first Tuesday of every second month, we don’t need to change any code except for the code that initializes the hotels: we just implement the new functionality, without changing code that already works: that’s the Open/Closed Principle at play. In fact, I tried to depict all of SOLID in this code – I hope I did something like that.
26 thoughts on “OOP VBA pt.2: Factories and Cheap Hotels”
Loving your application, thank you. Have studied this article and have a question. How do I reference a vba class sitting outside my current project?
Thanks and sorry for the noob question.
Thanks! Simply add a reference to the project that contains and exposes the class you want to use 😉
Add-in projects are great for this, since Excel loads them at startup anyway. So you can make yourself a “toolbox” addin, and reference it from your VBA projects.
LikeLiked by 1 person
Really great post. I can’t wait to read the part 3.
I’ve read an answer from you on Stackoverflow, which mentioned the MVC pattern in VBA. I’m very interested in this topic. Would you please share a simple example with two or three views/forms, just like the example in this post? Thank you very much! Best wishes!
Have been implementing this beaut factory pattern in my projects and love being able to unit test now I can compose objects this way, and use Rubberduck.
So keen to implement your repository interface, and other data management classes, but it is all a bit trial and error for me….
Would love to see a simple implementation of your VBTools.
Maybe there is one on Stack overflow but I have not found it yet. Have learned a lot from your code review posts there though, so thank you for all you have done for VBA.
LikeLiked by 1 person
One more quick question:
The Create method for StandardHotel returns a StandardHotel (concrete) type; the Get Self property returns an IHotel (abstract) type. How is it we can assign Create to Self (in the StandardHotel.Create function) when they are different types?
In the other factories you returned an interface object, which would not have worked for the StandardHotel class as we wanted to be able to change its state. But why not have the Self Getter return a concrete type in this case?
Ah, probably an oversight – good catch! It wouldn’t matter though, because the caller expects the abstraction, not the concrete type… but while they *are* different types, one *implements* the other, and this allows you to cast the concrete type into its interface and vice-versa, but casting an interface to a concrete type breaks the [Liskov Substitution Principle](https://lostechies.com/derickbailey/files/2011/03/LiskovSubtitutionPrinciple_52BB5162.jpg).
Hello! To make it even easier to understand, could you make a “myPlan.xls” available as an example? Thank you very much for the site, continue !!!
LikeLiked by 1 person
Thank you for your examples.
It’s very hard to dive into OOP VBA and there are only few good examples.
I still have one question left:
The classes that implement IPricingRule encapsulate IPricingRuleInfos.
Why do IPricingRules need an IPricingRuleInfo in order to evaluate a pricing? (even in your implementation, you don’t use the IPricingRuleInfo)
Since the IPricingRule knows it’s own IPricingRuleInfo, I don’t see any reason why it would need another Rule to be able to evaluate?
Thanks! …I’m going to have to dig into that (old) code a bit before I can answer your question, but IIRC there was indeed at least one interface that was rather superfluous for the problem at hand (i.e. the pricing rules as specified), but that I thought would be needed given different rules that I never ended up implementing
Take your time!
Reading your article, I just couldn’t see, why that rule is necessary (since it kinda works against the encapsulation)
Being new to this I have been trying to create a workbook with all of this in it and I am struggling to get it to compile simply because I am (likely) not naming the classes correctly. It sure would be nice to have the explicit name of the class above the code lines and to identify when a class needs to have the PredeclareID – True (I am presuming that if the class uses the “With New WhateverClass” technique it is = True).
Thank you for a very educational post – just need a small it more for those of us that are very new to this.
Thankyou for this. I have just used it and it makes me very happy 🙂
I have one question.
Using the nice syntax you show
Public Property Get Self() As IPricingRuleInfo
Set Self = Me
Public Function Create(ByVal dtType As DateType, ByVal custType As CustomerType) As IPricingRuleInfo
With New PricingRuleInfo
.DateType = dtType
.CustomerType = custType
Set Create = .Self
I noticed when I look in the Locals windows, I can keep going deeper and deeper looking at myself!
Does this cause any problem at all?
I messed around a bit and ended up deleting the self property and writing my Create function like below. It got rid of the recursive self property in the locals window. I don’t really know what I’m doing so just asking is this ok?
It seems to work, but being a noob I’m hesitant.
Public Function Create(d As String, l As String) As MyTableRow
Dim newRow As New MyTableRow
.Description = d
.Length = l
Set Create = newRow
LikeLiked by 1 person
Should work too, but then the Set Create instruction should be outside the With block, …and I’d probably just remove the With block altogether, since there’s now a local variable to use. The Self property is really just a convenience to enable accessing a With block variable inside that block – yes it’s “recursive” in that you can always invoke Self on the returned object, but then that’s a bit like doing Range.Cells.Cells.Cells.Cells…, or Application.Application.Application… – why would you want to do that?
Also with interfaces, if you make Create and Self return the interface you want the consumer to use, then these two members are only accessible through the default interface, e.g. MyTableRow, while ITableRow doesn’t need to expose Create or Self, and the setter for Description can very well be unavailable from ITableRow =)
LikeLiked by 1 person
Dear Mathieu, thank you for everything you post here and do to VBA.
I think, After examining this code I’ve found a mistake just to correct and have proper results
So, in CheapestHotelFinder function :
“ElseIf hotelTotal = cheapestAmount And cheapestHotel.Rating > place.Rating Then” is to be
“ElseIf hotelTotal = cheapestAmount And cheapestHotel.Rating < place.Rating Then"
LikeLiked by 1 person
Is this available as a downloadable sample?
I think I do have that workbook somewhere, I’ll try to dig it up tomorrow and put it up on GitHub. Cheers!
For properties and methods that are not exposed via the implemented interface and for those that are not used except within the default instance (“Create”), why would those properties and methods (e.g. CustomerType, DateType in PricingRuleInfo) not be Private instead of Public?
If they’re private, then the factory method can’t access them!
I guess I misunderstand the concept. I thought Private hides items from external use, but not from internal use by the class they’re in. Public would un-hide an item to an external call, but if we’re using the factory method to access and set/let the things that are not exposed in the Interface they would not be accessed externally if programming to the interface. Just trying to get my head around all of this OOP . Thank you for helping!
In VBA member accessibility describes how a member of an ::instance of the class:: is accessible; if it’s private, then it’s not on that object’s interface – it doesn’t matter that the object is the same type as the class we’re in! Similar to how “Me” doesn’t expose any of the private members either.
Thanks again Rubberduck. (I was last here in Feb)
So I wrote a couple of classes that worked out really nice using the technique.
But then this afternoon it wasn’t working. No errors or anything just no properties.
Finally worked it out.
I forgot the dot in this line
Set Create = .self
Hard to spot if you’re not looking closely – posting in case any other noobs fall into the same trap
Well done! Without the dot, the factory method would be essentially returning ‘Me’, i.e. the default instance itself, …every time, and with the default state regardless of parameters.
LikeLiked by 1 person
Pardon the pun.. I literally missed the whole point :p
LikeLiked by 2 people
Mathieu, did you ever put the example up on GitHub? I can’t seem to find it. Thanks!
No need. Everything needed is in the article to roll my own. Thanks!