Flyweight Pattern

A flyweight in Flyweight Pattern is a stateless object that can be shared across objects or maybe classes many times. Obviously, that suggests Flyweight Pattern is a pattern about memory efficiency and maybe performance if the construction of objects is expensive.

Taking drawing snowflakes as an example. Despite real snowflakes being different to each other, when we are trying to draw them onto canvas, we usually have a limited number of styles. However, by adding properties like sizes and transformations, we can create a beautiful snow scene with limited snowflake styles.

As a flyweight is stateless, ideally it allows multiple operations simultaneously. You might need to be cautious when working with multi-thread stuff. Fortunately, JavaScript is usually single-threaded and avoids this issue if all related code is synchronous. You will still need to take care in detailed scenarios if your code is working asynchronously.

Assume we have some flyweights of class Snowflake:

Flyweight Pattern

When it snows, it would look like this:

Flyweight Pattern

In the image above, snowflakes in different styles are the result of rendering with different properties.

It's common that we would have styles and image resources being loaded dynamically, thus we could use a FlyweightFactory for creating and managing flyweight objects.

Participants

The simplest implementation of Flyweight Pattern has the following participants:

  • Flyweight: Snowflake

    Defines the class of flyweight objects.

  • Flyweight factory: FlyweightFactory

    Creates and manages flyweight objects.

  • Client.

    Stores states of targets and uses flyweight objects to manipulate these targets.

With these participants, we assume that the manipulation could be accomplished through flyweights with different states. It would also be helpful sometimes to have concrete flyweight class allowing customized behaviors.

Pattern scope

Flyweight Pattern is a result of efforts to improving memory efficiency and performance. The implementation cares about having the instances being stateless, and it is usually the client that manages detailed states for different targets.

Implementation

What makes Flyweight Pattern useful in the snowflake example is that a snowflake with the same style usually shares the same image. The image is what consumes time to load and occupies notable memory.

We are starting with a fake Image class that pretends to load images:

class Image { 
  constructor(url: string) { } 
} 

The Snowflake class in our example has only a single image property, and that is a property that will be shared by many snowflakes to be drawn. As the instance is now stateless, parameters from context are required for rendering:

class Snowflake { 
  image: Image; 
 
  constructor( 
    public style: string 
  ) { 
    let url = style + '.png'; 
    this.image = new Image(url); 
  } 
 
  render(x: number, y: number, angle: number): void { 
    // ... 
  } 
} 

The flyweights are managed by a factory for easier accessing. We'll have a SnowflakeFactory that caches created snowflake objects with certain styles:

const hasOwnProperty = Object.prototype.hasOwnProperty; 
 
class SnowflakeFactory { 
  cache: { 
    [style: string]: Snowflake; 
  } = {}; 
 
  get(style: string): Snowflake { 
    let cache = this.cache; 
    let snowflake: Snowflake; 
 
    if (hasOwnProperty.call(cache, style)) { 
      snowflake = cache[style]; 
    } else { 
      snowflake = new Snowflake(style); 
      cache[style] = snowflake; 
    } 
 
    return snowflake; 
  } 
} 

With building blocks ready, we'll implement the client (Sky) that snows:

const SNOW_STYLES = ['A', 'B', 'C']; 
 
class Sky { 
  constructor( 
    public width: number, 
    public height: number 
  ) { } 
 
  snow(factory: SnowflakeFactory, count: number) { } 
} 

We are going to fill the sky with random snowflakes at random positions. Before that let's create a helper function that generates a number between 0 and a max value given:

function getRandomInteger(max: number): number { 
  return Math.floor(Math.random() * max); 
} 

And then complete method snow of Sky:

snow(factory: SnowflakeFactory, count: number) { 
    let stylesCount = SNOW_STYLES.length; 
 
    for (let i = 0; i < count; i++) { 
        let style = SNOW_STYLES[getRandomInteger(stylesCount)]; 
        let snowflake = factory.get(style); 
 
        let x = getRandomInteger(this.width); 
        let y = getRandomInteger(this.height); 
 
        let angle = getRandomInteger(60); 
 
        snowflake.render(x, y, angle); 
    } 
} 

Now we may have thousands of snowflakes in the sky but with only three instances of Snowflake created. You can continue this example by storing the state of snowflakes and animating the snowing.

Consequences

Flyweight Pattern reduces the total number of objects involved in a system. As a direct result, it may save quite a lot memory. This saving becomes more significant when the flyweights get used by the client that processes a large number of targets.

Flyweight Pattern also brings extra logic into the system. When to use or not to use this pattern is again a balancing game between development efficiency and runtime efficiency from this point of view. Though most of the time, if there's not a good reason, we go with development efficiency.

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

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