Note that after we create a variable of type car, we can access its
properties and methods through the drop-down completion box just as if
it had been a built - in class.
Note also that we do not deal with the class (Car) directly, but with
an INSTANCE of the class (myCar). This is familiar already.
We don't deal with the textbox class, but with individual instances of
it. (txtInput.text)
Polymorphism is often also used to allow one function to work on multiple
parameter types or lists. For example, the msgBox function you are used
to takes five parameters. Almost nobody uses all five. We frequently
will call this procedure with only one parameter. The ability to
still function with different types of parameters is a frequently seen
kind of polymorphism.
VB uses the variant data type to simulate polymorphism, and it supports
some other kinds of polymorphism as well.
Imagine the car class we discussed above. This would be quite useful on its own, as we could generate new instances of the class to make several different cars. However, what if we wanted to create a police car? It ought to start with the same characteristics as a regular car, but then should add some special features that relate to cop cars, such as a floodLightOn property and a soundSiren() method.
We could copy and paste all the features of the original car class to a new blank class, then add the new features, but this seems like a waste. It would be much more like the real world if we could take the car class 'straight from the factory' and work on only the modifications needed to make it a police car class. This is what inheritance promises. You can build new classes from existing classes, so you don't have to start from scratch every time.
Windows shows some evidence of inheritance in its design. If you
look closely at the VB controls, they tend to have very similar property
and event lists. In particular, the picture box and the form object
have strikingly similar lists of properties, events, and methods.
This is because they both inherit the same characteristics from a common
parent, the windows Rectangle class (which is hidden from us by VB).
Have you ever noticed that the command button has a backColor property,
but that changing the property has no effect? Why was this property
included? Because it was INHERITED from a parent class. The
default behavior (of allowing the background color to be changed) was over-ridden
by the Microsoft standard that command buttons are always the same color,
defined in the user settings.
VB shows evidence of Window's inheritance features, although it allows
very limited support for inheritance in the objects you create within the
language.
Examine the following module:
Type planeType
altitude As Integer
airspeed As Integer
direction As Integer
End Type
It is attached to this form:
...and here is the associated code:
Dim myPlane As planeType
Private Sub scrAlt_Change()
myPlane.altitude = scrAlt.Value
End Sub
Private Sub scrDirec_Change()
myPlane.direction = scrDirec.Value
End Sub
Private Sub scrSpeed_Change()
myPlane.airspeed = scrSpeed.Value
End Sub
Private Sub Timer1_Timer()
Dim temp As String
temp = ""
temp = temp + "airspeed:" + Str(myPlane.airspeed) + vbCrLf
temp = temp + "altitude:" + Str(myPlane.altitude) + vbCrLf
temp = temp + "direction:" + Str(myPlane.direction) + vbCrLf
lblOutput.Caption = temp
End Sub
Public Enum DirecTypes
NORTHWEST = 0
NORTH = 1
NORTHEAST = 2
EAST = 3
SOUTHEAST = 4
SOUTH = 5
SOUTHWEST = 6
WEST = 7
End Enum
Type planeType
altitude As Integer
airspeed As Integer
direction As DirecTypes
End Type
Now look at what happens when we attempt to assign a value to myPlane.direction from the editor:
as you can see, a list of legal DirecTypes values appears.
An enumeration is simply a special list of constants. It is used
in situations like this to protect the user from dealing with the actual
values and giving them an easier name to work with. Note that the
values are still integers, and we can use them as such (in fact we are
when we are assigning values from the scroll bars) but we also can think
in terms of these pre-defined constants.
Enums are very handy in this kind of situation, as you will see.
In the project menu, one of the choices is 'class module'. Let's add that sonofagun. Don't use the class file interface wizard yet!! We'll try it by hand, because we don't always trust the wizards. - Here's my first effort:
Public altitude As Integer
Public direction As DirecTypes
Public x As Integer
Public y As Integer
Public moving As Boolean
Private speedVal As Integer 'how many units to move each
time
Public Enum DirecTypes
NORTHWEST = 0
NORTH = 1
NORTHEAST = 2
EAST = 3
SOUTHEAST = 4
SOUTH = 5
SOUTHWEST = 6
WEST = 7
End Enum
Notice what I have done. I started with the Enum exactly as it was, and I added a set of public module - level variables. We have been ignoring the public/private declarations up to now, but they are important in this case. Note that I changed my variables a bit, but most of them are public. This means that the programmer who uses this object can get to the variable as a property. I'll save this as 'lazyPlane.cls' (because I'm making a plane, but I'm doing properties the lazy way). Now in my form, I can generate a variable of type lazyPlane, and set it to a new lazyPlane. Then when I try to access the variable, I will have access to its properties.
Public Sub turnRight()
Me.direction = Me.direction + 1
If Me.direction > 7 Then
Me.direction = 0
End If
End Sub
Public Sub turnLeft()
Me.direction = Me.direction - 1
If Me.direction < 0 Then
Me.direction = 7
End If
End Sub
Public Sub land()
If Me.altitude > 5 Then
MsgBox "you are too high to land"
Else
Me.moving = False
Me.altitude = 0
End If
End Sub
Public Sub takeOff()
Me.moving = True
Me.altitude = 5
End Sub
Public Sub move()
'analyzes direction, generates new values for x and y
If (Me.moving = True) Then
Select Case Me.direction
Case NORTH
Me.x = Me.x + 0
Me.y = Me.y + speedVal
Case NORTHEAST
Me.x = Me.x + speedVal
Me.y = Me.y + speedVal
Case EAST
Me.x = Me.x + speedVal
Me.y = Me.y + 0
Case SOUTHEAST
Me.x = Me.x + speedVal
Me.y = Me.y - speedVal
Case SOUTH
Me.x = Me.x + 0
Me.y = Me.y - speedVal
Case SOUTHWEST
Me.x = Me.x - speedVal
Me.y = Me.y - speedVal
Case WEST
Me.x = Me.x - speedVal
Me.y = Me.y + 0
Case NORTHWEST
Me.x = Me.x - speedVal
Me.y = Me.y + speedVal
End Select
End If
End Sub
If we try to use this object in a form, you will see that the properties
and methods do show up.
Now, we are beginning to have what can be considered an object.
Let's take a closer look at what is happening in the code above:
Remember, we considered variable scope to be a part of encapsulation. We are interested in keeping our variables as local as possible. In a class declaration, a public variable is WIDE open, and can be changed by anybody, with very unpredictable results. We do not want this behavior. Somehow, we need to protect our properties so they cannot be changed without some degree of monitoring. Also, some properties (like direction, perhaps) ought to be read-only. That is, a program that uses the class can read the property value, but cannot set it on the fly.
One solution is to have all of the class level variables (often called instance variables) listed as private. This is much safer than having them public, but now they are not accessible as properties.
In Java, it is common to have all instance variables set as private. The class designer then uses methods to allow access to the instance variables. In essence, such a class would have no properties at all, just methods. The methods could be used to set the properties, but the properties could not be set directly. A set of these methods for changing 'property' values is called 'setters' and another set designed to retrieve these values is called 'getters'.
In VB, we can define special methods very much like these setters and getters, and use them to generate properties.
Here's the key idea:
Properties are really methods acting on invisible private instance variables!!!
(No need to shout!)
To the user of the class, the properties will feel just like the public variables we have seen, but there are some important distinctions.
Here's another version of the Aircraft class general area (or part of it, anyway)
'local variable(s) to hold property value(s)
Private mvaraltitude As Integer 'local copy
Private mvardirection As DirecTypes 'local copy
Private mvarx As Integer 'local copy
Private mvary As Integer 'local copy
Private mvarmoving As Boolean 'local copy
Private speedVal As Integer 'how many units to move each
time
Notice that all my instance variables are now private and their names have been changed to mvarsomething. We'll talk about that in a minute.
Here's a new set of methods I have added:
Public Property Get direction() As DirecTypes
'used when retrieving value of a property, on the right side of
an assignment.
'Syntax: Debug.Print X.direction
direction = mvardirection
End Property
Public Property Let direction(ByVal vData As DirecTypes)
'used when assigning a value to the property, on the left side
of an assignment.
'Syntax: X.direction = 5
if (vData < 0) or (vData > 7) then
msgBox "Illegal value! Setting
direction to NORTH"
mvardirection = NORTH
else
mvardirection = vData
end if
End Property
The property get procedure returns back the value of mvardirection, but does not allow the variable to be changed directly. If I wanted to have a read-only property, I would give it a property get procedure without a property let.
The property Let procedure is far more interesting, and illustrates why properties are sometimes better than public instance variables. When direction was public, the calling program could set direction to ANY integer value, including illegal values such as -1 and 8. There was no way to prevent this.
I added error-checking code to the Let procedure, which accepts a value in a temporary variable called vData. I then checked vData with an if statement to ensure the value was legal. If not, I warned the user with a messagebox (great during debugging, but probably not such a swift idea in a final release) and set the value to something legal. It is now impossible to have an illegal value in direction.
Note that my original code for turnLeft and turnRight will no longer
work, because they incremented the property to illegal values before
testing them. I now have to change them slightly, but I'm glad, because
this means I can trust this property to always have a legal value.
Here's the new code for turnLeft():
Public Sub turnLeft()
If Me.direction > 0 Then
Me.direction = Me.direction - 1
Else
Me.direction = 7
End If
End Sub
This is very convenient, but you may still need to edit the code by hand.
Finally, here is the complete code for the Aircraft class, as generated
by the
class builder and modified by me:
'aircraft class
'a simple class file designed to model
'an aircraft in an Air Traffic Control simulation
'Andy Harris, 2/99
'local variable(s) to hold property value(s)
Private mvaraltitude As Integer 'local copy
Private mvardirection As DirecTypes 'local copy
Private mvarx As Integer 'local copy
Private mvary As Integer 'local copy
Private mvarmoving As Boolean 'local copy
Private speedVal As Integer 'how many units to move each
time
Public Enum DirecTypes
NORTHWEST = 0
NORTH = 1
NORTHEAST = 2
EAST = 3
SOUTHEAST = 4
SOUTH = 5
SOUTHWEST = 6
WEST = 7
End Enum
Public Sub turnRight()
If Me.direction < 7 Then
Me.direction = Me.direction + 1
Else
Me.direction = 0
End If
End Sub
Public Sub turnLeft()
If Me.direction > 0 Then
Me.direction = Me.direction - 1
Else
Me.direction = 7
End If
End Sub
Public Sub land()
If Me.altitude > 5 Then
MsgBox "you are too high to land"
Else
Me.moving = False
Me.altitude = 0
End If
End Sub
Public Sub takeOff()
Me.moving = True
Me.altitude = 5
End Sub
Public Sub move()
'analyzes direction, generates new values for x and y
'expects CARTESIAN coordinates (Y is larger at top of page)
If (Me.moving = True) Then
Select Case Me.direction
Case NORTH
Me.x = Me.x + 0
Me.y = Me.y + speedVal
Case NORTHEAST
Me.x = Me.x + speedVal
Me.y = Me.y + speedVal
Case EAST
Me.x = Me.x + speedVal
Me.y = Me.y + 0
Case SOUTHEAST
Me.x = Me.x + speedVal
Me.y = Me.y - speedVal
Case SOUTH
Me.x = Me.x + 0
Me.y = Me.y - speedVal
Case SOUTHWEST
Me.x = Me.x - speedVal
Me.y = Me.y - speedVal
Case WEST
Me.x = Me.x - speedVal
Me.y = Me.y + 0
Case NORTHWEST
Me.x = Me.x - speedVal
Me.y = Me.y + speedVal
End Select
End If
End Sub
Public Property Let moving(ByVal vData As Boolean)
'used when assigning a value to the property, on the left side
of an assignment.
'Syntax: X.moving = 5
mvarmoving = vData
End Property
Public Property Get moving() As Boolean
'used when retrieving value of a property, on the right side of
an assignment.
'Syntax: Debug.Print X.moving
moving = mvarmoving
End Property
Public Property Let y(ByVal vData As Integer)
'used when assigning a value to the property, on the left side
of an assignment.
'Syntax: X.y = 5
mvary = vData
End Property
Public Property Get y() As Integer
'used when retrieving value of a property, on the right side of
an assignment.
'Syntax: Debug.Print X.y
y = mvary
End Property
Public Property Let x(ByVal vData As Integer)
'used when assigning a value to the property, on the left side
of an assignment.
'Syntax: X.x = 5
mvarx = vData
End Property
Public Property Get x() As Integer
'used when retrieving value of a property, on the right side of
an assignment.
'Syntax: Debug.Print X.x
x = mvarx
End Property
Public Property Let direction(ByVal vData As DirecTypes)
'used when assigning a value to the property, on the left side
of an assignment.
'Syntax: X.direction = 5
If (vData < 0) Or (vData > 7) Then
MsgBox
"vData is " + Str(vData)
MsgBox
"Illegal value! Setting direction to NORTH"
mvardirection
= NORTH
Else
mvardirection
= vData
End If
End Property
Public Property Get direction() As DirecTypes
'used when retrieving value of a property, on the right side of
an assignment.
'Syntax: Debug.Print X.direction
direction = mvardirection
End Property
Public Property Let altitude(ByVal vData As Integer)
'used when assigning a value to the property, on the left side
of an assignment.
'Syntax: X.altitude = 5
mvaraltitude = vData
End Property
Public Property Get altitude() As Integer
'used when retrieving value of a property, on the right side of
an assignment.
'Syntax: Debug.Print X.altitude
altitude = mvaraltitude
End Property
Private Sub Class_Initialize()
speedVal = 5
Me.direction = NORTH
End Sub
Public Property Get propName() As type
where propName is the name of the property and type is the variable type of the property. A get procedure acts much like a function. It returns back the value of the property. When the property is on the right-hand side of an assignment statement, the get procedure is being accessed.
You rarely have to modify the code as it comes out of the class builder. it will simply return the value of the private instance variable.
Almost every property will have a get procedure. If it does not need to be retrieved by the user, it should probably be a private instance variable rather than a property.
Public Property Let propName(ByVal vData As type)
where again propName is the name of the property and type
is a variable type.
Let procedures act more like subprograms with a parameter. They
accept a value and temporarily store it in vData. The ByVal
keyword indicates that if a variable is passed to the procedure, we will
work on the value of that variable, over-riding VB's natural inclination
to work on a reference to that variable instead.
The code essentially copies the value in vData (a temporary variable) over to the private instance variable. You will almost always want some error checking in your let procedures, to ensure that only appropriate values get sent to your instance variables, and that your property cannot be given a value it cannot deal with.
One common strategy is to make vData a variant type, and then contain code to convert vData into the appropriate type before assigning it to the private variable. It is also common to do some bounds checking to ensure that the values fall in some pre-defined range.
If you want to have a read-only property, remove the property let procedure associated with that property.
' frmATC
'An air traffic control simulation
'designed to demonstrate OOP in VB
'andy Harris, 2/99
'create an aircraft instance
Dim plane As New Aircraft
Private Sub cmdHigher_Click()
plane.altitude = plane.altitude + 5
End Sub
Private Sub cmdLand_Click()
Dim direcOK As Boolean
'check the direction
'this airport has Runways going N, S, E, and W,
'but NW, NE, SW, SE not legal directions
direcOK = False
If plane.direction = NORTH Then
direcOK = True
ElseIf plane.direction = SOUTH Then
direcOK = True
ElseIf plane.direction = EAST Then
direcOK = True
ElseIf plane.direction = WEST Then
direcOK = True
End If
'ensure plane is on airport
If imgPlane.Left > imgAirport.Left Then
If imgPlane.Left < (imgAirport.Left + imgAirport.Width
- imgPlane.Width) Then
If imgPlane.Top < imgAirport.Top
Then
If imgPlane.Top > (imgAirport.Top
- imgAirport.Height + imgPlane.Height) Then
plane.land
MsgBox "Nice
Landing"
Else
MsgBox "Too
far South!"
End If
Else
MsgBox "too far North!"
End If
Else
MsgBox "too far East!"
End If
Else
MsgBox "too far West!"
End If
End Sub
Private Sub cmdLeft_Click()
plane.turnLeft
End Sub
Private Sub cmdLower_Click()
'go lower, but don't allow them to crash
If plane.altitude <= 5 Then
MsgBox "You cannot go lower than 500 feet!!"
Else
plane.altitude = plane.altitude - 5
End If
End Sub
Private Sub cmdRight_Click()
plane.turnRight
End Sub
Private Sub cmdTakeOff_Click()
plane.takeOff
End Sub
Private Sub Form_Load()
'initialize the form to a normal cartesian system
frmATC.Scale (-100, 100)-(100, -100)
'tell the OBJECT where the image box is
plane.x = imgPlane.Left
plane.y = imgPlane.Top
End Sub
Private Sub Timer1_Timer()
Dim temp As String
'move the OBJECT
plane.move
'set up the image box so it has the right picture
imgPlane.Picture = imgStore(plane.direction).Picture
'also refer to the object for the location
imgPlane.Left = plane.x
imgPlane.Top = plane.y
'output some stuff
temp = "X: " + Str(plane.x)
temp = temp + " Y: " + Str(plane.y)
temp = temp + " Alt: " + Str(plane.altitude) + "00
ft"
lblOutput.Caption = temp
End Sub
The full code is available here: