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.
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.
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.
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.
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
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.
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.
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
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.
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.
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.
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.
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.
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
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.
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.
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 |
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.