All objects are defined by a class. A class is essentially a template from which the object is created, which means that we can create many objects based on a single class. Each object is referred to as an instance of the class.
The word class is pretty descriptive, since we're basically classifying our objects. For instance, if we have a couple of objects, Squirrel and Rabbit, they could be instances of a Mammal class.
To use a more business-like example, we may have a Customer class. From this class we could create instances to represent each our customers, such as Fred Smith and Mary Jones. Each actual customer would have its own object, but all the objects would be created from the same template, the Customer class:
In Visual Basic, we define classes using class modules. We can then create objects based on the class module, with each object being an instance of the class.
Class modules are like form or standard modules, in that they have a declaration section followed by a series of Sub
, Function
and Property
subroutines. The difference is that the variables and code in a class module can only be used by creating an instance of the class.
Let's take a look at how Visual Basic's class modules are used to provide a template for our objects.
All objects contain data about themselves. For instance, an object representing a simple cardboard box might have data about the box's height, width, and depth. The object might also know whether the box is open or closed. Furthermore, we might have many box objects, each with its own height, width and depth. Each of these objects could be an instance of a Box class.
Data values in objects are stored in instance variables. Each instance of an object has its own set of variables, separate from the variables of any other object, even those of the same class. In this way, two objects of the same class can each have their own individual data, so each of our earlier box objects can have its own height, width, and depth.
In Visual Basic, a class's instance variables are stored as module-level variables inside a class module. For example:
Option Explicit
Private dblHeight As Double
Private dblWidth As Double
Private dblDepth As Double
Private blnOpen As Boolean
As with any other module, module-level variables in a class module can be declared as either Public
or Private
.
Private
variables are only accessible to code within the class module, and they are the preferable way to manage data within objects. For any client code to manipulate a Private
variable, it must call our object's code to do the manipulation; it can't do anything directly. This gives us a lot of control, since we can implement business or validation rules against any values that the client code attempts to change.
Instance variables declared as Public
are accessible to code within our object, but they are also directly accessible to any client code written to use our object. Use of Public
instance variables is very bad practice. Any client code can directly manipulate a Public
instance variable, without going through any of our object's code. This means our object gives up all control over the value, trusting the author of the client code not to break any rules or do anything that is invalid.
One of the primary principles of object design is encapsulation, a concept we'll explore throughout the rest of the book. Encapsulation means that objects should hide their internal design and data from the code that uses them.
Private
variables help by forcing any client code to use our code to change our object's data, thus allowing us to change how the object is implemented at any time with little or no impact on other programs that use this object. Public
variables directly break the principal of encapsulation, providing a client with direct and uncontrolled access to an object's data.
Of course, the whole idea behind having an object is so our programs can interact with it. If we're encapsulating an object's data then we need to provide the ability for the client programmer to access the information that the object holds.
Objects need to provide an interface to allow their client programs to gain access to these services. Just as in the real world, where objects interact - it is our aim in programming with objects to facilitate and control their interaction - via properties, methods and events. These form the interface the object presents to the world.
In Visual Basic, an object's interface is composed of Property
, Sub
and Function
routines, as well as any events declared using the Event
keyword. Any of these routines declared as Public
within the class module become part of the object's interface.
In reality, an object can have many interfaces, allowing client code to use the object in different ways as appropriate. We'll cover this in more detail in the next section.
A property is an attribute that describes the object. Objects often have many properties to provide client code with access to all of their attributes.
It's important to make that an object's properties are not the same as its data. Objects often contain data that would not be considered to be an attribute of the object. For instance, a Person object may have an Age property, but the underlying data might be a birth date. The birth date itself may or may not be available as a property of the object.
Properties allow us, as the object's designer, to pick and choose what information we want to make available about our object. We can also choose which property values can be changed by our object's users. Some of that information may come directly from our object's data, while other information may come from calculated values or other sources.
So in our earlier box example we may not care to let other programs see the box's dimensions, but we may want them to be able to get at the box's volume:
Public Property Get Volume() As Double
Volume = dblHeight * dblWidth * dblDepth
End Property
Visual Basic provides us with three different types of Property
routines. Property Get
routines are used to retrieve a property from an object. Then there are two routines to put values into a property, depending on whether the value is a reference to an object or not. Property Let
routines allow client code to put any value into a property other than a reference to an object, while Property Set
routines are used only for object references.
Properties can be read-only (Property
Get
), write-only (Property
Let/Set
) or read-write (both properties). Even if we include a Property
Let
or Property
Set
routine for a property, we can include code in that routine to validate the new value or check any other business rules before allowing the client code to change the value.
Objects, like their real-world counterparts, need to provide services (or functions) when they interact. Using their own data, or data passed as parameters to the method, they manipulate information to yield a result.
Methods are simply routines that we code within the class to implement the services we want to provide. Some methods return values or provide information back to the calling code. These are called interrogative methods. Others, called imperative methods, just perform a service and return nothing to the calling code.
In Visual Basic, methods are implemented using Sub
or Function
routines within the class module that defines our object. Sub
routines may accept parameters, but they do not return any result value when they are complete. Function
routines can also accept parameters, and they always generate a result value that can be used by the calling code.
The difference between a Function
routine and a Property Get
routine is quite subtle. Both return a value to the calling code and, either way, our object is running a subroutine defined by our class module to return the value.
The difference is less programmatic than it is a design choice. We could create all our objects without any Property
routines at all, just using methods for all interactions with the object. However, Property
routines are obviously attributes of the object, while a Function
might be an attribute or a method. By carefully implementing all attributes as Property Get
routines, and any interrogative methods as Function
routines, we will create more readable and understandable code.
Using our now familiar box example, programs may need to open or close the box - so we need to provide a way to do this:
Public Sub OpenBox()
blnOpen = True
End Sub
Public Sub CloseBox()
blnOpen = False
End Sub
With this approach, our client code might look like this:
Dim objBox As New Box
objBox.Open
objBox.Close
Where this gets more useful is when we add code to check our rules. We shouldn't be able to open an already open box, or close an already closed box. With absolutely no impact to our client code, we can add the following rules to the Box
class:
Public Sub OpenBox()
If Not blnOpen Then Err.Raise vbObjectError, _
"Box already open"
blnOpen = True
End Sub
Public Sub CloseBox()
If Not blnOpen Then Err.Raise vbObjectError, _
"Box already closed"
blnOpen = False
End Sub
In this case, we're simply raising an error if the client code tries to call either the OpenBox
method when the box is already open or the CloseBox
method when it's already closed. The real key, here, is that the implementation of these methods is totally encapsulated within our class module. We were able to make these changes without having to make any changes to our client code.
In object-oriented design, the concept of an event is pervasive. In OO parlance, objects act in response to events, such as a user providing a value, or another object calling one of our object's methods. In the Visual Basic 5.0 environment, we have access to a somewhat different type of event.
Any Visual Basic developer is used to writing code to respond to events. Controls on forms generate events continually, and we write code behind those events to take appropriate action. For instance, a CommandButton
control provides a Click
event, firing it any time the user clicks on the button. We then write code to respond appropriately:
Private Sub Command1_Click()
MsgBox "The user clicked the button"
End Sub
Before Visual Basic 5.0, there was no way for a Visual Basic developer to cause events, only to respond to them. Now we can use the Event
statement to declare an event and the RaiseEvent
command to fire it off.
Going back to our Box
class, let's allow the user to stretch our box by implementing a Stretch
method:
Public Sub Stretch(Percentage As Double)
dblHeight = dblHeight * Percentage
dblWidth = dblWidth * Percentage
dblDepth = dblDepth * Percentage
End Sub
As a convenience, perhaps we want to raise an event to let any client programs know that our box has a new size. Visual Basic makes this very easy. We just need to add an Event
declaration in the (General) (Declarations) section of our Box
class module:
Event Stretched()
Events are always Public
in scope, since they're always raised back to whatever client code has a reference to our object.
With our event declared, we can now fire it off using the RaiseEvent
command:
Public Sub Stretch(Percentage As Double)
dblHeight = dblHeight * Percentage
dblWidth = dblWidth * Percentage
dblDepth = dblDepth * Percentage
RaiseEvent Stretched
End Sub
We'll go into much more detail about events in Chapter 3.
So far, we've looked at an object as having a single interface composed of its Public
Sub
, Function
, and Property
routines, along with any events it has declared. With Visual Basic 5.0, things are a bit more complex. Our objects can actually have many different interfaces simultaneously, although a client can only use one at a time.
If we create an object from a class we built in Visual Basic, that object will have at least the one interface we defined in our class module. In this simple case, it appears as though our program has direct access to the object, although in reality we're getting at the object through an interface defined by the class:
With Visual Basic 5.0, a client program never has direct access to an object, only to one of the object's interfaces.
Sometimes, one interface isn't enough. Our object may represent more than one real world entity, and we need to be able to model this in Visual Basic. For instance, a Customer object is great, but a real customer is also a person. To model this in our application, we might need our Customer object to also act like a Person object from time to time.
To accomplish this, we can just add the Person interface to our object. Then a client program can use our object through the Customer interface with its set of properties and methods. It might also use the object through the Person interface, with its own separate set of properties and methods. This way, the client can use the object in whichever way is appropriate at any given time:
As we've seen, when we create a class in Visual Basic, its Public
members comprise the interface for our object. However, we can also use the Implements
statement to add another interface to our object. The Implements
keyword requires that we supply a class name with which it will work.
With our Box
example, for instance, we might also include an interface for a more generic Container
class. To do this, we'd need to add an Implements
statement in the general declarations section of our Box
class module:
Implements Container
As soon as we add this line, Visual Basic will require that we add code to our class module to implement all the Public
properties and methods from the Container
class. Suppose the Container
class module contains a single method, called PutIn
, and a property, called IsOpen
:
Public Sub PutIn(Item As Object)
End Sub
Public Property Get IsOpen() As Boolean
End Property
The property and method may or may not have code in them. As I've shown them in this example, there's no code. It doesn't really matter to our Box
class, since we need to add our own code in the Box
class module regardless. For instance, the Box
class's implementation of PutIn
may add any new items into a Private
collection variable, and IsOpen
may just return our blnOpen
flag:
Option Explicit
Implements Container
Private colItems As New Collection
Private dblHeight As Double
Private dblWidth As Double
Private dblDepth As Double
Private blnOpen As Boolean
Public Sub OpenBox()
If Not blnOpen Then Err.Raise vbObjectError, _
"Box already open"
blnOpen = True
End Sub
Public Sub CloseBox()
If Not blnOpen Then Err.Raise vbObjectError, _
"Box already closed"
blnOpen = False
End Sub
Private Sub Container_PutIn(Item As Object)
colItems.Add Item
End Sub
Private Property Get Container_IsOpen() As Boolean
Container_IsOpen = blnOpen
End Property
There are a couple interesting things about these new routines we've added. First off, they are declared as Private
routines, so at first glance they don't appear to be available to any client code. This is deceptive, however, since they are available to clients through the Container
interface.
If these routines were declared as Public
, they'd be part of the Box
interface. That's not what we are after though. Instead of declaring them as Public
, we declare them as Private
, and then indicate that they belong to the Container
interface by putting Container_
in front of the name of the routine.
We'll discuss multiple interfaces in more detail later in the book.