Chapter 21. Flyweight

Public transportation, such as buses, has been around for over a century. A lot of passengers can share the expenses of owning and operating vehicles (e.g., buses) when they all go in the same direction. There are multiple stops/stations in public transport. People just hop on and off along the route close to the place where they want to go. The expenses of getting to the place are limited to just the trip. Compared to the costs of owning a vehicle, taking public transport is a lot cheaper. That is the beauty of sharing common resources.

In object-oriented software design, sometimes sharing common objects not only saves resources but also increases performance. For instance, we need one million instances of a class for a particular task, but if we can make an instance of the class sharable by externalizing some unique information, the saved cost could be significant (one unique instance vs. one million of it). A sharable object contains only some intrinsic information that doesn't identify it. A design pattern that focuses on designing sharable objects is called the Flyweight pattern.

In this chapter, we are going to discuss the concepts of the pattern. We will also implement the pattern with a sample app that displays hundreds of flower images with only six unique instances.

What Is the Flyweight Pattern?

There are two key components to implement the Flyweight pattern. They are usually sharable flyweight objects and a pool that keeps them. Some sort of centralized object is crucial for maintaining the pool and returning appropriate instances from it. A factory (either an abstract or a concrete one—see Abstract Factory pattern, Chapter 5) is a perfect candidate for this role. It can return various types of concrete flyweight objects based on their parent type through a single factory method. That kind of factory is commonly referred to as a "manager" in different frameworks (The name "Manager" seems to sound cooler than "Factory" anyway). No matter what it's called, its primary objectives are to maintain flyweight objects in a pool and return any of them appropriately.

What makes flyweight objects "flyweight" in the first place? It doesn't refer to the size of them but the sheer amount of space they can save by sharing. Some (or most) of the object's unique state (extrinsic state) can be taken out and managed elsewhere, and the rest of it is shared. Let's say that you originally needed a million objects of a class, but because objects of that class are flyweights, now one is enough. That's what makes the whole system lightweight thanks to the sharable flyweight objects. With a careful design, memory savings can be significant. In iOS development, saving memory means boosting overall performance.

A class diagram in Figure 21-1 depicts their static relationships.

A class diagram of the Flyweight pattern

Figure 21-1. A class diagram of the Flyweight pattern

Flyweight is the parent interface (protocol) for two concrete flyweight classes, ConcreteFlyweight1 and ConcreteFlyweight2. Each ConcreteFlyweight class maintains its own intrinsicState that doesn't make any object of it unique. Flyweight declares the operation:extrinsicState method, which is implemented by both ConcreteFlyweight classes. IntrinsicState is something that can be shared in a flyweight object, while extrinsicState supplements any missing information that makes a flyweight object unique. Clients provide extrinsicState to an operation: message to let the flyweight object complete its tasks with the unique information from extrinsicState.

Figure 21-2 shows how an instance of FlyweightFactory manages flyweight objects in its pool at runtime.

An object diagram that shows how flyweights are shared at runtime

Figure 21-2. An object diagram that shows how flyweights are shared at runtime

Note

THE FLYWEIGHT PATTERN: Uses sharing to support large numbers of fine-grained objects efficiently.[19]

When Would You Use the Flyweight Pattern?

You'd naturally think about using it when all of the following are true:

  • Your app uses a lot of objects.

  • Keeping objects in memory can affect memory performance.

  • Most of the object's unique state (extrinsic state) can be externalized and lightweight.

  • Relatively few shared objects can replace the original bunch of objects after the objects' extrinsic state is removed.

  • Your app doesn't depend on object identity since shared objects cannot have unique identities.

We are going to develop an app that draws hundreds or more flower patterns from a pool of shareable flowers to illustrate the concepts of the pattern.

Creating a Hundred-Flower Pool

We are going to create a small app that displays a lot of random flower images on the screen. There are six types of flowers we want to display, as shown in Figure 21-3.

From left to right, top row: anemone, cosmos, and gerberas; bottom row: hollyhock, jasmine, and zinnia

Figure 21-3. From left to right, top row: anemone, cosmos, and gerberas; bottom row: hollyhock, jasmine, and zinnia

