Chapter 14. The Prototype Pattern

The Prototype pattern is another tool you can use when you can specify the general class needed in a program but need to defer the exact class until execution time. It is similar to the Builder in that some class decides what components or details make up the final class. However, it differs in that the target classes are constructed by cloning one or more prototype classes and then changing or filling in the details of the cloned class to behave as desired.

Prototypes can be used whenever you need classes that differ only in the type of processing they offer—for example, in parsing of strings representing numbers in different radixes. In this sense, the prototype is nearly the same as the Examplar pattern described by Coplien (1992).

Let's consider the case of an extensive database where you need to make a number of queries to construct an answer. Once you have this answer as a table or RecordSet, you might like to manipulate it to produce other answers without issuing additional queries.

In a case like the one we have been working on, we'll consider a database of a large number of swimmers in a league or statewide organization. Each swimmer swims several strokes and distances throughout a season. The “best times” for swimmers are tabulated by age group, and even within a single four-month season many swimmers will pass their birthdays and fall into new age groups. Thus, the query to determine which swimmers did the best in their age group that season is dependent on the date of each meet and on each swimmer's birthday. The computational cost of assembling this table of times is therefore fairly high.

Once we have a class containing this table sorted by sex, we could imagine wanting to examine this information sorted just by time or by actual age rather than by age group. It would not be sensible to recompute these data, and we don't want to destroy the original data order, so some sort of copy of the data object is desirable.

Cloning in Visual Basic 6

The idea of cloning a class (making an exact copy) is not a designed-in feature of Visual Basic, but nothing actually stops you from carrying out such a copy yourself. The only place the Clone method appears in VB is in database manipulation. You can create a Recordset as a result of a database query and move through it a row at a time. If for some reason you need to keep references to two places in this Recordset, you would need two “current rows.” The simplest way to handle this in VB6 is to clone the Recordset.

Public Sub cloneRec(Query As String)
 Dim db As Database
 Dim rec As Recordset, crec As Recordset

 'open database recordset
 Set rec = db.OpenRecordset(Query, dbOpenDynaset)

 'clone a copy
 Set crec = rec.Clone
End Sub

Now this approach does not generate two copies of the Recordset. It just generates two sets of row pointers to use to move through the records independently of each other. Any change you make in one clone of the Recordset is immediately reflected in the other because there is in fact only one data table. We discuss a similar problem in the following example.

Using the Prototype

Now let's write a simple program that reads data from a database and then clones the resulting object. In our example program, we just read these data from a file, but the original data were derived from a large database, as we discussed previously. That file has the following form.

Kristen Frost, 9, CAT, 26.31, F
Kimberly Watcke, 10, CDEV,27.37, F
Jaclyn Carey, 10, ARAC, 27.53, F
Megan Crapster, 10, LEHY, 27.68, F

We'll use the vbFile class we developed earlier.

First, we create a class called Swimmer that holds one name, club name, sex, and time, and read them in using the File class.

Option Explicit
'Class Swimmer
Private ssex As String
Private sage As Integer
Private stime As Single
Private sclub As String
Private sfrname As String, slname As String
'-----
Public Sub init(Fl As vbFile)
 Dim i As Integer
 Dim nm As String

 nm = Fl.readToken   'read in name
 i = InStr(nm, " ")
 If i > 0 Then       'separate into first and last
   sfrname = Left(nm, i - 1)
   slname = Right$(nm, Len(nm) - i)
 Else
   sfrname = ""
   slname = nm       'or just use one
 End If

 sage = Val(Fl.readToken)    'get age
 sclub = Fl.readToken        'get club
 stime = Val(Fl.readToken)   'get time
 ssex = Fl.readToken         'get sex
End Sub
'-----
Public Function getTime() As Single
 getTime = stime
End Function
'-----
Public Function getSex() As String
 getSex = ssex
End Function
'-----
Public Function getName() As String
 getName = sfrname & " " & slname
End Function
'-----
Public Function getClub() As String
 getClub = sclub
End Function
'-----
Public Function getAge() As Integer
 getAge = sage
End Function

