We've just seen a couple of examples of the simplest of factories. The factory concept recurs all throughout object-oriented programming, and we find a few examples embedded in VB itself and in other design patterns (such as the Builder pattern). In these cases a single class acts as a traffic cop and decides which subclass of a single hierarchy will be instantiated.
The Factory Method pattern is a clever but subtle extension of this idea, where no single class makes the decision as to which subclass to instantiate. Instead, the superclass defers the decision to each subclass. This pattern does not actually have a decision point where one subclass is directly selected over another class. Instead, programs written to this pattern define an abstract class that creates objects but lets each subclass decide which object to create.
We can draw a pretty simple example from the way that swimmers are seeded into lanes in a swim meet. When swimmers compete in multiple heats in a given event, they are sorted to compete from slowest in the early heats to fastest in the last heat and arranged within a heat with the fastest swimmers in the center lanes. This is referred to as straight seeding.
Now, when swimmers swim in championships, they frequently swim the event twice. During preliminaries everyone competes, and the top 12 or 16 swimmers return to compete against each other at finals. In order to make the preliminaries more equitable, the top heats are circle seeded: The fastest three swimmers are in the center lane in the fastest three heats, the second fastest three swimmers are in the next to center lane in the top three heats, and so forth.
So, how do we build some objects to implement this seeding scheme and illustrate the Factory Method. First, let's design an abstract Events class.
Option Explicit 'Class Events Private numLanes As Integer Private swimmers As New Collection 'list of swimmers '----- Public Sub init(Filename As String, lanes As Integer) End Sub '----- Public Function getSwimmers() As Collection Set getSwimmers = swimmers End Function '----- Public Function isPrelim() As Boolean End Function '----- Public Function isFinal() As Boolean End Function '----- Public Function isTimedFinal() As Boolean End Function '----- Public Function getSeeding() As Seeding End Function
Grammatically, it would have been better to call this an “Event” class, but “Event” is a reserved word in VB6.
This defines the remaining methods simply without any necessity of filling them in. Then we can implement concrete classes from the Events class, called PrelimEvent and TimedFinalEvent. The only difference between these classes is that one returns one kind of seeding and the other returns a different kind of seeding.
We also define an abstract Seeding class with the following methods.
'Class Seeding Private numLanes As Integer Private laneOrder As Collection Dim asw() As Swimmer '----- Public Function getSeeding() As Collection End Function '----- Public Function getHeat() As Integer End Function '----- Public Function getCount() As Integer End Function '----- Public Sub seed() End Sub '----- Public Function getSwimmers() As Collection End Function '----- Public Function getHeats() As Integer End Function '----- Private Function odd(n As Integer) As Boolean odd = (n 2) * 2 <> n End Function '----- Public Function calcLaneOrder(lns As Integer) As Collection numLanes = lns 'This function is implemented but not shown here ReDim lanes(numLanes) As Integer End Function '----- Public Sub init(swmrs As Collection, lanes As Integer) End Sub '----- Public Function sort(sw As Collection) As Collection ReDim asw(sw.count) As Swimmer 'This function is implemented but not shown here End Function
Note that we actually included code for the calcLaneOrder and sort functions but omit the code here for simplicity. The derived classes then each create an instance of the base Seeding class to call these functions.
We can then create two concrete seeding subclasses: StraightSeeding and CircleSeeding. The PrelimEvent class will return an instance of CircleSeeding, and the TimedFinalEvent class will return an instance of StraightSeeding. Thus, we see that we have two hierarchies: one of Events and one of Seedings. We see these two hierarchies illustrated in Figures 10-1 and 10-2.
In the Events hierarchy, you will see that both derived Events classes contain a getSeeding method. One of them returns an instance of StraightSeeding and the other an instance of CircleSeeding. So you see, there is no real factory decision point as we had in our simple example. Instead, the decision as to which Event class to instantiate is the one that determines which Seeding class will be instantiated.
While it looks like there is a one-to-one correspondence between the two class hierarchies, there needn't be. There could be many kinds of Events and only a few kinds of Seeding used.
We haven't said much about the Swimmer class, except that it contains a name, club age, seed time, and place to put the heat and lane after seeding. The Event class reads in the Swimmers from some database (a file in our example) and then passes that Collection to the Seeding class when you call the getSeeding method for that event.
We have seen the previous abstract base Events class. In actual use, we use it to read in the swimmer data (here from a file) and pass it on to instances of the Swimmer class to parse.
Option Explicit 'Class Events Private numLanes As Integer Private swimmers As New Collection 'list of swimmers '----- Public Sub init(Filename As String, lanes As Integer) Dim f As Integer, s As String Dim sw As Swimmer Dim fl As New vbFile 'read in the data file in the constructor f = FreeFile numLanes = lanes Set swimmers = New Collection 'read in swimmers from file Filename = App.Path & "" & Filename fl.OpenForRead Filename s = fl.readLine While (Not fl.fEof) Set sw = New Swimmer 'create each swimmer sw.init s 'and initialize it swimmers.Add sw 'add to list s = fl.readLine 'read another Wend Close #f End Sub '----- Public Function getSwimmers() As Collection Set getSwimmers = swimmers End Function '----- Public Function isPrelim() As Boolean End Function '----- Public Function isFinal() As Boolean End Function '----- Public Function isTimedFinal() As Boolean End Function '----- Public Function getSeeding() As Seeding End Function
The base Event class has empty methods for whether the event is a prelim, final, or timed final event. We fill in the event in the derived classes.
Our PrelimEvent class just returns an instance of CircleSeeding.
'Class PrelimEvent Implements Events Private numLanes As Integer Private swimmers As Collection Private evnts As New Events Private sd As Seeding Private Sub Class_Initialize() Set evnts = New Events End Sub '----- Private Function Events_getSeeding() As Seeding Set sd = New CircleSeeding sd.init swimmers, numLanes Set Events_getSeeding = sd End Function '----- Private Function Events_getSwimmers() As Collection Set Events_getSwimmers = swimmers End Function '----- Private Function Events_isFinal() As Boolean Events_isFinal = False End Function '----- Private Function Events_isPrelim() As Boolean Events_isPrelim = True End Function '----- Private Function Events_isTimedFinal() As Boolean Events_isTimedFinal = False End Function '----- Private Sub Events_init(Filename As String, lanes As Integer) evnts.init Filename, lanes numLanes = lanes Set swimmers = evnts.getSwimmers End Sub
The TimedFinalEvent returns an instance of StraightSeeding.
'Class PrelimEvent Implements Events Private numLanes As Integer Private swimmers As Collection Private evnts As New Events Private sd As Seeding Private Sub Class_Initialize() Set evnts = New Events End Sub '----- Private Function Events_getSeeding() As Seeding Set sd = New CircleSeeding sd.init swimmers, numLanes Set Events_getSeeding = sd End Function
In both cases our events classes contain an instance of the base Events class, which we use to read in the data files.
In actually writing this program, we'll discover that most of the work is done in straight seeding. The changes for circle seeding are pretty minimal. So we instantiate our StraightSeeding class and copy in the Collection of swimmers and the number of lanes.
Private Sub Seeding_seed() Dim lastHeat As Integer, lastlanes As Integer Dim heats As Integer, i As Integer, j As Integer Dim swmr As Swimmer Set sw = sd.sort(sw) Set laneOrder = sd.calcLaneOrder(numLanes) count = sw.count lastHeat = count Mod numLanes If (lastHeat < 3) And lastHeat > 0 Then lastHeat = 3 'last heat must have 3 or more End If count = sw.count lastlanes = count - lastHeat numheats = lastlanes / numLanes If (lastHeat > 0) Then numheats = numheats + 1 End If heats = numheats 'place heat and lane in each swimmer's object 'details on CDROM 'Add in last partial heat 'details on CDROM End Sub
This makes the entire array of seeded Swimmers available when you call the getSwimmers method.
The CircleSeeding class is derived from StraightSeeding, so it copies in the same data.
'Circle seeding method Private Sub Seeding_seed() Dim i As Integer, j As Integer, k As Integer 'get the lane order] Set laneOrder = sd.calcLaneOrder(numLanes) Set sw = sd.sort(sw) 'sort the swimmers strSd.init sw, numLanes 'create Straight Seeding object strSd.seed 'seed into straight order numheats = strSd.getHeats 'get the total number of heats If (numheats >= 2) Then If (numheats >= 3) Then circlesd = 3 'seed either 3 Else circlesd = 2 'or 2 End If i = 1 'copy seeding info into swimmers data 'details on CDROM End If End Sub
Since the circle seeding calls straight seeding, it copies the swimmer collection and lanes values. Then our call to strSd.seed does the straight seeding. This simplifies things because we will always need to seed the remaining heats by straight seeding. Then we seed the last two or three heats as just shown, and we are done with that type of seeding as well.
In this example, we took a list of swimmers from the Web who competed in the 500-yard freestyle and the 100-yard freestyle and used them to build our TimedFinalEvent and PrelimEvent classes. You can see the results of these two seedings in Figure 10-3.
Now one issue that we have skipped over is how the program that reads in the swimmer data decides which kind of event to generate. We finesse this here by simply creating the correct type of event when we read in the data. This code is in the Form_Load event.
Dim ev As Events Set ev = New PrelimEvent 'create a Prelim/final event ev.init "100free.txt", 6 'read in the data Seedings.Add ev.getSeeding 'get the seeding and add to collection lsEvents.AddItem "100 Free" Set ev = New TimedFinalEvent 'create a new Timed final event ev.init "500free.txt", 6 'read in the data Seedings.Add ev.getSeeding 'get the seeeding lsEvents.AddItem "500 Free" 'and add to collection
Clearly, this is an instance where an EventFactory may be needed to decide which kind of event to generate. This revisits the simple factory with which we began the discussion.
In VB7, we can make effective use of inheritance to make each of these classes substantially simpler. For example, the Events class is an abstract class where we fill in the methods in the derived TimedFinalEvent and PrelimEvent classes. In VB7, these classes differ in that we put the file reading methods in the base Seeding class and let them be used by the derived classes, whereas in VB6, we had to create an instance of the base Event class inside the TimedFinal and Prelim event classes and call its functions specifically. The basic abstract class for Events is now simply the following.
Public Class Events Protected numLanes As Integer Protected swmmers As Swimmers '----- Public Sub New(ByVal Filename As String, _ ByVal lanes As Integer) MyBase.New() Dim s As String Dim sw As Swimmer Dim fl As vbFile fl = New vbFile(filename) 'Open the file fl.OpenForRead() numLanes = lanes 'Remember lane number swmmers = New Swimmers() 'list of kids 'read in swimmers from file s = fl.ReadLine While Not fl.feof sw = New Swimmer(s) 'create each swimmer swmmers.Add(sw) 'add to list s = fl.ReadLine 'read another End While fl.closeFile() End Sub '----- Public Function getSwimmers() As ArrayList Return swmmers End Function '----- Public Overridable Function isPrelim() As Boolean End Function '----- Public Overridable Function isFinal() As Boolean End Function '----- Public Overridable Function isTimedFinal() As Boolean End Function '----- Public Overridable Function getSeeding() As Seeding End Function End Class
Then our TimedFinalEvent is derived from that and creates an instance of the StraightSeeding class.
Public Class TimedFinalEvent Inherits Events Public Sub New(ByVal Filename As String, _ ByVal lanes As Integer) MyBase.New(Filename, lanes) End Sub '----- Public Overrides Function getSeeding() As Seeding Dim sd As Seeding 'create seeding and execute it sd = New StraightSeeding(swmmers, numLanes) sd.seed() Return sd End Function '----- Public Overrides Function isFinal() As Boolean Return False End Function '----- Public Overrides Function isPrelim() As Boolean Return False End Function '----- Public Overrides Function isTimedFinal() As Boolean Return True End Function End Class
The PrelimEvent class is basically the same, except that we create an instance of circle seeding and set the prelim and finals flags differently. Here is the getSeeding method.
Public Overrides Function getSeeding() As Seeding Return New CircleSeeding(swmmers, numLanes) End Function
In a similar fashion, the base Seeding class contains the functions sort and getLaneOrder, and the derived classes for Straight and Circle seeding contain only the changed seed methods. You can see the class inheritance structure of this VB7 version in Figure 10-4.
You should consider using a Factory method in the following situations.
A class can't anticipate which kind of class of objects it must create.
A class uses its subclasses to specify which objects it creates.
You want to localize the knowledge of which class gets created.
There are several variations on the factory pattern to recognize.
The base class is abstract and the pattern must return a complete working class.
The base class contains default methods and these methods are called unless the default methods are insufficient.
Parameters are passed to the factory telling it which of several class types to return. In this case the classes may share the same method names but may do something quite different.
Seeding in track is carried out from inside to outside lanes. What classes would you need to develop to carry out tracklike seeding as well?