After many of these flowers have been drawn, the screen will be filled up with them like the one in Figure 21-4.

An actual screenshot of our "Hundred-Flower Pool" with only six distinct instances of flower views despite five hundred of them shown onscreen

Figure 21-4. An actual screenshot of our "Hundred-Flower Pool" with only six distinct instances of flower views despite five hundred of them shown onscreen

Our goal is to draw a lot (hundreds or more) of randomly sized and positioned flowers with just six unique instances of them. If we create one flower instance for each instance drawn on the screen, the app may eat up a lot of memory. Our solution here is to use the Flyweight pattern to restrict our flower instances to no more than the total number of flower types we can choose from.

We need some sort of flyweight factory and some flyweight products for the design as shown in the class diagram in Figure 21-1. FlowerView is a subclass of UIImageView, with which we can customize drawing a flower image. Our flyweight factory for this app is called FlowerFactory, which manages a pool of FlowerView instances. Although the objects' class in the pool is FlowerView, clients expect instances only of UIView returned from FlowerFactory. It gives us a more flexible design rather than the factory returning objects of UIImage as the ultimate product type. If, for some reason, we also want to have flowers that "draw" themselves instead of display solid images, then we will have to change almost everything—that means we could be in trouble. UIView is considered a high-level class for anything that draws on the screen. FlowerFactory can return objects of any UIView subclass, and it won't break the system. That's one of the benefits of "program to an interface, not an implementation."

Designing and Implementing Sharable Flowers

We have six flower images as shown in Figure 21-3: anemone, cosmos, gerberas, hollyhock, jasmine, and zinnia. Each image is maintained in a unique instance of FlowerView. The FlowerFactory returns only the unique instances of FlowerView no matter how many flowers need to be returned. Their static relationships are illustrated as a class diagram in Figure 21-5.

A class diagram of FlowerView as a sharable flyweight

Figure 21-5. A class diagram of FlowerView as a sharable flyweight

FlowerFactory maintains an aggregated reference to a pool of flowers as flowerPool_. The flowerPool_ is a data structure that keeps track of any instances of FlowerView. FlowerFactory returns an instance of FlowerView as UIView with its flowerViewWithType: method. A new instance of FlowerView will be created and returned if a view for a requested flower type does not exist in the pool.

Implementing the FlowerView Class

A class declaration of FlowerView is shown in Listing 21-1.

Example 21-1. A Class Declaration of FlowerView in FlowerView.h

@interface FlowerView : UIImageView
{

}

- (void) drawRect:(CGRect)rect;

@end

Nothing can be simpler than that! It overrides only the drawRect:rect method of UIImageView and nothing else. We are re-declaring the method here to make it clear about what methods this class implements. The implementation of the method lets only the image stored in super (UIImage) draw itself separately in a provided rectangular area, as shown in Listing 21-2.

Example 21-2. An Implementation of FlowerView in FlowerView.m

#import "FlowerView.h"

@implementation FlowerView

- (void) drawRect:(CGRect)rect
{
  [self.image drawInRect:rect];
}

@end

Implementing the FlowerFactory Class

Like what we've seen in Figure 21-5, FlowerFactory is declared in Listing 21-3.

Example 21-3. A Class Declaration of FlowerFactory in FlowerFactory.h

@interface FlowerFactory : NSObject
{
  @private
  NSMutableDictionary *flowerPool_;
}

- (UIView *) flowerViewWithType:(FlowerType)type;

@end

FlowerFactory has a private NSMutableDictionary to keep track of a whole pool of ready-to-return flowers as flowerPool_. It also defines flowerViewWithType:(FlowerType)type as a convenience factory method that returns unique instances of UIView based on the FlowerType parameter. The FlowerType is based on the names of flowers we have in Figure 21-3. Their types are defined as a set of enum values, as shown in Listing 21-4.

Example 21-4. The Definition of FlowerType

typedef enum
{
  kAnemone,
  kCosmos,
  kGerberas,
  kHollyhock,
  kJasmine,
  kZinnia,
  kTotalNumberOfFlowerTypes
} FlowerType;

It's apparent that the value of kTotalNumberOfFlowerTypes is the total number of flower types we support. So except that value, the FlowerFactory's flowerWithType: method uses any one of these values to determine which flower instance to return.

