As you begin to work more with classes, you soon come across programming cases where you have classes that are similar to others you are already using in this program or another one. It seems a shame to just copy all that code over again and have a lot of objects that are separate but very alike.
In languages like Java and VB.NET, you can derive new classes from existing classes and change only those methods that differ in the new class, with the unchanged parent methods called automatically. VB6 does not support this level of inheritance, but it does provide interfaces and implementations that allow you to produce related classes with only a small amount of effort.
In VB, you can create a class containing only definitions of the methods and no actual code. This is called an interface definition. Then you can create other classes that implement that interface, and they all can be treated as if they were instances of the parent interface, even though they implement the methods differently.
For example, you could create an interface called Command that has the following methods.
Public Sub Execute() End Sub '------ Public Sub init(nm As String) End Sub
Then you could create a number of Command objects, such as ExitCommand, that implement the Command interface. To do this, you create a class called ExitCommand, and insert this line.
Implements Command
Then, from the left drop-down you select the Command interface, and from the right drop-down you create instances of the Execute and init methods. You can now fill in these methods with whatever code is appropriate.
Private Sub Command_Execute() 'do something End Sub '------ Private Sub Command_init(nm As String) 'initialize something End Sub
The advantage of this approach is that the ExitCommand class is now also of the type Command, and all of the classes that implement the Command interface can be treated as instances of the Command class. To see how this can be helpful, let's consider a program for simulating investment growth.
Our investment simulation program will present us with a mixture of stocks and bonds, and we can look at their growth during any time interval. We will assume that the bonds are all tax-free municipal bonds and that all the stocks have positive growth rates.
The program starts with a list of seven stocks and bonds and an investment nest egg of $10,000 to use. You can invest in any combination of stocks and bonds at any rate until all your money is invested. The initial program state is shown in Figure 6-1.
You select investments by highlighting them, selecting a purchase amount, and clicking on the Buy button.
Once you have selected some stocks, you can enter any future date and compute the total stock value and the total taxable income. For simplicity, we assume that the stock income is all taxable and that the bond income is all nontaxable. A typical investment result is shown in Figure 6-2.
The taxable income display is shown in Figure 6-3.
The most important class in the simulator represents a stock. This class has an init method that sets the name and type (stock or bond) and an invest method that determines the date and how much was invested.
Private stockName As String 'name Private isMuniBond As Boolean 'true of muni bond Private investment As Single 'amount invested Private invDate As Date 'date of investment Private rate As Single 'growth rate '------ Public Sub init(nm As String, muniBond As Boolean) stockName = nm 'save the name isMuniBond = muniBond 'and whether a bond If isMuniBond Then rate = 0.05 'low fixed rate for bonds Else rate = Rnd / 10 'random rate for stocks End If End Sub '----- Public Sub invest(amt As Single) invDate = CVDate(Date$) 'remember date investment = amt 'and amount invested End Sub
Then, when we ask for the amount of the investment or the taxable amount earned, we compute them based on the days elapsed since the investment.
Public Function getName() As String getName = stockName 'return the stock name End Function '------ Public Function getValue(toDate As Date) As Single Dim diff, value As Single 'compute the value of the investment diff = DateDiff("d", invDate, toDate) value = (diff / 365) * rate * investment + investment getValue = value 'and return it End Function '------ Public Function getTaxable(toDate As Date) As Single If isMuniBond Then getTaxable = 0 'no taxable income Else 'return the taxable income getTaxable = getValue(toDate) - investment End If End Function
There are two places in the preceding code where we have to ask what kind of investment this is. One is when we decide on the rate and the other is when we decide on the taxable return. Whenever you see decisions like this inside classes, you should treat them as a yellow caution flag indicating that there might be a better way. Why should a class have to make such decisions? Would it be better if each class represented only one type of investment? If we did create a Stock and a Bond class, the program would become more complicated because our display of list data assumes the data are all of type stock.
Private Sub Taxable_Click() Dim i As Integer, dt As Date Dim stk as Stock lsOwn.Clear dt = CVDate(txDate.Text) For i = 1 To stocksOwned.Count Set stk = stocksOwned(i) lsOwn.AddItem stk.getName & vbTab & _ Format$(stk.getTaxable(dt), "####.00") Next i End Sub
Instead, we'll create a new class called Equity and derive the Stock and Bond classes from it. Here is our empty Equity interface.
Public Sub init(nm As String) End Sub '------ Public Sub invest(amt As Single) End Sub '------ Public Function getName() As String End Function '------ Public Function getValue(toDate As Date) As Single End Function '------ Public Function getTaxable(toDate As Date) As Single End Function '------ Public Function isBond() As Boolean End Function
Now, our Stock class just becomes the following.
Implements Equity Private stockName As String 'stock name Private investment As Single 'amount invested Private invDate As Date 'investment date Private rate As Single 'rate of return '------ Private Function Equity_getName() As String Equity_getName = stockName 'return the name End Function '------ Private Function Equity_getTaxable(toDate As Date) As Single 'compute the taxable include Equity_getTaxable = Equity_getValue(toDate) - investment End Function '------ Private Function Equity_getValue(toDate As Date) As Single Dim diff, value As Single 'compute the total value of the investment to date diff = DateDiff("d", invDate, toDate) value = (diff / 365) * rate * investment + investment Equity_getValue = value End Function '------ Private Sub Equity_init(nm As String) stockName = nm 'initialize the name rate = Rnd / 10 'and a rate End Sub '------ Private Sub Equity_invest(amt As Single) invDate = CVDate(Date$) 'set the date investment = amt 'and the amount End Sub '------ Private Function Equity_isBond() As Boolean Equity_isBond = False 'is not a bond End Function
The Bond class is pretty similar, except for the getTaxable, init, and isBond methods.
Implements Equity Private stockName As String Private investment As Single Private invDate As Date Private rate As Single '------ Private Function Equity_getName() As String Equity_getName = stockName End Function '------ Private Function Equity_getTaxable(toDate As Date) As Single Equity_getTaxable = 0 End Function '------ Private Function Equity_getValue(toDate As Date) As Single Dim diff, value As Single diff = DateDiff("d", invDate, toDate) value = (diff / 365) * rate * investment + investment Equity_getValue = value End Function '------ Private Sub Equity_init(nm As String) stockName = nm rate = 0.05 End Sub '------ Private Sub Equity_invest(amt As Single) invDate = CVDate(Date$) investment = amt End Sub '------ Private Function Equity_isBond() As Boolean Equity_isBond = True End Function
However, by making both Stocks and Bonds implement the Equity interface, we can treat them all as Equities, since they have the same methods, rather than having to decide which kind is which.
Private Sub Taxable_Click()
Dim i As Integer, dt As Date
Dim stk as Equity
'Show the list of taxable incomes
lsOwn.Clear
dt = CVDate(txDate.Text)
For i = 1 To stocksOwned.Count
Set stk = stocksOwned(i)
lsOwn.AddItem stk.getName & vbTab & _
Format$(stk.getTaxable(dt), "####.00")
Next i
End Sub
A quick glance at the preceding code shows that the Stock and Bond classes have some duplicated code. One way to prevent this from happening is to put some methods in the base class and then call them from the derived classes. While the initial idea was to make the interface module just a series of empty methods, VB6 does not require this, and they can indeed have code in them.
For example, we could rewrite the basic Equity class to contain another method that actually computes the interest and is called by the derived classes.
'Class Equity with calcValue added Public Sub init(nm As String) End Sub '------ Public Sub invest(amt As Single) End Sub '------ Public Function getName() As String End Function '------ Public Function getValue(toDate As Date) As Single End Function '------ Public Function getTaxable(toDate As Date) As Single End Function '------ Public Function isBond() As Boolean End Function '------ Public Function calcValue(invDate As Date, toDate As Date, _ rate As Single, investment As Single) Dim diff, value As Single diff = DateDiff("d", invDate, toDate) value = (diff / 365) * rate * investment + investment calcValue = value End Function
Now, since we don't have real inheritance in VB6, we can't call this from the derived classes directly, but we can insert an instance of the Equity class inside the Stock and Bond classes and call its calcValue method. This simplifies the Stock class to the following.
Implements Equity Private stockName As String 'stock name Private investment As Single 'amount invested Private invDate As Date 'date of investment Private rate As Single 'rate of return Private eq As New Equity 'instance of base Equity class '------ Private Function Equity_getName() As String Equity_getName = stockName End Function '------ Private Function Equity_getTaxable(toDate As Date) As Single Equity_getTaxable = Equity_getValue(toDate) - investment End Function '------ Private Function Equity_getValue(toDate As Date) As Single 'compute using method in base Equity class Equity_getValue = eq.calcValue(invDate, toDate, rate, _ investment) End Function '------ Private Sub Equity_init(nm As String) stockName = nm rate = Rnd / 10 End Sub '------ Private Sub Equity_invest(amt As Single) invDate = CVDate(Date$) investment = amt End Sub '------ Private Function Equity_isBond() As Boolean Equity_isBond = False End Function
Now, since the calcValue method is part of the interface, you have to include an empty method by that name in the Stock and Bond classes as well so the classes can compile without error.
Private Function Equity_calcValue(invDate As Date, _ toDate As Date, rate As Single, _ investment As Single) As Variant 'never used in child classes End Function
You could avoid this by creating an ancillary class that contains the computation method and creating an instance of it in the Stock and Bond classes, but this does lead to more clutter of extra classes.
Another way of accomplishing the same thing in this particular case is to give Stock and Bond the same public interfaces without using an Equity interface at all. Since all the operations in this simple program take place through a Collection, we can obtain a collection item and call its public methods without ever knowing which type of equity it actually is. For example, the following code will work for a collection Stocks containing a mixture of Stock and Bond objects.
For i = 1 To stocks.Count sname = stocks(i).getName lsStocks.AddItem sname Next i
You should recognize, however, that this special case only occurs because we never need to get the objects back out as any particular type. In the cases we develop in the chapters that follow, this will seldom be the case.
In this chapter, we've shown how to construct an interface and a set of classes that implement it. We can then refer to all the derived classes as if they were an instance of the interface class and simplify our code considerably.