S.O.L.I.D. Visual Basic?

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.


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

view raw

CarpetQuote.cls

hosted with ❤ by GitHub

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…


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

view raw

CarpetQuote.cls

hosted with ❤ by GitHub

Now let’s move those functions to where they belong.


Option Explicit
Public Function Quote(ByRef room As Room, ByRef carpet As Carpet) As Double
Quote = carpet.Price(room.Area())
End Function

view raw

CarpetQuote.cls

hosted with ❤ by GitHub

Plus one for encapsulation, right? Well, not quite. Let’s take a look inside Room and Carpet.


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

view raw

Room.cls

hosted with ❤ by GitHub


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

view raw

Carpet.cls

hosted with ❤ by GitHub

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.


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.


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

view raw

RoomFactor.bas

hosted with ❤ by GitHub


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

view raw

Room.cls

hosted with ❤ by GitHub

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)


Option Explicit
Public Function Area() As Double
End Function

view raw

Room.cls

hosted with ❤ by GitHub

…and then have different implementations of Room that know about the details.


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


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?


Option Explicit
Public Function FlightsOfStairs() As Integer
End Function

view raw

FloorLevel.cls

hosted with ❤ by GitHub

A VB6 class can implement more than one COM interface.


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.


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