The Flyweight pattern is used to avoid the overhead of large numbers of very similar classes. There are cases in programming where it seems that you need to generate a very large number of small class instances to represent data. Sometimes you can greatly reduce the number of different classes that you need to instantiate if you can recognize that the instances are fundamentally the same except for a few parameters. If you can move those variables outside the class instance and pass them in as part of a method call, the number of separate instances can be greatly reduced by sharing them.
The Flyweight design pattern provides an approach for handling such classes. It refers to the instance's intrinsic data that makes the instance unique and the extrinsic data that is passed in as arguments. The Flyweight is appropriate for small, fine-grained classes like individual characters or icons on the screen. For example, you might be drawing a series of icons on the screen in a window, where each represents a person or data file as a folder, as shown in Figure 20-1.
Figure 20-1. A set of folders representing information about various people. Since these are so similar, they are candidates for the Flyweight pattern.
In this case, it does not make sense to have an individual class instance for each folder that remembers the person's name and the icon's screen position. Typically, these icons are one of a few similar images, and the position where they are drawn is calculated dynamically based on the window's size in any case.
In another example in Design Patterns, each character in a Document is represented as a single instance of a character class, but the positions where the characters are drawn on the screen are kept as external data, so there only has to be one instance of each character, rather than one for each appearance of that character.
Flyweights are sharable instances of a class. It might at first seem that each class is a Singleton, but in fact there might be a small number of instances, such as one for every character or one for every icon type. The number of instances that are allocated must be decided as the class instances are needed, and this is usually accomplished with a FlyweightFactory class. This Factory class usually is a Singleton, since it needs to keep track of whether a particular instance has been generated yet. It then either returns a new instance or a reference to one it has already generated.
To decide if some part of your program is a candidate for using Flyweights, consider whether it is possible to remove some data from the class and make it extrinsic. If this makes it possible to greatly reduce the number of different class instances your program needs to maintain, this might be a case where Flyweights will help.
Suppose we want to draw a small folder icon with a name under it for each person in an organization. If this is a large organization, there could be a large number of such icons, but they are actually all the same graphical image. Even if we have two icons—one for “is Selected” and one for “not Selected”—the number of different icons is small. In such a system, having an icon object for each person, with its own coordinates, name, and selected state, is a waste of resources. We show two such icons in Figure 20-2.
Instead, we'll create a FolderFactory that returns either the selected or the unselected folder drawing class but does not create additional instances once one of each has been created. Since this is such a simple case, we just create them both at the outset and then return one or the other.
'Class FolderFactory 'Returns selected or unselected folder Private Selected As Folder, unSelected As Folder Const selColor = vbActiveTitlebar '--------- Public Sub init(Pic As PictureBox) 'create one instance of each of 2 folders Set Selected = New Folder Selected.init Pic, selColor Set unSelected = New Folder unSelected.init Pic, vbYellow End Sub '--------- Public Function getFolder(isSelected As Boolean) As Folder If isSelected Then Set getFolder = Selected Else Set getFolder = unSelected End If End Function
For cases where more instances could exist, the Factory could keep a table of those it had already created and only create new ones if they weren't already in the table.
The unique thing about using Flyweights, however, is that we pass the coordinates and the name to be drawn into the folder when we draw it. These coordinates are the extrinsic data that allow us to share the folder objects and, in this case, create only two instances. The complete folder class shown here simply creates a folder instance with one background color or the other and has a public Draw method that draws the folder at the point you specify.
'Class Folder 'draws a folder on the picture box panel Private Pic As PictureBox Private bColor As Long Private Const w As Integer = 50, h As Integer = 30 Private Const Gray As Long = vbWindowBackground '--------- Public Sub init(pc As PictureBox, bc As Long) Set Pic = pc bColor = bc End Sub '--------- Public Sub draw(X As Integer, Y As Integer, title As String) Pic.Line (X, Y)-(X + w, Y + h), bColor, BF Pic.Line (X, Y)-(X + w, Y + h), vbBlack, B Pic.Line (X + 1, Y + 1)-(X + w - 1, Y + 1), vbWhite Pic.Line (X + 1, Y)-(X + 1, Y + h), vbWhite Pic.Line (X + 5, Y)-(X + 15, Y - 5), bColor, BF Pic.Line (X + 5, Y)-(X + 15, Y - 5), vbBlack, B Pic.Line (X, Y + h - 1)-(X + w, Y + h - 1), Gray Pic.Line (X + w - 1, Y)-(X + w - 1, Y + h - 1), Gray Pic.PSet (X, Y + h + 5), Pic.BackColor Pic.Print title; End Sub
To use a Flyweight class like this, your main program must calculate the position of each folder as part of its paint routine and then pass the coordinates to the folder instance. This is actually rather common, since you need a different layout, depending on the window's dimensions, and you would not want to have to keep telling each instance where its new location is going to be. Instead, we compute it dynamically during the paint routine.
Here we note that we could have generated an array or Collection of folders at the outset and simply scan through the array to draw each folder.
For i = 1 To names.Count Set fol = folders(i) 'get a folder fol.draw X, Y, names(i) 'and draw it cnt = cnt + 1 If cnt > HCount Then cnt = 1 X = pLeft Y = Y + VSpace Else X = X + HSpace End If Next I
Such an array is not as wasteful as a series of different instances because it is actually an array of references to one of only two folder instances. However, since we want to display one folder as “selected,” and we would like to be able to change which folder is selected dynamically, we just use the FolderFactory itself to give us the correct instance each time.
Private Sub Form_Paint() 'repaint entire pictureBox Dim i As Integer Dim X As Integer, Y As Integer X = pLeft Y = pTop cnt = 1 'go through all names For i = 1 To names.count 'get one kind of folder or other Set fol = factory.getFolder(names(i) = selectedName) fol.draw X, Y, names(i) cnt = cnt + 1 If cnt > HCount Then cnt = 1 X = pLeft Y = Y + VSpace Else X = X + HSpace End If Next i End Sub
The diagram in Figure 20-3 shows how these classes interact.
The FlyCanvas class is the main UI class, where the folders are arranged and drawn. It contains one instance of the FolderFactory and one instance of the Folder class. The FolderFactory class contains two instances of Folder: selected and unselected. One or the other of these is returned to the FlyCanvas by the FolderFactory.
Since we have two folder instances, selected and unselected, we'd like to be able to select folders by moving the mouse over them. In the previous paint routine, we simply remember the name of the folder that was selected and ask the factory to return a “selected” folder for it. Since the folders are not individual instances, we can't listen for mouse motion within each folder instance. In fact, even if we did listen within a folder, we'd need a way to tell the other instances to deselect themselves.
Instead, we check for mouse motion at the Picturebox level, and if the mouse is found to be within a Rectangle, we make that corresponding name the selected name. We create a single instance of a Rectangle class where the testing can be done as to whether a folder contains the mouse at that instant.
'Class Rectangle 'used to find out if an x,y coordinate 'lies within a rectangle area Private x1 As Integer Private y1 As Integer Private x2 As Integer Private y2 As Integer Private w As Integer Private h As Integer '-------- Public Function contains(X As Single, Y As Single) As Boolean If x1 <= X And X <= x2 And y1 <= Y And Y <= y2 Then contains = True Else contains = False End If End Function '-------- Public Sub init(x1_ As Integer, y1_ As Integer) x1 = x1_ x2 = x1 + w y1 = y1_ y2 = y1 + h End Sub '-------- Public Sub setSize(w_ As Integer, h_ As Integer) w = w_ h = h_ End Sub
This allows us to just check each name when we redraw and create a selected folder instance where it is needed.
Private Sub Pic_MouseMove(Button As Integer, Shift As Integer, _ mX As Single, mY As Single) Dim i As Integer, found As Boolean Dim X As Integer, Y As Integer 'go through folder list 'looking to see if mouse posn 'is inside any of them X = pLeft Y = pTop cnt = 1 i = 1 found = False selectedName = "" While i <= names.count And Not found rect.init X, Y If rect.contains(mX, mY) Then selectedName = names(i) 'save that name found = True End If cnt = cnt + 1 If cnt > HCount Then cnt = 1 X = pLeft Y = Y + VSpace Else X = X + HSpace End If i = i + 1 Wend Refresh End Sub
You can write very similar code in VB7 to handle this Flyweight pattern. Since we create only two instances of the Folder class and then select one or the other using a FolderFactory, we do not make any use of inheritance. Instead, our FolderFactory creates two instances in the constructor and returns one or the other.
Public Class FolderFactory Private selFolder, unselFolder As Folder '----- Public Sub New() 'create the two folders selFolder = New Folder(Color.Brown) unselFolder = New Folder(color.Bisque) End Sub '----- Public Function getFolder(ByVal isSelected As Boolean) _ As Folder 'return one or the other If isSelected Then Return selFolder Else Return unselFolder End If End Function End Class
The folder class itself differs only in that we use the Graphics object to do the drawing. Note that the drawRectangle method uses a width and height as the last two arguments rather than the second pair of coordinates.
Public Class Folder 'Draws a folder at the specified coordinates Private Const w As Integer = 50, h As Integer = 30 Private blackPen As Pen, whitePen As Pen Private grayPen As Pen Private backBrush, blackBrush As SolidBrush Private fnt As Font '----- Public Sub New(ByVal col As Color) backBrush = New SolidBrush(Col) blackBrush = New SolidBrush(Color.Black) blackPen = New Pen(color.Black) whitePen = New Pen(color.White) grayPen = New Pen(color.Gray) fnt = New Font("Arial", 12) End Sub '----- Public Sub draw(ByVal g As Graphics,_ ByVal x As Integer, _ ByVal y As Integer, ByVal title As String) g.FillRectangle(backBrush, x, y, w, h) g.DrawRectangle(blackPen, x, y, w, h) g.Drawline(whitePen, x + 1, y + 1, x + w - 1, y + 1) g.Drawline(whitePen, x + 1, y, x + 1, y + h) g.DrawRectangle(blackPen, x + 5, y - 5, 15, 5) g.FillRectangle(backBrush, x + 6, y - 4, 13, 6) g.DrawLine(graypen, x, y + h - 1, x + w, y + h - 1) g.DrawLine(graypen, x + w - 1, y, x + w - 1, y + h - 1) g.DrawString(title, fnt, blackBrush, x, y + h + 5) End Sub End Class
The only real differences in the VB7 approach are the ways we intercept the paint and mouse events. In both cases, we add an event handler. To do the painting of the folders, we add a paint event handler to the picture box.
AddHandler Pic.Paint, _ New PaintEventHandler(AddressOf picPaint)
The paint handler we add draws the folders, much as we did in the VB6 version.
'paints the folders in the picture box Private Sub picPaint(ByVal sender As Object, _ ByVal e As PaintEventArgs) Dim i, x , y , cnt As Integer Dim g As Graphics = e.Graphics x = pleft y = ptop cnt = 0 For i = 0 To names.Count - 1 fol = folfact.getFolder(selectedname = _ CType(names(i), String)) fol.draw(g, x, y, CType(names(i), String)) cnt = cnt + 1 If cnt > 2 Then cnt = 0 x = pleft y = y + vspace Else x = x + hspace End If Next End Sub
The mouse move event handler is very much analogous. We add a handler for mouse movement inside the picture box during the form's constructor.
AddHandler Pic.MouseMove, (AddressOf evMouse)
In order to detect whether a mouse position is inside a rectangle, we use a single instance of a Rectangle class. Since there already is a Rectangle class in the System.Drawing namespace, we put this rectangle in a VBPatterns namespace.
Namespace vbPatterns Public Class Rectangle Private x1, x2, y1, y2 As Integer Private w, h As Integer '----- Public Sub init(ByVal x_ As Integer,_ ByVal y_ As Integer) x1 = x_ y1 = y_ x2 = x1 + w y2 = y1 + h End Sub '----- Public Sub setSize(ByVal w_ As Integer, _ ByVal h_ As Integer) w = w_ h = h_ End Sub '----- Public Function contains(ByVal xp As Integer, _ ByVal yp As Integer) As Boolean Return x1 <= xp And xp <= x2 And _ y1 <= yp And yp <= y2 End Function End Class End Namespace
Then, using the contains method of the rectangle, we can check for whether the mouse is over a folder in the mouse move event handler.
'mouse move event handler Public Sub evMouse(ByVal sender As Object, _ ByVal e As MouseEventArgs) Dim x, y, i, cnt As Integer Dim oldname As String Dim found As Boolean oldname = selectedname 'save old name x = pleft 'move through coordinates y = ptop i = 0 cnt = 0 found = False While i < names.Count And Not found rect.init(x, y) 'see if a rectangle contains the mouse If rect.contains(e.X, e.Y) Then selectedname = CType(names(i), String) found = True End If i = i + 1 cnt = cnt + 1 'move on to next rectangle If cnt > 2 Then cnt = 0 x = pleft y = y + vspace Else x = x + hspace End If End While 'only refresh if mouse in new rectangle If found And oldname <> selectedname Then pic.Refresh() End If End Sub
You can see the final VB.Net Flyweight example in Figure 20-4.
Flyweights are not frequently used at the application level in VB. They are more of a system resource management technique used at a lower level. However, there are a number of stateless objects that get created in Internet programming that are somewhat analogous to Flyweights. It is generally useful to recognize that this technique exists so you can use it if you need it.
Some objects within the VB language could be implemented under the covers as Flyweights. For example, if there are two instances of a String constant with identical characters, they could refer to the same storage location. Similarly, it might be that two Integer or Float constants that contain the same value could be implemented as Flyweights, although they probably are not.
The Smalltalk Companion points out that sharable objects are much like Flyweights, although the purpose is somewhat different. When you have a very large object containing a lot of complex data, such as tables or bitmaps, you would want to minimize the number of instances of that object. Instead, in such cases, you'd return one instance to every part of the program that asked for it and avoid creating other instances.
A problem with such sharable objects occurs when one part of a program wants to change some data in a shared object. You then must decide whether to change the object for all users, prevent any change, or create a new instance with the changed data. If you change the object for every instance, you may have to notify them that the object has changed.
Sharable objects are also useful when you are referring to large data systems outside of VB, such as databases. The Dbase class we developed previously in the Façade pattern could be a candidate for a sharable object. We might not want a number of separate connections to the database from different program modules, preferring that only one be instantiated. However, should several modules in different threads decide to make queries simultaneously, the Database class might have to queue the queries or spawn extra connections.
The Flyweight pattern uses just a few object instances to represent many different objects in a program. All of them normally have the same base properties as intrinsic data and a few properties that represent extrinsic data that vary with each manifestation of the class instance. However, it could occur that some of these instances eventually take on new intrinsic properties (such as shape or folder tab position) and require a new specific instance of the class to represent them. Rather than creating these in advance as special subclasses, it is possible to copy the class instance and change its intrinsic properties when the program flow indicates that a new separate instance is required. The class copies this itself when the change becomes inevitable, changing those intrinsic properties in the new class. We call this process “copy-on-write” and can build this into Flyweights as well as a number of other classes, such as the Proxy, which we discuss next.
If Buttons can appear on several different tabs of a TabDialog, but each of them controls the same one or two tasks, is this an appropriate use for a Flyweight?