In my journey back through the decades, investigating how the software design principles I teach on Codemanship courses could have been applied in programming languages of the day, I’ve visited 2009 (Ruby), 1989 (C), 1979 (Fortran 77) and 1969 (Simula 67), as well as a shiny new language from the present day (Kotlin) to bring us up to date.
For 1999, I’ve thought long and hard about what language to choose, and eventually settled on arguably the most popular at the time: Visual Basic. In that year, VB was in it’s last incarnation before the introduction of .NET. This was the height of the Microsoft COM era. Visual Basic 6 had some elements of object orientation, which were – in reality – built on COM. That is, VB6 “classes” were modules that had private implementations hidden behind public COM interfaces (a class with no fields at all still took up 96 bytes because… COM).
Here’s the carpet quote example done using VB6 class modules.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Public Function Quote(ByRef room As Room, ByRef carpet As Carpet) As Double | |
| Dim area As Double | |
| area = room.Length * room.Length | |
| If carpet.RoundUp Then | |
| area = Ceiling(area) | |
| End If | |
| Quote = carpet.PricePerSqMtr * area | |
| End Function |
Room and Carpet are simple data classes, leading to the inevitable Feature Envy in CarpetQuote.
Also, the Quote() function does two distinct jobs: calculating the area of carpet required for a room and calculating the price of that fitted carpet. It knows too much.
Let’s break up the work…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Public Function Quote(ByRef room As Room, ByRef carpet As Carpet) As Double | |
| Quote = Price(carpet, Area(room)) | |
| End Function | |
| Public Function Area(ByRef room As Room) | |
| Area = room.Width * room.Length | |
| End Function | |
| Public Function Price(ByRef carpet As Carpet, ByVal area As Double) | |
| If carpet.RoundUp Then | |
| area = Ceiling(area) | |
| End If | |
| Price = carpet.PricePerSqMtr * area | |
| End Function |
Now let’s move those functions to where they belong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Public Function Quote(ByRef room As Room, ByRef carpet As Carpet) As Double | |
| Quote = carpet.Price(room.Area()) | |
| End Function |
Plus one for encapsulation, right? Well, not quite. Let’s take a look inside Room and Carpet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Private mWidth As Double | |
| Private mLength As Double | |
| Public Property Let Width(ByVal value As Double) | |
| mWidth = value | |
| End Property | |
| Public Property Get Width() As Double | |
| Width = mWidth | |
| End Property | |
| Public Property Let Length(ByVal value As Double) | |
| mLength = value | |
| End Property | |
| Public Property Get Length() As Double | |
| Length = mLength | |
| End Property | |
| Public Function Area() As Double | |
| Area = Width * Length | |
| End Function |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Private mPricePerSqMtr As Double | |
| Private mRoundUp As Boolean | |
| Public Property Let PricePerSqMtr(ByVal value As Double) | |
| mPricePerSqMtr = value | |
| End Property | |
| Public Property Get PricePerSqMtr() As Double | |
| PricePerSqMtr = mPricePerSqMtr | |
| End Property | |
| Public Property Let RoundUp(ByVal value As Boolean) | |
| mRoundUp = value | |
| End Property | |
| Public Property Get RoundUp() As Boolean | |
| RoundUp = mRoundUp | |
| End Property | |
| Public Function Price(ByVal Area As Double) | |
| If RoundUp Then | |
| Area = Ceiling(Area) | |
| End If | |
| Price = PricePerSqMtr * Area | |
| End Function | |
I’d like to hide the data, and get rid of these property procedures. In a language like Java, I could pass the data in to a constructor and keep them private. But VB6 doesn’t support constructors, because COM components don’t support constructors. So I have to instantiate each class and then set their data from outside.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Private Sub btnQuote_Click() | |
| Dim quote As New CarpetQuote | |
| Dim room As New Room | |
| Dim carpet As New Carpet | |
| room.Width = CDbl(txtWidth.Text) | |
| room.Length = CDbl(txtLength.Text) | |
| carpet.PricePerSqMtr = CDbl(txtPricePerSqMtr.Text) | |
| carpet.RoundUp = chkRoundUp.value | |
| lblPrice.Caption = "£" & quote.quote(room, carpet) | |
| End Sub |
Is there a way to approximate a constructor in VB6 and keep the data hidden? Well, not really. In C, we could use a function in the same module as the data is defined to initialie a new Room or Carpet. VB6 doesn’t support static methods on classes, so a function to create an object could only be defined in a separate module, so the ability to set field values would have to be exposed. We could get rid of our getters, but not our setters.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Public Function CreateRoom(ByVal Width As Double, ByVal Length As Double) As Room | |
| Dim room As New Room | |
| room.Width = Width | |
| room.Length = Length | |
| Set CreateRoom = room | |
| End Function |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Private mWidth As Double | |
| Private mLength As Double | |
| Public Property Let Width(ByVal value As Double) | |
| mWidth = value | |
| End Property | |
| Public Property Let Length(ByVal value As Double) | |
| mLength = value | |
| End Property | |
| Public Function Area() As Double | |
| Area = mWidth * mLength | |
| End Function |
Then our client can just use the CreateRoom() function to instantiate Room. It’s a little better, but – as with many languages – encapsulation can only really be achieved through discipline in writing client code, not actual language features. (This is just as true in, say, Python as in VB6.)
Now, how about swappability? Does VB6 support easy polymorphism? Remember that, in VB6, “classes” are really modules hidden behind COM interfaces. For sure, we can multiple classes that implement the same COM interface. So, if we wanted to have two different ways of calculating the area of a room, we could define a Room interface (as a .cls file)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Public Function Area() As Double | |
| End Function |
…and then have different implementations of Room that know about the details.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Implements Room | |
| Private mWidth As Double | |
| Private mLength As Double | |
| Public Property Let Width(ByVal value As Double) | |
| mWidth = value | |
| End Property | |
| Public Property Let Length(ByVal value As Double) | |
| mLength = value | |
| End Property | |
| Public Function Room_Area() As Double | |
| Room_Area = mWidth * mLength | |
| End Function |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Implements Room | |
| Private mRadius As Double | |
| Public Property Let Radius(ByVal value As Double) | |
| mRadius = value | |
| End Property | |
| Public Function Room_Area() As Double | |
| Room_Area = (mRadius * 2) * (mRadius * 2) | |
| End Function |
CarpetQuote works with either implementation of room, and only sees the Room COM interface with the Area() method. This is how we hide details/data in Visual Basic 6 – using COM interfaces.
So that’s a tick for swappability, and a kind of tick – after a fashion – to encapsulation. Finally, can we make a VB6 class present client-specific interfaces?
Imagine we have a client that just needs to know how many flights of stairs carpet fitters will have to climb/descend to reach a room, so we can calculate a premium. Can we add another interface to an implementation of Room?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Public Function FlightsOfStairs() As Integer | |
| End Function |
A VB6 class can implement more than one COM interface.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Option Explicit | |
| Implements Room | |
| Implements FloorLevel | |
| Private mWidth As Double | |
| Private mLength As Double | |
| Private mLevel As String | |
| Public Property Let Width(ByVal value As Double) | |
| mWidth = value | |
| End Property | |
| Public Property Let Length(ByVal value As Double) | |
| mLength = value | |
| End Property | |
| Public Property Let Level(ByVal value As String) | |
| mLevel = value | |
| End Property | |
| Public Function Room_Area() As Double | |
| Room_Area = mWidth * mLength | |
| End Function | |
| Public Function FloorLevel_FlightsOfStairs() As Integer | |
| Select Case mLevel | |
| Case "G" | |
| FloorLevel_FlightsOfStairs = 0 | |
| Case "B" | |
| FloorLevel_FlightsOfStairs = -1 | |
| Case Else | |
| FloorLevel_FlightsOfStairs = CInt(mLevel) | |
| End Select | |
| End Function |
Our client binds only to the FloorLevel interface.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Private Function CalculateStairsPremium(ByRef Level As FloorLevel) As Double | |
| CalculateStairsPremium = Abs(Level.FlightsOfStairs) * 50# | |
| End Function |
So, through the use of COM interfaces, that’s a tick for Interface Segregation.
Which means that – perhaps surprisingly – VB6 is 100% S.O.L.I.D. Who’d have thunk?
You can view the complete source code at https://github.com/jasongorman/SOLID-VB6