Simulating Object Oriented Programming in Visual Basic

Compuserve Text posted by Pete Washburn (73750,3141) January 1994

Here's some of the techniques I've used to emulate an OOP language with QB and VB.  I must add a couple of comments however.  I'm not saying that Basic is an OOP, but with a few techniques, you can get most of the principles of OOP into Basic.  These are just a few of techniques I've used. They may not be the most efficient or optimum methods available, but they seem to be working for me. Improvements are always accepted.  Debates about  the finer parts of OOP theory are not!  I do a fair amount of work with system simulation, so I'll use a segment of one of my projects to demonstrate.
Most OOP's contain a class tree to define the behavior of objects within the program.  In this demo case, we are modeling a plumbing system, with pipes, hoses, and valves.  The class tree may look as follows:
WaterPart
    Pipe
        Hoses
    Valves
    Pumps

We'll look at the WaterPart class.  It is defined as a Function as follows:
Function WaterPart (hObj, msg, value)

' all data for objects of this type is contained within the Function, 
' within the following instance array
    Static WaterPartInstances() As waterPartObject

' count of individual objects of this type
    Static WaterPartsCount%

' using the hObj passed, find the object's data in the Instance array 
' (there are a lot more sophisticated and efficient ways to do this, 
' used as an example only)
    For idx = 1 To WaterPartsCount%
        If WaterPartsInstances(idx).hObj = hObj Then
            Exit For
        End If
    Next idx

' methods
    Select Case msg
        Case GET_QUANTITY
            WaterPart = WaterPartInstances(idx).quantity
        Case SET_QUANTITY
            If value <> WaterPartInstances(idx).quantity Then
                If WaterPartInstances(idx).diameter = 0 Then
                    value = 0
                End If
                WaterPartInstances(idx).quantity = value
            End If
            WaterPart = WaterPartInstances(idx).quantity
        Case GET_DIAMETER
            WaterPart = WaterPartInstances(idx).diameter
        Case SET_DIAMETER
            If value <> WaterPartInstances(idx).diameter Then
                WaterPartInstances(idx).diameter = value
                WaterPartInstances(idx).area = .7854 * value ^ 2
            End If
            WaterPart = WaterPartInstances(idx).diameter
        Case GET_AREA
            WaterPart = WaterPartInstances(idx).area
        Case NEW_OBJECT
            WaterPartsCount% = WaterPartsCount% + 1
            idx = WaterPartsCount%
            WaterPartInstances(idx).hObj = hObj
            WaterPart = Self(hObj, INIT_OBJECT, value)
        Case INIT_OBJECT
            WaterPart = Self(hObj, SET_DIAMETER, value)
        Case DISPLAY_OBJECT
            ' code to display the part
        Case READ_OBJECT
            ' code to read info about the part from a file
        Case WRITE_OBJECT
            ' code to write part info to a file
        Case PRINT_OBJECT
            ' code to print the part to a printer
        Case INIT_CLASS
            ReDim WaterPartInstances(1 To MAX_WATERPARTS) As waterPartObject
        Case Else
            a = "Object Doesn't Understand Message." + Chr$(10) + Chr$(10)
            a = a + "Class:         Part" + Chr$(10)
            a = a + "Object:       " + Str$(hObj) + Chr$(10)
            a = a + "Message:  " + Str$(msg) + Chr$(10)
            a = MsgBox(a, 16, "Message Error")
    End Select 
End Function

Because VB won't let us define user Types within a Function, the Declarations section of our program describes the Type variable that will contain the object's information.
    Type waterPartObject
       hObj as Integer
        diameter As Single
        area As Single
        quantity As Integer
    End Type

Also, constants used by our VB program must be declared in the Declarations section.
' define messages sent to objects
    Global Const NEW_OBJECT = 1
    Global Const INIT_OBJECT = 2
    Global Const DISPLAY_OBJECT = 3
    Global Const READ_OBJECT = 4
    Global Const WRITE_OBJECT = 5
    Global Const INIT_CLASS = 6
    Global Const GET_QUANTITY = 100
    Global Const SET_QUANTITY = 101
    Global Const GET_DIAMETER = 102
    Global Const SET_DIAMETER = 103
    Global Const GET_AREA = 104

' define max number of water parts
    Global Const MAX_WATERPARTS = 30

A language is considered object oriented if it supports three major features:
       1.  Encapsulation (or information and implementation hiding)
       2.  Inheritance.
       3.  Polymorphism
The Function helps us with Encapsulation.  Both the data and the methods that work with the data are contained solely within the Function.  The Type we defined contains all of the instance variables of an object.  An array is created to hold the instance variables of each object of the class.  There are many different ways to find the instance data within the array for a specific object.  In this example, we simply do a brute search for the key, hObj within the array.

The data is not globally defined, so the only way to access an object's data is by the methods that are defined within the Function.  We work with an object by passing the handle of the object (hObj) to the Function along with the message that we want and any additional we need to provide.  To get the area of the particular part, anObj,  we would send the following:
       area = WaterPart(anObj, GET_AREA, Null)

Similarly, to set a new diameter, we would send the following message:
       returnValue = WaterPart(anObj, SET_DIAMETER, 4.55)

