I thought this question this question made a good excuse for some basic OOP code, so I whipped up a little bit of code to demonstrate it in VBA; the idea is to have tutorial-grade code, to show how interfaces and default instances can be used to implement immutable types and factory methods to simulate constructors in VBA.
I thought this question made a good excuse for some basic OOP code, so I whipped up a little bit of code to demonstrate it in VBA; the idea is to have tutorial-grade code, to show how interfaces and default instances can be used to implement immutable types and factory methods to simulate constructors in VBA.
I thought this question made a good excuse for some basic OOP code, so I whipped up a little bit of code to demonstrate it in VBA; the idea is to have tutorial-grade code, to show how interfaces and default instances can be used to implement immutable types and factory methods to simulate constructors in VBA.
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(Optional ByVal info As IPricingRuleInfo = Nothing) 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
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
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(Optional ByVal info As IPricingRuleInfo = Nothing) 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
I thought this question made a good excuse for some basic OOP code, so I whipped up a little bit of code to demonstrate it in VBA, however I'm not happy with how it turned out. How should I refactor it to be more flexible, OOP-style? TheVBA; the idea is to end up withhave tutorial-grade code.. and despite the fact that it works, I'm pretty sure this isn't itto show how interfaces and default instances can be used to implement immutable types and factory methods to simulate constructors in VBA.
Option Explicit
Public FunctionSub FindCheapestHotelTest(ByVal fromDatecheckin As Date, ByVal toDatecheckout As Date, ByVal custType As CustomerType) As String
Dim hotelsfinder As Collection
Set hotels = InitializeHotels
New HotelFinder
Dim prices AsInitializeHotels Dictionaryfinder
Set pricesDebug.Print =finder.FindCheapestHotel(checkin, Newcheckout, DictionarycustType)
End Sub
Dim place As Hotel
Private Sub DimInitializeHotels(ByVal checkedDatefinder As DateHotelFinder)
For EachWith placeStandardHotel.Create("Green InValley", hotels3)
prices.AddAddPricingRule placeFixedAmountPricingRule.Name, placeCreate(PricingRuleInfo.GetPriceCreate(fromDateWkDay, toDatePremium), custType800)
Debug.PrintAddPricingRule placeFixedAmountPricingRule.Name, Format(pricesCreate(placePricingRuleInfo.Name)Create(WkEnd, "$#,##0.00"Premium), Next800)
Dim cheapestAmount As Currency
cheapestAmount =.AddPricingRule pricesFixedAmountPricingRule.ItemsCreate(1)
PricingRuleInfo.Create(WkDay, Regular), 1100)
Dim cheapestKey As String
cheapestKey =.AddPricingRule pricesFixedAmountPricingRule.KeysCreate(1)
PricingRuleInfo.Create(WkEnd, Regular), 900)
Dim key As Variant
For Each key Infinder.Hotels.Add prices.KeysSelf
If prices(key) < cheapestAmountEnd ThenWith
cheapestKey = key
cheapestAmount =With pricesStandardHotel.Create(key)
"Red ElseIfRiver", prices(key4) = cheapestAmount Then
If.AddPricingRule hotelsFixedAmountPricingRule.itemCreate(key)PricingRuleInfo.Rating > hotelsCreate(cheapestKeyWkDay, Premium).Rating, Then1100)
.AddPricingRule cheapestKeyFixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, =Premium), key500)
.AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, EndRegular), If1600)
End If
.AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkEnd, Regular), Next600)
FindCheapestHotel =finder.Hotels.Add cheapestKey.Self
End Function
Private Function InitializeHotels() As CollectionWith
Static hotels As Collection
If hotelsWith IsStandardHotel.Create("Blue NothingHills", Then5)
Set hotels = New Collection
.AddPricingRule FixedAmountPricingRule.Create(PricingRuleInfo.Create(WkDay, WithPremium), hotels1000)
.AddAddPricingRule HotelFactoryFixedAmountPricingRule.Create("Green Valley", 3, 900, 1100, 800PricingRuleInfo.Create(WkEnd, 800Premium), "Green Valley"400)
.AddAddPricingRule HotelFactoryFixedAmountPricingRule.Create("Red River", 4, 600, 1600, 500PricingRuleInfo.Create(WkDay, 1100Regular), "Red River"2200)
.AddAddPricingRule HotelFactoryFixedAmountPricingRule.Create("Blue Hills"PricingRuleInfo.Create(WkEnd, 5Regular), 1500, 2200, 400, 1000), "Blue Hills"
Endfinder.Hotels.Add With.Self
End IfWith
Set InitializeHotels = hotels
End FunctionSub
?FindCheapestHotel(Test Now, Now+7Now + 3, Regular)Premium
Green Valley 8ドル3ドル,400200.00
Red River 10ドル4ドル,800400.00
Blue Hills 16ドル4ドル,200000.00
Green Valley
I started by defining anneeded a type to encapsulate the variables that can influence a hotel's pricing. And because I want these values to be immutable, I exposed getters for them in this IPricingStrategyIPricingRuleInfo
interface:
'@Interface
'@Folder("Abstract")
Option Explicit
Public Enum CustomerType
Regular
Premium
End Enum
Public FunctionEnum CalculatePricing(ByValDateType
checkin WkDay
WkEnd
End Enum
Public Property Get DateType() As Date,DateType
End ByValProperty
Public custTypeProperty Get CustomerType() As CustomerType
End Property
Public Function ToString() As CurrencyString
End Function
And then I implementedThe class that implements it inhas a generic way that I could useVB_PredeclaredId
attribute set to generate data off of an input file ifTrue
, which makes a PricingRuleInfo
object variable available to use the Create
factory method. I had to;added a Self
getter that wayreturns Me
, so that I could handle finding the cheapest stay among 3,500 hotels, for example. Here isuse a neat With New
syntax in the GenericPricingStrategyCreate
classmethod:
'@Folder("Concrete")
Option Explicit
Public Enum PricingError
Undefined = vbObjectError + 512
InvalidCustomerTypeError
End Enum
Private Const InvalidCustomerTypeMessage As String = "Specified customer type is not supported."
Private Type TPricingData
RegularWkDayPrice As Currency
regularWkEndPrice As CurrencyTInfo
premiumWkDayPriceDateType As CurrencyDateType
premiumWkEndPriceCustomerType As CurrencyCustomerType
End Type
Private this As TPricingDataTInfo
Implements IPricingStrategyIPricingRuleInfo
Public Property Get WeekdayPrice(ByVal custType As CustomerType() As Currency
Select Case custType
Case CustomerType.Premium
WeekdayPrice = this.premiumWkDayPrice
Case CustomerType.Regular
WeekdayPrice = this.RegularWkDayPrice
Case Else
OnInvalidCustomerType "WeekdayPrice (Get)"
End SelectCustomerType
End Property
FriendPublic Property Let WeekdayPrice(ByVal custType As CustomerType, (ByVal value As Currency)
Select Case custType
Case CustomerType.Premium)
this.premiumWkDayPrice = value
Case CustomerType.Regular
this.RegularWkDayPrice = value
Case Else
OnInvalidCustomerType "WeekdayPrice (Let)"
End Select
End Property
Public Property Get WeekendPriceDateType(ByVal custType As CustomerType) As Currency
Select Case custType
Case CustomerType.Premium
WeekendPrice = this.premiumWkEndPrice
Case CustomerType.RegularDateType
WeekendPriceDateType = this.regularWkEndPrice
Case Else
OnInvalidCustomerType "WeekendPrice (Get)"
End SelectDateType
End Property
FriendPublic Property Let WeekendPriceDateType(ByVal custType As CustomerType, ByVal value As CurrencyDateType)
Select Case custType
Case CustomerType.Premium
this.premiumWkEndPrice = value
Case CustomerType.Regular
this.regularWkEndPriceDateType = value
End Property
Case Else
Public OnInvalidCustomerTypeProperty "WeekendPriceGet Self(Let)"
As IPricingRuleInfo
Set Self End= SelectMe
End Property
PrivatePublic Function IPricingStrategy_CalculatePricingCreate(ByVal checkindtType As DateDateType, ByVal custType As CustomerType) As Currency
Dim wkDay As IntegerIPricingRuleInfo
wkDay =With Weekday(checkin,New vbMonday)PricingRuleInfo
Dim isWeekendPricing As Boolean
isWeekendPricing.DateType = (wkDay > 5)
dtType
Dim result As Currency
Select.CustomerType Case= custType
Case CustomerType.Regular
Set resultCreate = IIf(isWeekendPricing, this.regularWkEndPrice, this.RegularWkDayPrice)Self
End With
End Function
Private Property Get IPricingRuleInfo_CustomerType() CaseAs CustomerType.Premium
resultIPricingRuleInfo_CustomerType = IIf(isWeekendPricing, this.premiumWkEndPrice, this.premiumWkDayPrice)
CustomerType
CaseEnd ElseProperty
OnInvalidCustomerType "CalculatePricing"
EndPrivate Select
Property Get IPricingRuleInfo_DateType() As DateType
IPricingStrategy_CalculatePricingIPricingRuleInfo_DateType = result
this.DateType
End FunctionProperty
Private SubFunction OnInvalidCustomerTypeIPricingRuleInfo_ToString(ByVal source) As String)
Err.RaiseIPricingRuleInfo_ToString PricingError.InvalidCustomerTypeError,= TypeNameCStr(Methis.CustomerType) & "."";" & source, InvalidCustomerTypeMessageCStr(this.DateType)
End SubFunction
And this is where I believe I erredThe - theIPricingRule
interface exposes an HotelFactoryEvaluate
classmethod that takes an IPricingRuleInfo
object, so an IPricingRule
implementation could evaluate a price based on parameters, or whatever is applicable, really:
'@Folder("Concrete")
Option Explicit
Public Function Create(ByVal hotelName As String, _
ByVal hotelRating As Byte, _
ByVal regWkEndPrice As Currency, _
ByVal regWkDayPrice As Currency, _
ByVal premiumWkEndPrice As Currency, _
ByVal premiumWkDayPriceProperty AsGet CurrencyRuleInfo() As Hotel
Dim strategy As GenericPricingStrategyIPricingRuleInfo
Set strategy = NewEnd GenericPricingStrategyProperty
With strategy
.WeekdayPrice(Regular) = regWkDayPrice
Public Function .WeekendPriceEvaluate(Regular) = regWkEndPrice
Optional ByVal info As .WeekdayPrice(Premium)IPricingRuleInfo = premiumWkDayPrice
.WeekendPrice(PremiumNothing) = premiumWkEndPrice
End With
Dim result As New Hotel
result.Name = hotelName
result.Rating = hotelRating
Set result.PricingStrategy = strategy
Set Create = result
Currency
End Function
Here isTo solve the original problem all I needed was a HotelFixedAmountPricingRule
class, which I believe would havereturns a predetermined amount regardless of what parameter is passed to be implementing IPricingStrategyEvaluate
itself- again this class has a VB_PredeclaredId
attribute value set to True
, and exposes a factory method:
'@Folder("Concrete")
Option Explicit
Private Type THotelTRule
PricingRuleInfo As IPricingStrategyIPricingRuleInfo
NameAmount As StringCurrency
End Type
Private Ratingthis As ByteTRule
End
Implements TypeIPricingRule
Private Enum HotelError
Property Get IPricingRule_RuleInfo() UndefinedAs =IPricingRuleInfo
vbObjectError + 1024
Set IPricingRule_RuleInfo = InvalidDateRangethis.RuleInfo
End EnumProperty
Private ConstFunction InvalidDateRangeMessageIPricingRule_Evaluate(ByVal info As StringIPricingRuleInfo) =As "SpecifiedCurrency
date range is notIPricingRule_Evaluate valid."
Private= this As.Amount
End THotelFunction
Public Property Get NameRuleInfo() As StringIPricingRuleInfo
NameSet RuleInfo = this.NameRuleInfo
End Property
FriendPublic Property LetSet NameRuleInfo(ByVal value As StringIPricingRuleInfo)
Set this.NameRuleInfo = value
End Property
Public Property Get RatingAmount() As ByteCurrency
RatingAmount = this.RatingAmount
End Property
FriendPublic Property Let RatingAmount(ByVal value As ByteCurrency)
this.RatingAmount = value
End Property
FriendPublic Property SetGet PricingStrategySelf(ByVal value) As IPricingStrategy)IPricingRule
Set this.PricingSelf = valueMe
End Property
Public Function GetPriceCreate(ByVal checkin As Date, ByVal checkoutinfo As DateIPricingRuleInfo, ByVal custTypevalue As CustomerTypeCurrency) As Currency
If checkin > checkout Then OnInvalidDateRange "GetPrice"
IPricingRule
Dim resultWith AsNew CurrencyFixedAmountPricingRule
Dim checkedDay As Date
ForSet checkedDay.RuleInfo = checkin To checkoutinfo
result.Amount = result + this.Pricing.CalculatePricing(checkedDay, custType)value
Next
Set GetPriceCreate = result.Self
End Function
Private Sub OnInvalidDateRange(ByVal source As String)
Err.Raise HotelError.InvalidDateRange, TypeName(Me) & "." & source, InvalidDateRangeMessageWith
End SubFunction
I'm pretty sure I went overboard. How doNext, the IHotel
interface, which can map a Date
to a DateType
value, and calculate a price given an IPricingRuleInfo
parameter; of course the interface also exposes the establishment's Name
and Rating
:
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
The StandardHotel
class that implements this interface has a VB_PredeclaredId
attribute set to True
, and exposes a Create
factory method and an AddPricingRule
method to set up the hotel's pricing rules; because I wanted a way to ensure no two IPricingRuleInfo
would collide and make this right? As alwaysthe pricing potentially ambiguous, I'm openI decided to any &use a Scripting.Dictionary
and used IPricingRuleInfo.ToString
as a key. The GetDateType
for a StandardHotel
merely determines if a date is a WkDay
or a WkEnd
date - in time other implementations could return a Holiday
value given date X or Y, but I didn't need to go there for now:
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
Finally, the HotelFinder
class implements the logic that iterates all suggestionshotels and commentsfinds the cheapest one:
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
The Hotels
collection is initialized in the InitializeHotels
private procedure in Module1
, so the code at the top of the call stack has a pretty high level of abstraction.
I thought this question made a good excuse for some basic OOP code, so I whipped up a little bit of code to demonstrate it in VBA, however I'm not happy with how it turned out. How should I refactor it to be more flexible, OOP-style? The idea is to end up with tutorial-grade code.. and despite the fact that it works, I'm pretty sure this isn't it.
Option Explicit
Public Function FindCheapestHotel(ByVal fromDate As Date, ByVal toDate As Date, ByVal custType As CustomerType) As String
Dim hotels As Collection
Set hotels = InitializeHotels
Dim prices As Dictionary
Set prices = New Dictionary
Dim place As Hotel
Dim checkedDate As Date
For Each place In hotels
prices.Add place.Name, place.GetPrice(fromDate, toDate, custType)
Debug.Print place.Name, Format(prices(place.Name), "$#,##0.00") Next
Dim cheapestAmount As Currency
cheapestAmount = prices.Items(1)
Dim cheapestKey As String
cheapestKey = prices.Keys(1)
Dim key As Variant
For Each key In prices.Keys
If prices(key) < cheapestAmount Then
cheapestKey = key
cheapestAmount = prices(key)
ElseIf prices(key) = cheapestAmount Then
If hotels.item(key).Rating > hotels(cheapestKey).Rating Then
cheapestKey = key
End If
End If
Next
FindCheapestHotel = cheapestKey
End Function
Private Function InitializeHotels() As Collection
Static hotels As Collection
If hotels Is Nothing Then
Set hotels = New Collection
With hotels
.Add HotelFactory.Create("Green Valley", 3, 900, 1100, 800, 800), "Green Valley"
.Add HotelFactory.Create("Red River", 4, 600, 1600, 500, 1100), "Red River"
.Add HotelFactory.Create("Blue Hills", 5, 1500, 2200, 400, 1000), "Blue Hills"
End With
End If
Set InitializeHotels = hotels
End Function
?FindCheapestHotel(Now, Now+7, Regular)
Green Valley 8ドル,400.00
Red River 10ドル,800.00
Blue Hills 16ドル,200.00
Green Valley
I started by defining an IPricingStrategy
interface:
'@Interface
'@Folder("Abstract")
Option Explicit
Public Enum CustomerType
Regular
Premium
End Enum
Public Function CalculatePricing(ByVal checkin As Date, ByVal custType As CustomerType) As Currency
End Function
And then I implemented it in a generic way that I could use to generate data off of an input file if I had to; that way I could handle finding the cheapest stay among 3,500 hotels, for example. Here is the GenericPricingStrategy
class:
'@Folder("Concrete")
Option Explicit
Public Enum PricingError
Undefined = vbObjectError + 512
InvalidCustomerTypeError
End Enum
Private Const InvalidCustomerTypeMessage As String = "Specified customer type is not supported."
Private Type TPricingData
RegularWkDayPrice As Currency
regularWkEndPrice As Currency
premiumWkDayPrice As Currency
premiumWkEndPrice As Currency
End Type
Private this As TPricingData
Implements IPricingStrategy
Public Property Get WeekdayPrice(ByVal custType As CustomerType) As Currency
Select Case custType
Case CustomerType.Premium
WeekdayPrice = this.premiumWkDayPrice
Case CustomerType.Regular
WeekdayPrice = this.RegularWkDayPrice
Case Else
OnInvalidCustomerType "WeekdayPrice (Get)"
End Select
End Property
Friend Property Let WeekdayPrice(ByVal custType As CustomerType, ByVal value As Currency)
Select Case custType
Case CustomerType.Premium
this.premiumWkDayPrice = value
Case CustomerType.Regular
this.RegularWkDayPrice = value
Case Else
OnInvalidCustomerType "WeekdayPrice (Let)"
End Select
End Property
Public Property Get WeekendPrice(ByVal custType As CustomerType) As Currency
Select Case custType
Case CustomerType.Premium
WeekendPrice = this.premiumWkEndPrice
Case CustomerType.Regular
WeekendPrice = this.regularWkEndPrice
Case Else
OnInvalidCustomerType "WeekendPrice (Get)"
End Select
End Property
Friend Property Let WeekendPrice(ByVal custType As CustomerType, ByVal value As Currency)
Select Case custType
Case CustomerType.Premium
this.premiumWkEndPrice = value
Case CustomerType.Regular
this.regularWkEndPrice = value
Case Else
OnInvalidCustomerType "WeekendPrice (Let)"
End Select
End Property
Private Function IPricingStrategy_CalculatePricing(ByVal checkin As Date, ByVal custType As CustomerType) As Currency
Dim wkDay As Integer
wkDay = Weekday(checkin, vbMonday)
Dim isWeekendPricing As Boolean
isWeekendPricing = (wkDay > 5)
Dim result As Currency
Select Case custType
Case CustomerType.Regular
result = IIf(isWeekendPricing, this.regularWkEndPrice, this.RegularWkDayPrice)
Case CustomerType.Premium
result = IIf(isWeekendPricing, this.premiumWkEndPrice, this.premiumWkDayPrice)
Case Else
OnInvalidCustomerType "CalculatePricing"
End Select
IPricingStrategy_CalculatePricing = result
End Function
Private Sub OnInvalidCustomerType(ByVal source As String)
Err.Raise PricingError.InvalidCustomerTypeError, TypeName(Me) & "." & source, InvalidCustomerTypeMessage
End Sub
And this is where I believe I erred - the HotelFactory
class:
'@Folder("Concrete")
Option Explicit
Public Function Create(ByVal hotelName As String, _
ByVal hotelRating As Byte, _
ByVal regWkEndPrice As Currency, _
ByVal regWkDayPrice As Currency, _
ByVal premiumWkEndPrice As Currency, _
ByVal premiumWkDayPrice As Currency) As Hotel
Dim strategy As GenericPricingStrategy
Set strategy = New GenericPricingStrategy
With strategy
.WeekdayPrice(Regular) = regWkDayPrice
.WeekendPrice(Regular) = regWkEndPrice
.WeekdayPrice(Premium) = premiumWkDayPrice
.WeekendPrice(Premium) = premiumWkEndPrice
End With
Dim result As New Hotel
result.Name = hotelName
result.Rating = hotelRating
Set result.PricingStrategy = strategy
Set Create = result
End Function
Here is the Hotel
class, which I believe would have to be implementing IPricingStrategy
itself:
'@Folder("Concrete")
Option Explicit
Private Type THotel
Pricing As IPricingStrategy
Name As String
Rating As Byte
End Type
Private Enum HotelError
Undefined = vbObjectError + 1024
InvalidDateRange
End Enum
Private Const InvalidDateRangeMessage As String = "Specified date range is not valid."
Private this As THotel
Public Property Get Name() As String
Name = this.Name
End Property
Friend Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Rating() As Byte
Rating = this.Rating
End Property
Friend Property Let Rating(ByVal value As Byte)
this.Rating = value
End Property
Friend Property Set PricingStrategy(ByVal value As IPricingStrategy)
Set this.Pricing = value
End Property
Public Function GetPrice(ByVal checkin As Date, ByVal checkout As Date, ByVal custType As CustomerType) As Currency
If checkin > checkout Then OnInvalidDateRange "GetPrice"
Dim result As Currency
Dim checkedDay As Date
For checkedDay = checkin To checkout
result = result + this.Pricing.CalculatePricing(checkedDay, custType)
Next
GetPrice = result
End Function
Private Sub OnInvalidDateRange(ByVal source As String)
Err.Raise HotelError.InvalidDateRange, TypeName(Me) & "." & source, InvalidDateRangeMessage
End Sub
I'm pretty sure I went overboard. How do I make this right? As always, I'm open to any & all suggestions and comments.
I thought this question made a good excuse for some basic OOP code, so I whipped up a little bit of code to demonstrate it in VBA; the idea is to have tutorial-grade code, to show how interfaces and default instances can be used to implement immutable types and factory methods to simulate constructors in VBA.
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
Test Now, Now + 3, Premium
Green Valley 3ドル,200.00
Red River 4ドル,400.00
Blue Hills 4ドル,000.00
Green Valley
I needed a type to encapsulate the variables that can influence a hotel's pricing. And because I want these values to be immutable, I exposed getters for them in this IPricingRuleInfo
interface:
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
The class that implements it has a VB_PredeclaredId
attribute set to True
, which makes a PricingRuleInfo
object variable available to use the Create
factory method. I added a Self
getter that returns Me
, so that I could use a neat With New
syntax in the Create
method:
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
The IPricingRule
interface exposes an Evaluate
method that takes an IPricingRuleInfo
object, so an IPricingRule
implementation could evaluate a price based on parameters, or whatever is applicable, really:
Option Explicit
Public Property Get RuleInfo() As IPricingRuleInfo
End Property
Public Function Evaluate(Optional ByVal info As IPricingRuleInfo = Nothing) As Currency
End Function
To solve the original problem all I needed was a FixedAmountPricingRule
, which returns a predetermined amount regardless of what parameter is passed to Evaluate
- again this class has a VB_PredeclaredId
attribute value set to True
, and exposes a factory method:
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
Next, the IHotel
interface, which can map a Date
to a DateType
value, and calculate a price given an IPricingRuleInfo
parameter; of course the interface also exposes the establishment's Name
and Rating
:
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
The StandardHotel
class that implements this interface has a VB_PredeclaredId
attribute set to True
, and exposes a Create
factory method and an AddPricingRule
method to set up the hotel's pricing rules; because I wanted a way to ensure no two IPricingRuleInfo
would collide and make the pricing potentially ambiguous, I decided to use a Scripting.Dictionary
and used IPricingRuleInfo.ToString
as a key. The GetDateType
for a StandardHotel
merely determines if a date is a WkDay
or a WkEnd
date - in time other implementations could return a Holiday
value given date X or Y, but I didn't need to go there for now:
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
Finally, the HotelFinder
class implements the logic that iterates all hotels and finds the cheapest one:
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
The Hotels
collection is initialized in the InitializeHotels
private procedure in Module1
, so the code at the top of the call stack has a pretty high level of abstraction.