In this chapter we'll consider how to use the Builder pattern to construct objects from components. We have already seen that the Factory pattern returns one of several different subclasses, depending on the data passed in arguments to the creation methods. But suppose we don't want just a computing algorithm but a whole different user interface because of the data we need to display. A typical example might be your e-mail address book. You probably have both individual people and groups of people in your address book, and you would expect the display for the address book to change so that the People screen has places for first and last name, company, e-mail address, and phone number.
On the other hand, if you were displaying a group address page, you'd like to see the name of the group, its purpose, and a list of members and their e-mail addresses. You click on a person and get one display and on a group and get the other display. Let's assume that all e-mail addresses are kept in an object called an Address and that people and groups are derived from this base class, as shown in Figure 13-1.
Depending on which type of Address object we click on, we'd like to see a somewhat different display of that object's properties. This is a little more than just a Factory pattern because we aren't returning objects that are simple descendants of a base display object but totally different user interfaces made up of different combinations of display objects. The Builder pattern assembles a number of objects, such as display controls, in various ways, depending on the data. Furthermore, by using classes to represent the data and forms to represent the display, you can cleanly separate the data from the display methods into simple objects.
Let's consider a somewhat simpler case where it would be useful to have a class build our UI for us. Suppose we are going to write a program to keep track of the performance of our investments. We might have stocks, bonds, and mutual funds, and we'd like to display a list of our holdings in each category so we can select one or more of the investments and plot their comparative performance.
Even though we can't predict in advance how many of each kind of investment we might own at any given time, we'd like to have a display that is easy to use for either a large number of funds (such as stocks) or a small number of funds (such as mutual funds). In each case, we want some sort of a multiple-choice display so that we can select one or more funds to plot. If there is a large number of funds, we'll use a multichoice list box, and if there are three or fewer funds, we'll use a set of check boxes. We want our Builder class to generate an interface that depends on the number of items to be displayed and yet have the same methods for returning the results.
Our displays are shown in Figure 13-2. The left display contains a large number of stocks, and the right contains a small number of bonds.
Now let's consider how we can build the interface to carry out this variable display. We'll start with a multiChoice interface that defines the methods we need to implement.
'Interface MultiChoice 'This is the interface for the multi-Select windows '----- 'get collection of all selected stocks Public Function getSelected() As Collection End Function '----- 'get window containing multichoice controls Public Function getWindow() As Form End Function '----- 'store list of stocks Public Sub init(stocks As Collection) End Sub
The getWindow method returns a window with a multiple-choice display. The two displays we're using here, a check box panel or a list box panel, implement this interface.
'checkBox form Implements MultiChoice
or
'Listbox form Implements MultiChoice
Then we create a simple Factory class that decides which of these two classes to return.
'Class StockFactory 'gets correct window for number of stocks presented Public Function getBuilder(stocks As Collection) Dim mult As MultiChoice If stocks.Count <= 3 Then Set mult = New checkForm 'get check box form Else Set mult = New listForm 'get list box form End If mult.init stocks 'initialize it Set getBuilder = mult End Function
In the language of Design Patterns, this simple factory class is called the Director, and the actual classes derived from multiChoice are each Builders.
Visual Basic 6 doesn't make it easy to create and change forms dynamically during program execution, so we will instead create instances of the various types of multiChoice windows as components of an MDI form. Then we'll instantiate the one needed by the number of equities in a specific category.
Our user interface consists of an MDI form with the list of fund choices on the left and initially nothing on the right. Since windows grow to fill the available space, will fill the right space initially with a blank form, as shown in Figure 13-3.
We first define the interface Equities.
'Interface to Equities class Public Function getFunds() As Collection 'returns list of fund names End Function
Then we'll keep our three lists of investments in three classes called Stocks, Bonds, and Mutuals, each of which implements Equities. We create an instance of each of them in the Funds class and fetch one or the other of them from that class.
'Class funds Private fundList As New Collection Private eq As Equities Private Sub Class_Initialize() 'Creates a collection of equities fundList.Add New stocks fundList.Add New Bonds fundList.Add New Mutuals End Sub '----- Public Function getFund(i As Integer) As Equities 'return selected equity from collection If i > 0 And i <= fundList.Count Then Set getFund = fundList(i) End If End Function
We load them with arbitrary values as part of program initialization.
'Class stocks Implements Equities Private stockList As New Collection Private Sub Class_Initialize() 'add in arbitrary list of stocks stockList.Add "Cisco" stockList.Add "Coca Cola" stockList.Add "GE" stockList.Add "Harley Davidson" stockList.Add "IBM" stockList.Add "Microsoft" End Sub '----- Private Function Equities_getFunds() As Collection 'return collection of stocks Set Equities_getFunds = stockList End Function
It is the same for Bonds and Mutuals. In a real system, we'd probably read them in from a file or database. Then, when the user clicks on one of the three investment types in the left list box, we get the correct Equity from the Funds class and use it to create the right display.
Private Sub fundList_Click() Dim i As Integer, selStocks As Collection, eq As Equities 'catch list box click selecting fund type i = fundList.ListIndex + 1 If (i > 0) Then 'get one type of fund Set eq = fnds.getFund(i) 'get equit from funds class Set selStocks = eq.getFunds 'get list from equity 'get a multiChoice form from the factory Set mchoice = sfact.getBuilder(selStocks) 'tell the parent MDI to show it mParent.setShowForm mchoice End If End Sub
The simpler of the two Builders is the List Box Builder. It returns a form containing a list box showing the list of the investments in that equity type.
'Listbox form Implements MultiChoice Private sels As Collection '----- Private Sub MultiChoice_init(stocks As Collection) Private i As Integer For i = 1 To stocks.Count List1.AddItem stocks(i) Next i End Sub
The other important method is the getSelected method that returns a collection of Strings of the investments the user selects.
Private Function MultiChoice_getSelected() As Collection Dim i As Integer Set sels = New Collection For i = 0 To List1.ListCount - 1 If List1.Selected(i) Then sels.Add List1.List(i) End If Next i Set MultiChoice_getSelected = sels End Function
The Check Box Builder is also quite simple. Here we need to find out how many elements are to be displayed and display them. The number can only be between 0 and 3, so we create them all in advance and display only those we need.
Private Sub MultiChoice_init(stocks As Collection) Dim i As Integer 'set captions for the check boxes we are using For i = 1 To stocks.Count ckFunds(i - 1).Caption = stocks(i) Next i 'make the rest invisible For i = stocks.Count + 1 To 3 ckFunds(i).Visible = False Next i End Sub
The getSelected method is analogous to the preceding one. It is shown here. We illustrate the final UML class diagram in Figure 13-4.
Implements MultiChoice Private sels As Collection '----- Private Function MultiChoice_getSelected() As Collection Dim i As Integer 'create collection of checked stock names Set sels = New Collection For i = 1 To 3 If ckFunds(i - 1).Value = 1 Then sels.Add ckFunds(i - 1).Caption End If Next i 'return collection to caller Set MultiChoice_getSelected = sels End Function
VB7 gives us considerably more flexibility in designing Builder classes, since we have direct access to the methods that allow us to construct a window from basic components. For this example, we'll let each builder construct a Panel containing whatever components it needs. We can then add that Panel to the form and position it. When the display changes, you remove the old Panel and add a new one. VB6 does not have a Panel class, but in VB7, a Panel is just an unbordered container that can hold any number of Windows components. As we did previously, the two implementations of the Panel will satisfy the MultiChoice interface.
Public Interface MultiChoice 'an interface to any group of components 'that can return zero or more selected items 'the names are returned in an Arraylist Function getSelected() As ArrayList Sub clear() 'clear all selected Function getWindow() As Panel End Interface
We will create a base abstract class called Equities and derive the stocks, bonds, and mutual funds from it.
Public MustInherit Class Equities Protected ar As Arraylist '----- Public Overrides Function toString() As String 'must override this one End Function '----- Public Function getNames() As ArrayList Return ar End Function '----- Public Function count() As Integer Return ar.count End Function '----- End Class
Note the toString method. We'll use this to display each kind of equity in the list box. Now our Stocks class will just contain the code to load the ArrayList with the stock names.
Public Class Stocks Inherits Equities '----- Public Sub New() MyBase.New() ar = New ArrayList() ar.Add("Cisco") ar.Add("Coca Cola") ar.Add("GE") ar.Add("Harley Davidson") ar.Add("IBM") ar.Add("Microsoft") End Sub '----- Public Overrides Function toString() As String Return "Stocks" End Function End Class
All the remaining code (getNames and count) is implemented in the base Equities class. The Bonds and Mutuals classes are equally simple.
We need a little class to decide whether we want to return a check box panel or a list box panel. We'll call this class the StockFactory class. However, we will never need more than one instance of this class, so we'll create the class so its one method is Shared.
Public Class StockFactory 'This class has only one shared method Public Shared Function getBuilder(ByVal _ stocks As Equities) _ As MultiChoice Dim mult As MultiChoice If stocks.Count <= 3 Then 'get check boxes mult = New checkChoice(stocks) Else 'get a list box mult = New listChoice(stocks) End If Return mult End Function End Class
Our Check Box Builder constructs a panel containing 0 to 3 check boxes and returns that panel to the calling program.
Public Class CheckChoice Implements MultiChoice Private stocks As ArrayList Private pnl As Panel Private boxes As ArrayList '----- 'create a Panel containing '0 to 3 check boxes Public Sub New(ByVal stks As Equities) MyBase.New() stocks = stks.getNames pnl = New Panel() boxes = New Arraylist() 'in an ArrayList Dim i As Integer For i = 0 To stocks.count - 1 Dim Ck As New Checkbox() Ck.Location = _ New System.Drawing.Point(8, 16 + i * 32) Ck.Text = stocks(i).toString Ck.Size = New Size(112, 24) Ck.TabIndex = 0 Ck.TextAlign = _ ContentAlignment.MiddleLeft boxes.add(ck) 'internal array pnl.Controls.add(ck) 'add into panel Next i End Sub End Class
The methods for returning the window and the list of selected names are shown here. Note that we use the Ctype function to convert between the Object type returned by an ArrayList and the Checkbox type the method actually requires.
'clear all selected check boxes Public Sub clear() Implements MultiChoice.clear Dim i As Integer Dim ck As Checkbox For i = 0 To boxes.count - 1 ck = CType(boxes(i), Checkbox) ck.Checked = False Next i End Sub '-------- 'gets list of selected names Public Function getSelected() As ArrayList _ Implements MultiChoice.getSelected Dim ar As New ArrayList() Dim i As Integer Dim ck As Checkbox For i = 0 To Boxes.count - 1 ck = CType(boxes(i), Checkbox) If ck.Checked Then ar.add(ck.Text) End If Next i Return ar End Function '-------- 'gets the Panel containing the check boxes Public Function getWindow() As Panel _ Implements MultiChoice.getWindow Return pnl End Function
This class creates a multiselect list box, inserts it into a Panel, and loads the names into the list.
Public Class ListChoice Implements MultiChoice Private stocks As ArrayList Private pnl As Panel Private lst As ListBox '-------- 'create a panel containing a 'multiselectable list box Public Sub New(ByVal stks As Equities) MyBase.New() stocks = stks.getNames 'get the names pnl = New Panel() 'create the list box lst = New ListBox() lst.Location = New Point(16, 0) lst.Size = New Size(120, 160) lst.SelectionMode = _ SelectionMode.MultiExtended lst.TabIndex = 0 'add it into the panel pnl.Controls.Add(lst) 'add the names into the list Dim i As Integer For i = 0 To stks.count - 1 lst.items.add(stocks(i)) Next i End Sub
Since this is a multiselect list box, we can get all the selected items in a single SelectedIndices collection. This method, however, only works for a multiselect list box. It returns a –1 for a single-select list box. We use it to load the array list of selected names as follows.
Public Function getSelected() As ArrayList _ Implements MultiChoice.getSelected Dim i As Integer Dim item As String Dim arl As New ArrayList() 'get items and put in ArrayList For i = 0 To lst.SelectedIndices.count - 1 item = lst.Items( _ lst.SelectedIndices(i)).toString arl.add(item) Next i Return arl 'return the ArrayList End Function '----- 'clear all selected items Public Sub clear() Implements MultiChoice.clear lst.Items.clear() End Sub '----- 'return the constructed panel Public Function getWindow() As Panel _ Implements MultiChoice.getWindow Return pnl End Function
You are not limited to populating a list box with strings in VB7. When you add data to the Items collection, it can be any kind of object that has a toString method. This takes the place of the much more limited Itemdata property of the listbox in VB6.
Since we created our three Equities classes to have a toString method, we can add them directly to the list box in our main program's constructor.
Public Class wBuilder Inherits System.Windows.Forms.Form Private Pnl As Panel Private mchoice As MultiChoice Private eq As Equities '----- Private Sub init() lsEqTypes.Items.Add(New Stocks()) lsEqTypes.Items.Add(New Bonds()) lsEqTypes.Items.Add(New Mutuals()) End Sub
Whenever we click on a line of the list box, the click method obtains that instance of an Equities class and passes it to the MultiChoice factory, which in turn produces a Panel containing the items in that class. It then removes the old panel and adds the new one.
Private Sub lsEqTypes_SelectedIndexChanged( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles lsEqTypes.SelectedIndexChanged Dim i As Integer i = lsEqTypes.SelectedIndex 'get the Equity from the list box eq = CType(lsEqTypes.Items(i), Equities) 'get the right Builder mchoice = StockFactory.getBuilder(eq) 'remove the old panel Me.Controls.Remove(Pnl) 'get the new one and add it to the window Pnl = mchoice.getWindow setPanel() End Sub
Now that we have created all the needed classes, we can run the program. It starts with a blank panel on the right side, so there will always be some panel there to remove. Then each time we click on one of the names of the Equities, that panel is removed and a new one is added in its place. We see the three cases in Figure 13-5.
You can see the relationships between the classes in the UML diagram in Figure 13-6.
A Builder lets you vary the internal representation of the product it builds. It also hides the details of how the product is assembled.
Each specific Builder is independent of the others and of the rest of the program. This improves modularity and makes the addition of other Builders relatively simple.
Because each Builder constructs the final product step by step, depending on the data, you have more control over each final product that a Builder constructs.
A Builder pattern is somewhat like an Abstract Factory pattern in that both return classes made up of a number of methods and objects. The main difference is that while the Abstract Factory returns a family of related classes, the Builder constructs a complex object step by step, depending on the data presented to it.
Some word-processing and graphics programs construct menus dynamically based on the context of the data being displayed. How could you use a Builder effectively here?
Not all Builders must construct visual objects. What might you construct with a Builder in the personal finance industry? Suppose you were scoring a track meet, made up of five or six different events. How can you use a Builder there?