Let's see how FlowerFactory manages the flower pool in Listing 21-5.

Example 21-5. An Implementation of FlowerFactory in FlowerFactory.m

#import "FlowerFactory.h"
#import "FlowerView.h"

@implementation FlowerFactory


- (UIView *) flowerViewWithType:(FlowerType)type
{
  // lazy-load a flower pool
  if (flowerPool_ == nil)
  {
    flowerPool_ = [[NSMutableDictionary alloc]
                   initWithCapacity:kTotalNumberOfFlowerTypes];
  }

  // try to retrieve a flower
  // from the pool
  UIView *flowerView = [flowerPool_ objectForKey:[NSNumber
                                                  numberWithInt:type]];

  // if the type requested
  // is not available then
  // create a new one and
  // add it to the pool
  if (flowerView == nil)
  {
    UIImage *flowerImage;

    switch (type)
{
      case kAnemone:
        flowerImage = [UIImage imageNamed:@"anemone.png"];
        break;
      case kCosmos:
        flowerImage = [UIImage imageNamed:@"cosmos.png"];
        break;
      case kGerberas:
        flowerImage = [UIImage imageNamed:@"gerberas.png"];
        break;
      case kHollyhock:
        flowerImage = [UIImage imageNamed:@"hollyhock.png"];
        break;
      case kJasmine:
        flowerImage = [UIImage imageNamed:@"jasmine.png"];
        break;
      case kZinnia:
        flowerImage = [UIImage imageNamed:@"zinnia.png"];
        break;
      default:
        break;
    }

    flowerView = [[[FlowerView alloc]
                   initWithImage:flowerImage] autorelease];
    [flowerPool_ setObject:flowerView
                    forKey:[NSNumber numberWithInt:type]];
  }

  return flowerView;
}

- (void) dealloc
{
  [flowerPool_ release];
  [super dealloc];
}

@end

The flower pool (flowerPool_) is not initiated in FlowerFactory's init method. Instead, it's lazy-loaded in the flowerViewWithType: factory method. The default capacity for the pool is kTotalNumberOfFlowerTypes (i.e., 6). If the requested instance is not in the pool, then the factory will create a new instance of FlowerView with an appropriate flower image and add it to the pool using type as the key and the new instance as the value. Any subsequent requests for a created flower will be returned from the pool. The mechanism of sharing flowers from the pool is pretty straightforward.

How Are FlowerView Objects Shared?

In the original definition of the pattern, flyweight objects usually seem to associate with some sort of intrinsic state that is sharable. Though it may not be the case all the time, our FlowerView flyweight objects do share their internal flower images as intrinsic states. Sharable flyweight objects can also be used as "strategies" (see Strategy pattern, Chapter 19). Strategies are usually referred to embedded algorithms, by the way. However, regardless of whether a flyweight object has any sharable, intrinsic state, there is still a need for defining some sort of external data structure to keep track of any extrinsic state (unique information) of the flyweight objects. We define a C-struct of ExtrinsicFlowerState to serve that purpose, as defined in Listing 21-6.

Example 21-6. A Definition of the ExtrinsicFlowerState Struct

typedef struct
{
  UIView *flowerView;
  CGRect area;
} ExtrinsicFlowerState;

The area to display is unique to each flower, so it needs to be treated as an extrinsic state. We need an array to store a bunch of them for flowers created by a client. Figure 21-6 depicts an example of how an array of extrinsic flower states relates to unique instances of FlowerView in the flyweight pool of FlowerFactory.

Elements in an array of extrinsic flower states share the same unique instances of FlowerView objects in the flyweight pool maintained by FlowerManager.

Figure 21-6. Elements in an array of extrinsic flower states share the same unique instances of FlowerView objects in the flyweight pool maintained by FlowerManager.

Each unit in an array stores a pointer to a real FlowerView object from the pool as well as an area that the flower is supposed to display in. The size of the array doesn't affect the size of the pool at all.

Implementing Client Code

Now we have all the pieces in place, and we are ready to see how a client draws 500 flowers using the flyweight infrastructure, as shown in Listing 21-7.