We also provide a getSwimmer method in SwimData and getName, getAge, and getTime methods in the Swimmer class. Once we've read the data into SwimInfo, we can display it in a list box.

Then we create an interface class called SwimData that maintains a Collection of the Swimmers we read in from the database.

'Interface SwimData
Public Sub init(Filename As String)
End Sub
'-----
Public Sub Clone(swd As SwimData)
End Sub
'-----
Public Sub setData(swcol As Collection)
End Sub
'-----
Public Sub sort()
End Sub
'-----
Public Sub MoveFirst()
End Sub
'-----
Public Function hasMoreElements() As Boolean
End Function
'-----
Public Function getNextSwimmer() As Swimmer
End Function

When the user clicks on the Clone button, we'll clone this class and sort the data differently in the new class. Again, we clone the data because creating a new class instance would be much slower, and we want to keep the data in both forms.

Private Sub SwimData_Clone(swd As SwimData)
    swd.setData swimmers    'copy data into new class
End Sub

In the original class, the names are sorted by sex and then by time, whereas in the cloned class they are sorted only by time. In Figure 14-1, we see the simple user interface that allows us to display the original data on the left and the sorted data in the cloned class on the right.

Prototype example. The left-hand list box is loaded when the program starts and the right-hand list box is loaded when you click on the Clone button.

Figure 14-1. Prototype example. The left-hand list box is loaded when the program starts and the right-hand list box is loaded when you click on the Clone button.

Now, let's click on the Refresh button to reload the left-hand list box from the original data. The somewhat disconcerting result is shown in Figure 14-2.

Prototype example after clicking on Clone and then on Refresh

Figure 14-2. Prototype example after clicking on Clone and then on Refresh

Why have the names in the left-hand list box also been re-sorted? This occurs because the clone method is a shallow copy of the original class. In other words, the references to the data objects are copies, but they refer to the same underlying data. Thus, any operation we perform on the copied data will also occur on the original data in the Prototype class.

In some cases, this shallow copy may be acceptable, but if you want to make a deep copy of the data, you must write a deep cloning routine of your own as part of the class you want to clone. In this simple class, you just create a new Collection and copy the elements of the old class's Collection into the new one.

Private Sub SwimData_Clone(swd As SwimData)
 Dim swmrs As New Collection
 Dim i As Integer
 'copy data from one collection
 ' to another
 For i = 1 To swimmers.Count
   swmrs.Add swimmers(i)
 Next i
 'and put into new class
 swd.setData swmrs
End Sub

Using the Prototype Pattern

You can use the Prototype pattern whenever any of a number of classes might be created or when the classes are modified after being created. As long as all the classes have the same interface, they can actually carry out rather different operations.

Let's consider a more elaborate example of the listing of swimmers we just discussed. Instead of just sorting the swimmers, let's create subclasses that operate on that data, modifying it and presenting the result for display in a list box. We start with the same abstract class SwimData.

Then it becomes possible to write different concrete SwimData classes, depending on the application's requirements. We always start with the SexSwimData class and then clone it for various other displays. For example, the OneSexSwimData class resorts the data by sex and displays only one sex. This is shown in Figure 14-3.

The OneSexSwimData class displays only one sex on the right.

Figure 14-3. The OneSexSwimData class displays only one sex on the right.

In the OneSexSwimData class, we sort the data by time but return them for display based on whether girls or boys are supposed to be displayed. This class has this additional method.

Public Sub setSex(sx As String)
  Sex = sx 'copy current sex preference
End Sub

Each time you click on one of the sex option buttons, the class is given the current state of these buttons.

Additional Methods in Subclasses

The OneSexSwimData class implements the SwimData interface, but we want it to have an additional method as well, which allows us to tell it which sex we want to display. The setSex method is not part of the SwimData interface, and thus if we just create a SwimData object and assign it the value of a new OneSexSwimData class instance, we won't have access to the setSex method.

Private swd As SwimData
Private tsd As SwimData
'-----
Private Sub Clone_Click()
    Set tsd = New OneSexSwimData
    swd.Clone tsd   'clone into any type
    tsd.sort        'call interface method

