When I first started doing OOP, I went through far too many iterations of "Ahh, NOW I get it" only to quickly realise that I still didn't. In many ways, it is easier to learn OOP if you have never programmed as there is less unlearing to do.
However, OOP can be very useful in attacking many types of problems so the effort involved is worthwhile. And sticktoitiveness does pay off – you will grok it in the fullness of time.
Around 1996 (I think) I developed some VB4 courseware and I needed a gentle introduction to OOP for an audience of primarily VB3 programmers. The following is what I came up with way back then.
Please note that some of the material is dated and does not apply to the current versions of VB. However, I felt that it might be useful for some people here so I "UBB"ed it and posted it below.
I hope it helps somebody.
One of the best new features introduced Version 4 of Visual Basic is some support for basic object-oriented programming (OOP). While there has been wide criticism from OOP purists that VB has stopped short of full support for object orientation, those facilities that Microsoft did implement are a huge improvement on previous versions.
It is important to take a moment to review why OOP is A Good Thing. In the late 1980s and early 1990s, there was a lot of hype about how OOP was going to allow us to eliminate the application backlog. Reading the popular press, it was easy to get the impression that OOP was some kind of magic bullet that was going to cure the world of all known evils.
Needless to say, that did not eventuate. However, this does not mean that OOP has failed; it only indicates that the journalists of the time were reporting on concepts that they generally knew nothing about. Plus ça change, plus ça même chose.
OOP can provide a number of very real benefits to software development. Actually realising these benefits requires an up-front investment of time and effort to design and build re-usable objects. Simply using an OOP-capable language, even using OOP techniques, will not in and of itself gain you any more code re-use than you would have achieved without it. However, if you do decide to invest the time and effort, you will find that OOP environments will provide you with tools to make that re-use more convenient and comfortable and (maybe) a little faster.
So by all means look at these features of Visual Basic 4 and start educating yourself about OOP techniques; the benefits are there if you want them. Just don’t expect instant miracles.
What is an object?
If you are new to the world of OOP, your first question is likely to be "what, exactly, is an object?".
In OOP terms, an object is a unit that binds some data with the code that acts on it. It has some similarities to both user-defined types (UDTs, or records if you prefer) and to code libraries.
In a non-OOP environment, we tend to separate data from code. We declare some variables and then we write code that manipulates those variables. Modern languages have multiple scopes of visibility for variables, making it possible to partition some sets of variables so that they can only be accessed from some parts of the code. Controlling access to variables using scope rules is considered a good thing, the general wisdom being that you should keep variables defined at the narrowest possible scope so that you minimise the chances of a variable being corrupted by code that does not know how it should be used.
OOP simply takes that encapsulation to the next level. Rather than simply controlling access to data using scope, OOP introduces an object which is some data and the code that manipulates that data bound into one unit. If you like, it is smart data, data that knows how to do things.
An example might help to clarify things somewhat. In a non-OOP environment, you might define a data structure to represent the attributes of something in the real world. Let’s say you want to keep track of customers. You would design a customer record that included fields like the name, address, telephone number, current credit limit, current balance and whatever else you needed to record. You would also write programs that made use of that record. To make this easier, you would probably create common routines to create a new customer, find a customer by name, calculate a new balance and so on; you get the idea.
Now, this is important, so listen up. When we said before that you created a customer record, that was not really accurate. What you really did was define a customer record type. Later, you would create one or more instances of that record type, which would be the actual records your code would manipulate. That distinction might seem trivial now, but it is fundamental to understanding how OOP works so if you don’t understand it then please review this section before proceeding.
You can ensure that the procedures you write are passed instances of the correct record type because most modern languages will signal an error at compile time if you try to pass, say, an invoice record when a customer record is expected. What you cannot do is protect the data in the record because that data is in an instance of the record that is typically owned by the caller. There is nothing stopping the calling process from directly setting the credit limit to a new value rather than calling the appropriate function. If that function did some validation, or if it updated some other files, then that has been bypassed.
This is the problem with non-OOP approaches. The procedures can be made to work only with the right types of data structures, but the data structures cannot be limited to be acted upon only by the right procedures. Scope rules can help somewhat but they cannot address all the issues.
In an OOP environment you would define an object type instead of a record type. When defining the customer object type, you would not only have declared a set of data fields to store the appropriate data items but also a set of procedures that were to act on those fields. The definition of the procedures would be done right in the object definition; the data structure design and the code is bound together and is collectively known as the object type.
Remember, this is only an object type. You would later create perhaps multiple instances of those objects for your program to use. Each instance of the object would have its own, distinct set of data fields just as each instance of a record does. Each instance of the object also contains knowledge of what procedures can operate on that data.
Another very important aspect of OOP is polymorphism, which is a fancy work that means that the same statement can achieve different results based on what type of object it relates to. Polymorphism is typically tightly bound with and implemented by inheritance, a mechanism whereby we can create a hierarchy of object types where some objects are more-specialised versions of others and are built by inheriting the general behaviour and modifying or expanding it as required.
Visual Basic does not support inheritance and this has been the major criticism leveled at it by OOP purists. It is not appropriate to buy into the debate here. Certainly, it would be nice to have support for inheritance in VB but the fact is that it is not there. That does not mean that the other parts are useless.
Visual Basic does, however, support polymorphism. It does this by using the OLE Automation interface. Essentially, objects are polymorphic with each other if they have the same set of procedures. (To be more accurate, two objects are polymorphic for any given procedure if they both implement that procedure and both procedures take the same parameters.)
Again, a rather silly example might clarify this. Let’s say we have a Dog object and a Cat object. Both objects implement a "Speak" procedure. The Speak procedure for the Dog object prints the word “Woof” and the Speak procedure for the Cat object prints the word “Miaow”. Here is how those objects would be used in VB; we will look at the actual creation of the objects in a little while as well as looking at how objects are used in detail.
Dim aDog As New Dog Dim aCat As New Cat Dim Animal As Object aDog.Speak ' prints "Woof" aCat.Speak ' prints "Miaow" Set Animal = aDog Animal.Speak ' prints "Woof" Set Animal = New Cat Animal.Speak ' prints "Miaow"
This is a rather silly example, but it illustrates some very important concepts. Note that we have DIMmed three objects. The first, aDog, is an instance of the Dog object type. The second, aCat, is an instance of the Cat object type. The third is a generic object; at run time it can be made to refer to any type of object using a Set statement. When it is set to refer to an instance of the Dog object type, Animal.Speak emits a "Woof", whereas when it is set to an instance of the Cat object type, it miaows.
Also notice how Animal is first set to aDog, which is an instance of the Dog object type. After that statement, aDog and Animal both refer to the exact same instance of the object. When we transform Animal to a cat, we use the New key-word to create a brand new object of type Cat. Now both aCat and Animal are objects of the Cat type, but they are different objects. This means that they have separate sets of data fields.
If you can understand this rather silly example, you are well on your way to understanding object orientation. We still have a little bit of learning to do about the details of how OOP is implemented in VB, but the concepts have been introduced.
Class as Template
It is time for some terminology. You gotta talk the talk.
Strictly speaking, in the previous example, Cat and Dog were not objects at all. They were classes. A class is like a definition for a user-defined type. It acts as a template from which any number of actual instances can be created. In a similar way, Dog and Cat are templates that can be used as cookie-cutters to create as many of our canine and feline friends as we need.
Each class in a VB program needs to be in a separate module and this module needs to be a class module. The name of the module becomes the name of the class. This is a property of the class and is set in the properties window for the class in much the same way that you set the name property of a form. It is not necessarily the file name in which the source is stored.
A particular instance of a class is an object. In the preceding example, aDog and aCat were objects of their corresponding classes. Simply creating a variable of type Dog caused VB to allocate storage for a new set of data fields as specified by the Dog class. The variable Animal, on the other hand, is a generic object. VB does not allocate any storage for that object because it does not know what type of object you want or, in other words, which class to use as a cookie-cutter.
Creating an object is as simple as creating a variable. You simply use the Dim statement to create one as we have already seen.
If you Dim an object of a particular type, then that object can only ever refer to objects of that type. While this seems more restrictive, it is often what we want anyway and allows the compiler to optimise the code and to check that the operations we attempt are supported by that object type.
If you Dim an object as a generic "Object", then that object can be made to refer to instances of any class of object at run time. This is very flexible but that flexibility does incur significant run-time performance overhead. It also means that the compiler cannot check whether the operations you code for that object will be valid at run time. For example, if you were to assign Animal to an object of the Stapler class (which presumably does not have a Speak procedure defined for it), then this could not be picked up by the compiler and would cause a run-time error in your program.
The approach to take is to use specific object variables wherever they will achieve the desired result. However, do not be afraid of generic object variables – they are very useful critters indeed.
For specific types of objects, you need to decide whether you want to actually create a new object or simply a reference (a pointer) to an object. Consider these two statements :
Dim Rover As Dog Dim Rex As New Dog
Rover is a reference to a Dog object. If we try to use Rover as it stands, we will get an error because Rover does not currently reference any particular Dog object. In order to actually create a new Dog object, we need to follow the declaration of Rover with a SET statement as follows
Set Rover = New Dog
This actually creates the Dog object and sets up Rover as a pointer to it. Because this is such a common requirement, VB allows for the two steps to be combined as we have done for Rex. By declaring Rex as a New Dog, VB will actually create a new instance of a Dog object and set up Res to point to it.
You need to decide what you are trying to achieve and use the appropriate form of the Dim statement.
Just before we actually build our first classes and objects (yes, you will create the Cat and Dog classes!), we need to cover methods. In the preceding discussions, we have deliberately steered clear of OOP-speak initially and introduced the terms one-by-one. Whenever we said "procedure", the correct word is "method". The methods of a class are the executable procedures that the class makes available. In our example, Speak is a method.
Methods are simply public Sub or Function procedures that are defined in the class module for the class. For example, in the Dog class, we might define a method Speak like this :
Public Sub Speak() MsgBox "Woof" End Sub
Notice that it is vanilla VB code. If it is a public Sub or Function, then it becomes a valid method of that class. Private Subs and Functions can only be called by other code within that class, just as you would expect.
That is all we need to get started. There is still one very important concept, properties, that we will look at after we try our hand at creating some simple classes and objects.
You will already be familiar with the concept of properties through your use of the VB development environment. You know that a property is like a data field that belongs to an object and that you can read and/or write using the notation :
object.property = value or variable = object.property
You may have thought about what was going on behind the scenes. Obviously, this was more than a simple assignment statement. When you set the Visible property of a control, it will actually become visible or invisible as the case may be. When you change the Top or Left properties, the control or form will move. What is going on here?
Actually, properties are an illusion created by the compiler. When you assign a value to a property, the compiler generates code behind the scenes to call a procedure to which it passes the value that you wish to assign. That procedure can do whatever is necessary to store the value and cause whatever side-effects are desired (like changing the visibility or position). In a similar way, when you try to retrieve the current value of a property, the compiler generates a call to a function that will return the current value. Again, retrieving a value can cause side effects; for example, there may not be any local variable that stores the value and the function may need to query the operating system to determine what value should be returned.
The big news in VB4 is the ability to define your own properties. You can do this for forms and for classes (which are essentially just forms without any visual aspects).
Remember, a property is just a pair of procedures – a Sub procedure to store the value and a Function procedure to return it.
The easiest way to explain the syntax is to start with a trivial but nonetheless complete example.
Option Explicit Private FEmotion As String Public Property Let Emotion(AnEmotion As String) FEmotion = LCase$(AnEmotion) Select Case FEmotion Case "happy": MsgBox "Yippee!!!" Case "sad": MsgBox "Boo hoo" Case Else: MsgBox "Ho Hum" End If End Property Public Property Get Emotion() As String Emotion = FEmotion End Property
This is some code that could be in a form or a class module.
Notice how we have declared a module-level, private variable called FEmotion to store our current state of mind. This is a common coding practice as we will often want to store a copy of the last value assigned to a property both for internal use by the module and so that we can return it to any external code that needs it. These private, module-level variables are usually given a name that is derived from the name of the property that they are used by. In this case, the property is called "Emotion" and we add an upper-case "F" to it in order to create the variable name. ("F" stands for Field, in case you were wondering.) It does not matter which convention you adopt but it is important to adopt some convention and to apply it consistently.
The variable is declared as a private variable so that it cannot be accessed by any code outside of the current procedure. The only way that the outside world can see or affect our emotion (this is getting entirely too Freudian) is through the public Let and Get property procedures.
When a property is assigned a value, the compiler generates code behind the scenes to call the Property Let procedure for the property, passing it the value that is being assigned. A Property Let procedure takes the form of a Sub procedure – that is, it does not return a value.
Property Let procedures are typically used to validate or normalise the values assigned to properties. In our example, the value assigned is coerced to lower case. They are also used to trigger side-effects. Again in our example, assigning a value to the Emotion property elicits some response from the object. Obviously, these are trivial examples but the concept should be clear. The Property Let procedure is where you do your validation and where you trigger action from.
If it is appropriate to allow users of the object to retrieve the current setting for a property, we will also create a Property Get procedure. The compiler generates a call to this procedure whenever a property name appears on the right hand side of a statement (ie when the value is being read). It needs to return a value so it takes the form of a Function procedure.
Property Get procedures are typically quite straight forward as we see in the example above. Very often, they will simply return the value that was stored in a module-level, private variable by the Property Let procedure.
This is not an absolute requirement, of course. It may well be that the act of retrieving a property value involves running a query on a remote server, processing the results and then returning an appropriate value.
Read-Only and Write-Only Properties
It is possible to create read-only and write-only properties.
For a read-only property, you would define a Property Get procedure with no corresponding Property Let procedure.
Similarly, to create a write-only property, you would define a Property Let procedure with no corresponding Property Get procedure.
If you have used the Microsoft Data Access Objects or done any OLE programming, you will be familiar with the concept of object hierarchies. VB does not support inheritance but it does support the concept of hierarchies by using containment, a technique where one object can create and own (or contain) another.
We will look at object hierarchies and the use of collections to control them a little later in this course. We introduce it now only so that we can wrap up our treatment of properties. If a property is of an object type (as opposed to a variable) then it cannot use the Property Let procedure to assign a value to it. In its place, it uses a Property Set procedure, which has exactly the same syntax as a Property Let and is passed an Object as the parameter.
For example, if we wanted to allow a caller to assign a recordset to our property, we could use something like this :
Public Property Set RecordSet (RS As RecordSet) Set FRecordSet = RS End Property
This might be used in an object that does some processing of recordsets. For example, it might dynamically build a report based an whatever columns it found in that recordset.