Example 21-7. Client Code That Constructs a List of Flowers

- (void)viewDidLoad
{
  [super viewDidLoad];

  // construct a flower list
  FlowerFactory *factory = [[[FlowerFactory alloc] init] autorelease];
  NSMutableArray *flowerList = [[[NSMutableArray alloc]
                                 initWithCapacity:500] autorelease];

  for (int i = 0; i < 500; ++i)
  {
    // retrieve a shared instance
    // of a flower flyweight object
    // from a flower factory with a
    // random flower type
    FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
    UIView *flowerView = [factory flowerViewWithType:flowerType];

    // set up a location and an area for the flower
    // to display onscreen
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
    CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
    NSInteger minSize = 10;
    NSInteger maxSize = 50;
    CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;

    // assign attributes for a flower
// to an extrinsic state object
    ExtrinsicFlowerState extrinsicState;
    extrinsicState.flowerView = flowerView;
    extrinsicState.area = CGRectMake(x, y, size, size);

    // add an extrinsic flower state
    // to the flower list
    [flowerList addObject:[NSValue value:&extrinsicState
                            withObjCType:@encode(ExtrinsicFlowerState)]];
  }

  // add the flower list to
  // this FlyweightView instance
  [(FlyweightView *)self.view setFlowerList:flowerList];
}

In each iteration in the main for loop, a UIView instance (in fact, FlowerView) is returned by an instance of FlowerFactory based on a randomly selected flower type with FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes. The integer value of flowerType shouldn't be greater than kTotalNumberOfFlowerTypes. A unique location and size are put in an ExtrinsicFlowerState structure associated with a pointer to an instance of FlowerView. Then the whole extrinsic state structure is added to flowerList, which is an NSArray object. Since ExtrinsicFlowerState is not an Objective-C object, we needed to @encode it in an NSValue object before we can safely add it to the array. When a complete list of 500 flowers is allocated to the array, we will set the list to an instance of FlyweightView (i.e., self.view).

Adding the flower list to FlyweightView is just half of the story; we also need to know how FlyweightView populates and presents all the flowers in the list onscreen, as shown in Listing 21-8. After the flower list is constructed, assign it to a custom view that displays each flower in the list. The list in this example is called flowerList_.

Example 21-8. The Custom View is Overriding a UIView's drawRect: Method to Draw the Flowers Onscreen.

- (void)drawRect:(CGRect)rect
{
  // Drawing code

  for (NSValue *stateValue in flowerList_)
  {
    ExtrinsicFlowerState state;
    [stateValue getValue:&state];

    UIView *flowerView = state.flowerView;
    CGRect area = state.area;

    [flowerView drawRect:area];
  }
}

In its drawRect: method, the place where you normally define any custom drawing routine, we have a for loop that populates a whole list of previously saved NSValue entries in the flower array. We then retrieve an ExtrinsicFlowerState structure from each NSValue instance in each iteration. Once we get a FlowerView pointer and an area where it should draw into from the ExtrinsicFlowerState structure, we send a drawRect:rect message to the FlowerView instance to perform its drawing operation that we explained in Listing 21-2.

After all 500 flowers are drawn on the view, we will see something like Figure 21-4.

Summary

Sharing is a virtue in humanity. Sharing the same resources to perform a task can be more cost-effective than achieving the same thing with an individual's resources. In this chapter, we have designed an app that can display 500+ flowers on the screen with only six unique instances of different flower images. Those unique flower instances had some uniquely identifiable information (i.e., location and size) stripped off and were left with only the bare-bones operation that displays the flower images. When a unique flower is requested, a client needs to provide some unique information (extrinsic state) to the flower instance so it can draw a unique flower with the provided information. Without using the Flyweight pattern, the app needs to instantiate as many UIImageView instances as required to draw on the screen (i.e., 500+). With a careful design, the Flyweight pattern can save a significant amount of memory by sharing a fraction of the required objects.

In the next chapter, we will see another design pattern that can improve performance as well as provide object access by delaying the operations to the objects with proxy objects.



[19] The original definition appeared in Design Patterns, by the "Gang of Four" (Addison-Wesley, 1994).

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

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