On the other hand, if we create an instance of the OneSexSwimData class, we won't have access to the methods of the SwimData interface.

Private swd As SwimData
Private osd As OneSexSwimData
'-----
Private Sub Clone_Click()
Set osd = tsd   'copy to specific type

We can solve this problem by creating a variable of each type and referring to the same class using both the SwimData and the OneSexSwimData variables.

Private swd As SwimData
Private tsd As SwimData
Private osd As OneSexSwimData
'-----
Private Sub Clone_Click()
    Set tsd = New OneSexSwimData
    swd.Clone tsd   'clone into any type
    tsd.sort        'call interface method

    Set osd = tsd   'copy to specific type
    osd.setSex "F"  'call derived class method
    SexFrame.Enabled = True 'enable sex selection
    loadRightList
End Sub

Note that we enable the SexFrame containing the F and M sex selection option buttons only when a clone has been performed. This prevents performing the setSex method on a class that has not yet been initialized.

Private Sub Sex_Click(Index As Integer)
'sets the sex of the class to either F or M
  osd.setSex Sex(Index).Caption
  loadRightList
End Sub

Dissimilar Classes with the Same Interface

Classes, however, do not have to be even that similar. The AgeSwimData class takes the cloned input data array and creates a simple histogram by age. If you click on “F,” you see the girls' age distribution and if you click on “M,” you see the boys' age distribution, as shown in Figure 14-4.

The AgeSwimData class displays an age distribution.

Figure 14-4. The AgeSwimData class displays an age distribution.

This is an interesting case where the AgeSwimData class uses all the interface methods of the base SwimData class and also uses the setSex method of the OneSexSwimData class we showed previously. We could just make the setSex method a new public method in our AgeSwimData class, or we could declare that AgeSwimData implements both interfaces.

'Class AgeSwimData
Implements OneSexSwimData
Implements SwimData

There is little to choose between them in this case, since there is only one extra method, setSex, in the OneSexSwimData class. However, the data are manipulated differently to create the histogram.

Private Sub SwimData_sort()
 Dim i As Integer, j As Integer
 Dim sw As Swimmer, age As Integer
 Dim ageString As String
 'Sort the data inbto increasing age order
 max = swimmers.Count
 ReDim sws(max) As Swimmer
 'copy the data into an array
 For i = 1 To max
   Set sws(i) = swimmers(i)
 Next i
 'sort by increasing age
 For i = 1 To max
   For j = i To max
     If sws(i).getAge > sws(j).getAge Then
       Set sw = sws(i)
       Set sws(i) = sws(j)
       Set sws(j) = sw
     End If
   Next j
 Next i
 'empty the collection
 For i = max To 1 Step -1
   swimmers.Remove i
 Next i
 'fill it with the sorted data
 For i = 1 To max
   swimmers.Add sws(i)
 Next i
 'create the histogram
 countAgeSex
End Sub
'----
Private Sub countAgeSex()
 Dim i As Integer, j As Integer
 Dim sw As Swimmer, age As Integer
 Dim ageString As String

 'now count number in each age
 Set ageList = New Collection
 age = swimmers(1).getAge
 ageString = ""
 i = 1
 While i <= max
   'add to histogram if in age and sex
   If age = swimmers(i).getAge And Sex = swimmers(i).getSex Then
     ageString = ageString & "X"
   End If
   If age <> swimmers(i).getAge And Sex = swimmers(i).getSex Then
     'create new swimmer if age changes
      Set sw = New Swimmer
      sw.setFirst Str$(age) 'put string of age in 1st name
      sw.setLast ageString  'put histogram in last name
      ageList.Add sw        'add to collection
      age = swimmers(i).getAge
      ageString = "X"       'start new age histogram
   End If
   i = i + 1
 Wend
 'copy last one in
  Set sw = New Swimmer
  sw.setFirst Str$(age)
  sw.setLast ageString
  ageList.Add sw
  amax = ageList.Count

End Sub

Now, since our original classes display first and last names of selected swimmers, note that we achieve this same display, returning Swimmer objects with the first name set to the age string and the last name set to the histogram.

