Chapter 19. 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 19-1.

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

image

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. In this case 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 19-2.

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

image

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.


public class FolderFactory {
      private Folder selFolder, unselFolder;
      //-----
      public FolderFactory()            {
             //create the two folders
             selFolder = new Folder(Color.Brown);
             unselFolder = new Folder(Color.Bisque);
      }
      //-----
      public Folder getFolder(bool selected) {
             if(selected)
                    return selFolder;
             else
                    return unselFolder;
      }
}

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.


public class Folder {
      //Draws a folder at the specified coordinates
      private const int w  = 50;
      private const int h = 30;
      private Pen blackPen, whitePen;
      private Pen grayPen;

      private SolidBrush backBrush, blackBrush;
      private Font fnt;
      //------
      public Folder(Color col) {
             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);
      }
      //-----
      public void draw(Graphics g, int x, int y, string title) {
             //color folder
             g.FillRectangle(backBrush, x, y, w, h);
             //outline in black
             g.DrawRectangle(blackPen, x, y, w, h);
             //left 2 sides have white line
             g.DrawLine(whitePen, x + 1, y + 1, x + w - 1, y + 1);
             g.DrawLine(whitePen, x + 1, y, x + 1, y + h);
             //draw tab
             g.DrawRectangle(blackPen, x + 5, y - 5, 15, 5);
             g.FillRectangle(backBrush, x + 6, y - 4, 13, 6);
             //gray line on right and bottom
             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);
      }
}

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 ArrayList of folders at the outset and simply scanned through the array to draw each folder. 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.

There are two places in our display routine where we need to compute the positions of folders: when we draw them and when we check for a mouse hovering over them. Thus, it is convenient to abstract out the positioning code into a Positioner class.


public class Positioner   {
      private const int pLeft = 30;
      private const int pTop  = 30;
      private const int HSpace = 70;
      private const int VSpace = 80;
      private const int rowMax = 2;
      private int x, y, cnt;
      //-----
      public Positioner() {
             reset();
      }
      //-----
      public void reset() {
             x = pLeft;
             y = pTop;
             cnt = 0;
      }
      //-----
      public int nextX() {
             return x;
      }
      //-----
      public void incr() {
             cnt++;
             if (cnt > rowMax) { //reset to start new row
                    cnt = 0;
                    x = pLeft;
                    y += VSpace;
             }
             else {
                    x += HSpace;
             }
      }
      //-----
      public int nextY() {
             return y;
      }
}

Then we can write a much simpler paint routine.


private void picPaint(object sender,  PaintEventArgs e ) {
      Graphics g = e.Graphics;
      posn.reset ();
      for(int i = 0; i < names.Count; i++) {
             fol = folFact.getFolder(selectedName.Equals(
                    (string)names[i]));
             fol.draw(g, posn.nextX() , posn.nextY (),
                    (string)names[i]);
             posn.incr();
             }
}

The Class Diagram

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

Figure 19-3. How Flyweights are generated

image

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. Note that we make this class part of the csPatterns namespace to make sure it does not collide with the Rectangle class in the System.Drawing namespace.


namespace csPatterns {
      public class Rectangle    {
      private int x1, x2, y1, y2;
      private int w, h;
      public Rectangle()         {                 }
      //-----
      public void init(int x, int y) {
             x1 = x;
             y1 = y;
             x2 = x1 + w;
             y2 = y1 + h;
      }
      //-----
      public void setSize(int w_, int h_) {
             w = w_;
             h = h_;
      }
      //-----
      public bool contains(int xp, int yp) {
             return (x1 <= xp) && (xp <= x2) &&
                    (y1 <= yp) && (yp <= y2);
      }
  }
}

This allows us to just check each name when we redraw and create a selected folder instance where it is needed.


private void Pic_MouseMove(object sender, MouseEventArgs e) {
      string oldname = selectedName;  //save old name
      bool found = false;
      posn.reset ();
      int i = 0;
      selectedName = "";
      while (i < names.Count && ! found) {
             rect.init (posn.nextX() , posn.nextY ());
             //see if a rectangle contains the mouse
             if (rect.contains(e.X, e.Y) ){
                    selectedName = (string)names[i];
                    found = true;
             }
             posn.incr ();
             i++;
      }
      //only refresh if mouse in new rectangle
      if(  !oldname.Equals ( selectedName)) {
             Pic.Refresh();
      }
}

Handling the Mouse and Paint Events

In C# we intercept the paint and mouse events by adding event handlers. To do the painting of the folders, we add a paint event handler to the picture box.


Pic.Paint += new PaintEventHandler (picPaint);

The picPaint handler we add draws the folders, as we showed above. We added this code manually because we knew the signature of a paint routine.


private void picPaint(object sender,  PaintEventArgs e ) {

While the mouse move event handler is very much analogous, we might not remember its exact form. So, we use the Visual Studio IDE to generate it for us. While displaying the form in design mode, we click on the PictureBox and in the Properties window we click on the lightning bolt to display the possible events for the PictureBox, as shown in Figure 19-4.

Figure 19-4. Selecting the MouseMove event from the Properties window

image

Then we double-click on MouseMove, and it generates the correct code for the mouse move event and adds the event handler automatically. The generated empty method is as follows.


private void Pic_MouseMove(object sender, MouseEventArgs e) {
}

The code generated to add the event handler is inside the Windows Form Designer generated section. It amounts to this.


Pic.MouseMove += new MouseEventHandler(Pic_MouseMove);

Flyweight Uses in C#

Flyweights are not frequently used at the application level in C#. They are more of a system resource management technique used at a lower level than C#. 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 C# 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 C#, 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

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?

Program on the CD-ROM

image

..................Content has been hidden....................

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