Chapter 20. The Flyweight Pattern

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.

A set of folders representing information about various people. Since these are so similar, they are candidates for the Flyweight pattern.

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.

Discussion

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.

Example Code

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.

The Flyweight display with one folder selected

Figure 20-2. The Flyweight display with one folder selected

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 Class Diagram

The diagram in Figure 20-3 shows how these classes interact.

How Flyweights are generated

Figure 20-3. How Flyweights are generated

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.

Selecting a Folder

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

Writing a Flyweight Folder in VB.NET

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.

The VB.Net Flyweight program.

Figure 20-4. The VB.Net Flyweight program.

Flyweight Uses in VB

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.

Sharable Objects

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.

Copy-on-Write Objects

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.

Thought Question

Thought Question

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?

Programs on the CD-ROM

FlyweightFlywt VB6 folders
FlyweightVbnet VB7 Flyweight folders
..................Content has been hidden....................

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