One difference between this technique with VB and a real OOP is that the number of parameters sent to a Function is constant.  In a real OOP, each method would be defined separately, with the specific number of parameters it needed. Because we're wrapping our class and all of its methods within one Function, the number of parameters is fixed.  Therefore, each message consist of three parameters, even if you don't need all three.  Simply send a Null for any parameter that isn't needed.  Likewise, there might be some cases that need more than the three parameters specified.  With QB, I had defined the third parameter as a string and encoded all information I needed into that string. The Function then parsed out the info as it needed it. VB is a lot more flexible in this regard.  I haven't tried it yet, but by defining the third parameter as being of the Variant type, I believe you can send any type of variable you wish as the third parameter.  Perhaps even a user defined Type or array could be sent as the third parameter.
Likewise,  QB required that you defined the variable type that the Function returned.  As a result, I defined all the class functions as being strings. That way, I could encode any variables being returned from the class function into the string.  A VB Function again is more flexible, as it doesn't require you to define the variable type it is, and you can return any type of variable you want back from the function.
All of this allows us the advantages of Encapsulation and information and implementation hiding that OOP's normally provide with VB.
The second major feature of an OOP is inheritance.  We can easily model that within VB.  Let's define another class, Pipe, that is a descendant of the WaterPart class we've already defined.
Function Pipe(hObj, msg, value)

    Static pipeInstances() As pipeObject
    Static pipeCount%

' find instance data for this object
    For idx = 1 To hoseCount%
        If pipeInstances(idx).hObj = hObj Then
            Exit For
        End If
    Next idx

' methods
    Select Case msg
        Case GET_LENGTH
           Pipe = pipeInstances(idx).length
        Case SET_LENGTH
            If value <> pipeInstances(idx).length Then
                pipeInstances(idx).length = value
                pipeInstances(idx).volume = Pipe(hObj, GET_DIAMETER, Null)
            End If
            Pipe = pipeInstances(idx).length
        Case GET_VOLUME
            Pipe = pipeInstances(idx).volume
        Case SET_DIAMETER
            diameter = WaterPart(hObj, msg, value)
            pipeInstances(idx).volume = diameter * pipeInstances(idx).length
        Case Else
            ' message not handled by this class, pass on to ancestor
                Pipe = WaterPart(hObj, msg, value)
    End Select
End Function

This has been simplified quite a lot, but basically, the only difference Pipe objects have from the parent class, WaterPart is that Pipes have a length in addition to a diameter.  Therefore this class has to process all info in regards to the length of the Pipe.  Note the Case Else statement though.  If the message hasn't been handled by this class, it is passed on to it's ancestor, which is WaterPart in this case.  Note that in the WaterPart class function, the Case Else statement has an error trap as the WaterPart class doesn't have an ancestor class, it is a top level class.
Notice that the Pipe class has a SET_DIAMETER method.  This overrides the ancestor WaterPart classes method.  Actually, it does call it first and then recalculates the volume within the pipe.  This brings up a very significant point, if the class sends a message to itself, it needs to be concerned that any overridden methods in its descendants get a chance to process the message first.  This is handled within my OOP techniques by calling a special function, Self.  Notice in the WaterPart class, that both NEW_OBJECT and INIT_OBJECT send messages to Self.  Here's the code for the Self object.
' route a message to the appropriate class of the object

Function Self (hObj, msg, value)

    ' find the class of the object by looking it up in the object
    ' class array
        Select Case objectClass%(hObj)
            Case WATERPART_CLASS
                Self = WaterPart(hObj, msg, value)
            Case PIPE_CLASS
                Self = Pipe(hObj, msg, value)
        End Select

End Function

objectClass%() is an array that contains an entry for each object in the program designating what class the object is.  Self() simply redirects the message to the appropriate class for the object specified by hObj.  Here are the class designators as listed in the Declarations section of the program.
' class identifiers
    Global Const WATERPART_CLASS = 1
    Global Const PIPE_CLASS = 2

This gives us most of the late binding features of OOP's.  Its not as elegant or automatic as it is in most OOP's, but it does work.  Just make sure when you create an object, you list its class in the objectClass%() array.  A similar array could also be created to list the ancestor class of the object.
The last major characteristic to be modeled is polymorphism.  This means that there might be several messages that are system wide and that all (or most) classes can respond to.  In our example, messages such as NEW_OBJECT, INIT_OBJECT, DISPLAY_OBJECT, READ_OBJECT, WRITE_OBJECT, PRINT_OBJECT, and INIT_CLASS are examples of polymorphic messages that most classes implement. The sender of the message doesn't need to know about the class, it simply sends the message. The class handles the details.  The Self() function is again used to when sending a message to an object in these cases, as the sender doesn't know even the class of the object that it is sending the message to.
Well, that's most of the techniques I've developed for making QB and VB more like a pure OOP.  I'm not proposing these techniques as being the best or only way to program!  They are simply the techniques that I have developed to provide most of the features I had with Actor in QB and VB.  As I work with them, I am refining them and making them more efficient.  But the overhead and code to make it work is more complex than it would be if this was a pure OOP.