The UML diagram in Figure 14-5 illustrates this system fairly clearly. The SwimInfo class is the main GUI class. It keeps two instances of SwimData but does not specify which ones. The TimeSwimData and SexSwimData classes are concrete classes derived from the abstract SwimData class, and the AgeSwimData class, which creates the histograms, is derived from the SexSwimData class.

The UML diagram for the various SwimData classes

Figure 14-5. The UML diagram for the various SwimData classes

You should also note that you are not limited to the few subclasses we demonstrated here. It would be quite simple to create additional concrete classes and register them with whatever code selects the appropriate concrete class. In our example program, the user is the deciding point or factory because he or she simply clicks on one of several buttons. In a more elaborate case, each concrete class could have an array of characteristics, and the decision point could be a class registry or prototype manager that examines this characteristic and selects the most suitable class. You could also combine the Factory Method pattern with the Prototype, where each of several concrete classes uses a different concrete class from those available.

Prototype Managers

A prototype manager class can be used to decide which of several concrete classes to return to the client. It can also manage several sets of prototypes at once. For example, in addition to returning one of several classes of swimmers, it could return different groups of swimmers who swam different strokes and distances. It could also manage which of several types of list boxes are returned in which to display them, including tables, multicolumn lists, and graphical displays. It is best that whichever subclass is returned, it does not require conversion to a new class type to be used in the program. In other words, the methods of the parent abstract or base class should be sufficient, and the client should never need to know which actual subclass it is dealing with.

Writing a Prototype in VB7

In VB7, we can write more or less the same code. The major changes are that we will use ArrayLists and zero-based arrays and we can write a base SwimData class from which we can inherit a number of useful methods. We create the base SwimData class without a sort method and specify using MustInherit for the class and MustOverride for the method that you must provide an implementation of sort in the child classes.

'Base class for SwimData
Public MustInherit Class SwimData
    Protected Swimmers As ArrayList
    Private index As Integer
    '-------
    'constructor to be used with setData
    Public Sub New()
        MyBase.New()
        index = 0
    End Sub
    '-----
    'Constructor to be used with filename
    Public Sub New(ByVal Filename As String)
        MyBase.New()
        Dim fl As New vbFile(Filename)
        Dim sw As Swimmer
        Dim sname As String

        swimmers = New ArrayList()
        Fl.OpenForRead(Filename)

        sname = fl.readLine
        While sname.length > 0
            If (sname.length > 0) Then
                sw = New Swimmer(sname)
                swimmers.Add(sw)
            End If
            sname = fl.readLine
        End While
        sort()
        index = 0
    End Sub
    '-------
    Public Sub setData(ByVal swcol As ArrayList)
        swimmers = swcol
        movefirst()
    End Sub
    '-------
    'Clone dataset from other swimdata object
    Public Sub Clone(ByVal swd As SwimData)
        Dim swmrs As New ArrayList()
        Dim i As Integer
        'copy data from one collection
        ' to another
        For i = 0 To swimmers.Count - 1
            swmrs.Add(swimmers(i))
        Next i
        'and put into new class
        swd.setData(swmrs)
    End Sub
    '-----
    'sorting method must be specified
    'in the child classes
    Public MustOverride Sub sort()
    '-----
    Public Sub MoveFirst()
        index = -1
    End Sub
    '-----
    Public Function hasMoreElements() As Boolean
        Return (index < (Swimmers.count - 1))
    End Function
    '-----
    Public Function getNextSwimmer() As Swimmer
        index = index + 1
        Return CType(swimmers(index), Swimmer)
    End Function

End Class

Note that we use the vbFile class we wrote earlier to read lines from the file. However, once we read the data, we parse each data line in the Swimmer class. Data conversions have a different form in VB7. Instead of using the Val function, we use the CInt function to convert integers.

sage = CInt(tok.nextToken)  'get age

We use the toSingle method to convert the time value.

stime = CSng(tok.nextToken)     'get time

This is the complete constructor for the Swimmer Class.

