Book Review: Beginning Visual Basic 6 Objects
What is Inheritance?
With inheritance, a class gains all the properties and methods that make up the interface of the base class, and it can then extend the interface by adding properties and methods of its own. The new class can also extend or change the implementation of each of the properties and methods from the original base class.
Inheritance, in a nutshell, would mean that we could create an object and then use it (plus all its properties and functionality) as the basis for a new object.
Inheritance at DataDamage Inc.
Consider our application for DataDamage Inc. If our application were a little bigger, then at some point we would reach a worrying situation. Given that the place where we stored the data would sooner or later change, we would eventually have to go through and change all our data access code to deal with that change. We would have work to do in both in the clsAuthors class and the clsTitles class - not to mention any other classes that may have been added over time which access the database directly. Not a pleasant situation, especially if you scale up the size of our application.
Inheritance would solve our dilemma here. We could simply put all the common functionality for accessing the database in one object, and then build on that object with new ones that inherited the old. This would be ideal in our case - we could have a clsRecordset object holding a lot of code to talk to the database, and then we could inherit that object into clsAuthors and clsTitles, just adding any appropriate code specific to those tables.
It seems we don't have that luxury yet, however. Maybe in a future version of Visual Basic we will find inheritance. For now, is all this just wishful thinking? Read on.
As we've seen, VB6 comes complete with a fantastic command: Implements. When people first saw this command, the immediate reaction from a lot of them was to jump up and down proclaiming that Microsoft had at last brought inheritance to Visual Basic. This was not the case.
We can, however, gain interface inheritance by using the Implements keyword. The Implements keyword just tells Visual Basic that we would like to implement code for another object's interface - we don't inherit the original object's data or behavior. However, the big drawback is that we can't extend an interface that has been created with the Implements keyword, so with this method we don't gain one of the key benefits of inheritance.
So how do we use interface inheritance? Take a look at this example of a Vehicle class:
Option Explicit Public Property Get Wheels() As Integer End Property Public Property Get NumberOfSeats() As Integer End Property Public Property Get Color() As String End Property
Notice that there isn't any code in these routines. Remember that interface inheritance allows the inheritance of interfaces only. Any code in these routines would not be used anyway.
Now we can create classes based on the Vehicle class by using the Implements keyword to inherit the interface. Take a look at this Van class:
Option Explicit Implements Vehicle Private Property Get Vehicle_Wheels() As Integer Vehicle_Wheels = 4 End Property Private Property Get Vehicle_NumberOfSeats() As Integer Vehicle_NumberOfSeats = 2 End Property Private Property Get Vehicle_Color() As String Vehicle_Color = "White" End Property
We could do the same with a Car class:
Option Explicit Implements Vehicle Private Property Get Vehicle_Wheels() As Integer Vehicle_Wheels = 4 End Property Private Property Get Vehicle_NumberOfSeats() As Integer Vehicle_NumberOfSeats = 5 End Property Private Property Get Vehicle_Color() As String Vehicle_Color = "Red" End Property
Notice that we have to prefix the properties Wheels, NumberOfSeats and Color with Vehicle_ so that Visual Basic knows that these routines belong to the Vehicle interface. Unfortunately we can't extend the interface, so if we want to add a property to the Van class for StorageCapacity we'll have to use a different technique.
It's possible to simulate inheritance by using a combination of two techniques, containment and delegation. So what do these terms mean?
The idea behind containment is that, in object-oriented analysis, an object can have a private instance of another object inside itself. For instance, a old Television object could contain a Valve object, but that Valve object would be private to the Television object, since no code outside of that Television object would be likely to interact with that Valve.
Delegation refers to the situation where one object delegates a task to another object rather than doing the work itself. Now when we elect not to call the methods in the base class, we are choosing not to delegate tasks down to the base class - and we write methods within the current class to perform the task instead.
Let's see how we can simulate inheritance with our Vehicle example:
Option Explicit Public Property Get Wheels() As Integer Wheels = 4 End Property Public Property Get NumberOfSeats() As Integer NumberOfSeats = 5 End Property Public Property Get Color() As String End Property
This time we've put some code into our Wheels and NumberOfSeats routines. With simulated inheritance these properties will be available in any new classes we create from Vehicle.
Now let's create a new Car class.
Option Explicit Private objVehicle As Vehicle Public Property Get Wheels() As Integer Wheels = objVehicle.Wheels End Property Public Property Get NumberOfSeats() As Integer NumberOfSeats = objVehicle.NumberOfSeats End Property Public Property Get Color() As String Color = "Red" End Property
Notice that this time we don't use Implements and that we create a Private Vehicle object inside our new Car class. Our Car class contains an instance of the Vehicle class.
Private objVehicle As Vehicle
In our Wheels and NumberOfSeats routines, the Car class is delegating the work down to the private Vehicle object.
NumberOfSeats = objVehicle.NumberOfSeats
Now let's create a new Van class in the same way:
Option Explicit Private objVehicle As Vehicle Public Property Get Wheels() As Integer Wheels = objVehicle.Wheels End Property Public Property Get NumberOfSeats() As Integer NumberOfSeats = 2 End Property Public Property Get Color() As String Color = "White" End Property Public Property Get StorageCapacity() As String StorageCapacity = "A Lot" End Property
Now in this case the NumberOfSeats for a Van is different, so there is no delegation down to the Vehicle object. Instead, the Van overrides this functionality by implementing the routine itself.
NumberOfSeats = 2
We've also extended our interface for the Van object by adding a StorageCapacity property - something we were unable to do with interface inheritance.
Public Property Get StorageCapacity() As String StorageCapacity = "A Lot" End Property
Containment at DataDamage Inc.
In our application for DataDamage, the combination of containment and delegation allows us to move the basic functionality of the clsTitles and clsAuthors classes back into clsRecordset. We can then call that functionality from the methods in clsTitles and clsAuthors.
Try It Out - Using Containment in Our BiblioTech Project
1 Let's begin by moving the basic functionality that was in the clsTitles and clsAuthors into clsRecordset. Change the clsRecordset class so that it looks like this:
Option Explicit ' clsRecordset object - a generic object designed to wrap ' up the functionality of a recordset in a generic re-useable ' object. ' Class methods Public Sub MoveNext(recRecords As Recordset) If Not recRecords.EOF recRecords.MoveNext End If End Sub Public Sub MovePrevious(recRecords As Recordset) If Not recRecords.BOF Then recRecords.MovePrevious End If End Sub Public Sub MoveLast(recRecords As Recordset) recRecords.MoveLast End Sub Public Sub MoveFirst(recRecords As Recordset) recRecords.MoveFirst End Sub
2 Now we come to the interesting part! We're going to use containment in clsTitles, so that a private instance of clsRecordset is contained within our clsTitles object.
The top of our clsTitles class thus becomes:
Option Explicit ' clsTitles - wraps up the Biblio Titles table. By keeping the ' interface consistent it should be possible to move the class ' to point at some other data source, such as an RDO Resultset, ' or flat file, without users of the class ever noticing. Public Event DataChanged() ' Public members to implement properties Public strTitle As String Public intYear_Published As Integer Public strISBN As String Public lngPubID As Long Public strDescription As String Public strNotes As String Public strSubject As String Public strComments As String Private m_objRecordset As New clsRecordset ' Private members to hold connection to database, ' and to the recordset itself Private m_recTitles As Recordset Private m_dbDatabase As Database
Notice we've removed the Implements command from this code now: we no longer need to implement clsRecordset in clsTitles, since we can access the clsRecordset routines through our new m_recRecordset private member variable, which contains the private instance of the clsRecordset object.
3 Now we're able to remove the basic functionality from our clsTitles class - remember, we're delegating the recordset navigation tasks to our clsRecordset class. The end of your clsTitles class should look like this:
' Class methods Public Sub MoveNext() m_objRecordset.MoveNext m_recTitles Reload_Members End Sub Public Sub MovePrevious() m_objRecordset.MovePrevious m_recTitles Reload_Members End Sub Public Sub MoveLast() m_objRecordset.MoveLast m_recTitles Reload_Members End Sub Public Sub MoveFirst() m_objRecordset.MoveFirst m_recTitles Reload_Members End Sub
All we're doing here is delegating the navigation of the recordset held in the m_recTitles member variable, to our private instance of clsRecordset (i.e. m_objRecordset).
Notice also that we've deleted the following routines from our class. As we're using a combination of containment and delegation to simulate inheritance, instead of the Implements keyword, we're now able to extend the interface of our clsAuthors class. Therefore, these routines are no longer needed in our clsTitles class.
Public Sub clsRecordset_AddAsNew() ' Do nothing End Sub Public Sub clsRecordset_SaveChanges() ' Do nothing End Sub Public Sub clsRecordset_DeleteCurrent() ' Do nothing End Sub
4 Now we must repeat the process with the clsAuthors class. Change the code at the beginning of clsAuthors so that it looks like this:
Option Explicit ' clsAuthors - wraps up the functionality to browse and ' maintain the authors table in the Biblio database Public Event DataChanged() ' Public members to implement properties Public lngAu_ID As Long Public strAuthor As String Public intYear_Born As Integer Private m_objRecordset As New clsRecordset ' Private members to hold connection to database, ' and to the recordset itself Private m_recAuthors As Recordset Private m_dbDatabase As Database
Once again, the Implements command has been removed and we've declared a member variable m_objRecordset to hold our private instance of the clsRecordset object. No surprises here!
5 Now move to the bottom of the clsAuthors class and make the following changes:
' Class methods Public Sub MoveNext() m_objRecordset.MoveNext m_recAuthors Reload_Members End Sub Public Sub MovePrevious() m_objRecordset.MovePrevious m_recAuthors Reload_Members End Sub Public Sub MoveLast() m_objRecordset.MoveLast m_recAuthors Reload_Members End Sub Public Sub MoveFirst() m_objRecordset.MoveFirst m_recAuthors Reload_Members End Sub Public Sub SaveChanges() With m_recAuthors .Edit Save_Members .Update End With End Sub Public Sub AddAsNew() With m_recAuthors .AddNew Save_Members .Update End With End Sub Public Sub DeleteCurrent() m_recAuthors.Delete On Error Resume Next m_recAuthors.MoveFirst Reload_Members End Sub
Just as with our clsTitles class, we can remove the clsRecordset_ prefix from our method names. In addition, as the code for navigating the recordset is held within the clsRecordset class we just need to delegate these tasks to the base object, m_objRecordset.
Finally, we've extended the interface of our clsAuthors class with the AddAsNew, SaveChanges and DeleteCurrent routines. These routines need to be Public as they're being called from our frmAuthor form.
Now all that remains to be done is to make some minor alterations to our browsing forms.
6 Open the code window for frmTitle and change your code so that it looks like this:
Option Explicit Private WithEvents Titles As clsTitles Private Sub cmdFirst_Click() Titles.MoveFirst End Sub Private Sub cmdLast_Click() Titles.MoveLast End Sub Private Sub cmdNext_Click() Titles.MoveNext End Sub Private Sub cmdPrevious_Click() Titles.MovePrevious End Sub
7 Now repeat the process with the frmAuthor form - it should look like this:
Option Explicit Private WithEvents Authors As clsAuthorsPrivate Sub Authors_DataChanged() ' When the data in the Authors object changes (by moving to a new ' record, update the controls on the form with the revised data txtAuthor = Authors.strAuthor txtYearBorn = Authors.intYear_born End Sub Private Sub cmdAdd_Click() Update_Authors Authors.AddAsNew End Sub Private Sub cmdDelete_Click() Authors.DeleteCurrent End Sub Private Sub cmdFirst_Click() Authors.MoveFirst End Sub Private Sub cmdLast_Click() Authors.MoveLast End Sub Private Sub cmdNext_Click() Authors.MoveNext End Sub Private Sub cmdPrevious_Click() Authors.MovePrevious End Sub Private Sub cmdUpdate_Click() Update_Authors Authors.SaveChanges End Sub
8 Now run the project! You shouldn't notice any difference as a user - but there's a whole world of difference in how the application is structured under the hood.
For the DataDamage application on its present scale, there wouldn't really be much code benefit to us at this stage in using these techniques. In the real world, though, should the database ever need to be moved to somewhere other than a nice simple Access database, these techniques would take on a new significance. Our trump card would be that we could easily redesign that base object in such a way that nothing else in the application would have to change.
Naturally, the code in the base class would have to change to reflect the change in database, but those changes would ripple up to any other classes which contained its functionality - and from there the changes would ripple up to the forms that used those classes. Now THAT is a maintainable system!
Here's a thought for you. Moving all that code out of the original class and into a new one might seem like a lot of work if the classes we were dealing with were a little larger and more complex. So what's the point? Well, the point does depend on your project and your needs. That's why we've looked at the different ways you can implement objects in Visual Basic. But I do have a real-life example that might help you make your mind up...
I did some consultancy for a large company that had a team of 10-20 inexperienced Visual Basic developers. In order to reduce the burden on them (they were all newbies after all) the Management decided that we would develop boilerplate code, which was pre-built blocks of code that the other developers would just fill in the blanks on. My suggestion was "Hey, why don't we do something with interface inheritance and containment of objects - that's going to save us a lot of time and effort, and the Visual Basic compiler can even help us enforce it all." The management answer was the age old "We don't have time to do that... it's too new."
The end result then was that we got on with the management's "quicker" solution. The rest of the team then took our boilerplate forms, added their minor changes, and then added them into the grand project. There were more than 90 forms in that project, all of which were based on the boilerplate code. Every time that boilerplate changed, the programmers had to stop and make the changes to their working forms by hand. Then they had to retest the forms for errors. This process took about an hour per form, each and every time the base code changed, which averaged out at once a month.
So, the management decision that we couldn't afford 2 days at the start to get it right resulted in them spending approximately 45 man days retro-fitting their code with boilerplate changes.
The moral of my story is this: containment may take some extra time in the short term, but in the long term it's usually worth every minute.
You have been warned.
Page 8 of 9