Public Class Swimmer
    Private ssex As String
    Private sage As Integer
    Private stime As Single
    Private sclub As String
    Private sfrname, slname As String
    '-----
    Public Sub New(ByVal nm As String)
        MyBase.New()
        Dim i As Integer
        Dim s As String
        Dim t As Single
        Dim tok As StringTokenizer

        tok = New StringTokenizer(nm, ",")
        nm = tok.nextToken
        i = nm.indexOf(" ")
        If i > 0 Then       'separate into first and last
            sfrname = nm.substring(0, i)
            slname = nm.substring(i + 1)
        Else
            sfrname = ""
            slname = nm       'or just use one
        End If
        sage = CInt(tok.nextToken)      'get age
        sclub = tok.nextToken           'get club
        stime = CSng(tok.nextToken)     'get time
        ssex = tok.nextToken            'get sex
    End Sub

The final running VB7 Prototype program is shown in Figure 14-6.

The VB.Net Prototype with a deep clone

Figure 14-6. The VB.Net Prototype with a deep clone

Then our TimeSwimData class is very simple, consisting only of the New methods and the sort method.

Public Class TimeSwimData
    Inherits SwimData
    '---------
    Public Sub New(ByVal filename As String)
        MyBase.New(filename)
    End Sub
    '---------
    Public Sub New()
        MyBase.New()
    End Sub
    '---------
    'Required sort method
    Public Overrides Sub sort()
        Dim i, j, max As Integer
        Dim sw As Swimmer
        max = swimmers.Count
        'copy into array
        Dim sws(max) As Swimmer
        swimmers.CopyTo(sws)
        'sort by time
        For i = 0 To max - 1
            For j = i To max - 1
                If sws(i).getTime > sws(j).getTime Then
                    sw = sws(i)
                    sws(i) = sws(j)
                    sws(j) = sw
                End If
            Next j
        Next i
        'copy back into new ArrayList
        swimmers = New Arraylist()
        For i = 0 To max - 1
            swimmers.Add(sws(i))
        Next i
    End Sub

End Class

Consequences of the Prototype Pattern

Using the Prototype pattern, you can add and remove classes at run time by cloning them as needed. You can revise the internal data representation of a class at run time, based on program conditions. You can also specify new objects at run time without creating a proliferation of classes.

One difficulty in implementing the Prototype pattern in VB is that if the classes already exist, you may not be able to change them to add the required clone methods. In addition, classes that have circular references to other classes cannot really be cloned.

Like the registry of Singletons discussed before, you can also create a registry of Prototype classes that can be cloned and ask the registry object for a list of possible prototypes. You may be able to clone an existing class rather than writing one from scratch.

Note that every class that you might use as a prototype must itself be instantiated (perhaps at some expense) in order for you to use a Prototype Registry. This can be a performance drawback.

Finally, the idea of having prototype classes to copy implies that you have sufficient access to the data or methods in these classes to change them after cloning. This may require adding data access methods to these prototype classes so that you can modify the data once you have cloned the class.

Thought Question

Thought Question

An entertaining banner program shows a slogan starting at different places on the screen at different times and in different fonts and sizes. Design the program using a Prototype pattern.

Programs on the CD-ROM

PrototypeAgeplot VB6 age plot
PrototypeDeepProto VB6 deep prototype
PrototypeOneSex VB6 display by sex
PrototypeSimpleProto VB6 shallow copy
PrototypeTwoclassAgePlot VB6 age and sex display
PrototypeVBNetDeepProt VB7 deep prototype

Summary of Creational Patterns

The Factory pattern is used to choose and return an instance of a class from a number of similar classes, based on data you provide to the factory.

The Abstract Factory pattern is used to return one of several groups of classes. In some cases, it actually returns a Factory for that group of classes.

The Builder pattern assembles a number of objects to make a new object, based on the data with which it is presented. Frequently, the choice of which way the objects are assembled is achieved using a Factory.

The Prototype pattern copies or clones an existing class, rather than creating a new instance, when creating new instances is more expensive.

The Singleton pattern is a pattern that ensures there is one and only one instance of an object and that it is possible to obtain global access to that one instance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset