Flyweight Design Pattern

Flyweight Design Pattern – Definition

The Flyweight pattern is a structural design pattern. It is defined as follows: Use sharing to support large number of fine-grained objects efficiently. 

Game application example

There is a game application which has the game player to collect gems from stars in the space. The space has many star objects and the player travels in the space going from star to star and collect the gems on each star. There can be multiple stars in a location.

Each star has a name, color, weight, mass, brightness, and gravitationalForce on it. It also has the and y co-ordinate to represent its location. 

public class Star {
    private String name;
    private Color color;
    private long weight;
    private long mass;
    private long brightness;
    private long gravitationalForce;

    //the location
    private int x;
    private int y;


    public void extractGem() {
        System.out.println("Extracting the gems of star " + name + " of color " + color
                + " at location (" + x + ", " + y + ")");
    }
}
The extractGem method will be called once a player reaches the star to collect the gems from it.
 
There are only three possible color values.
public enum Color {
    RED,
    BLUE,
    YELLOW
}

Creation of multiple objects

The game can have hundreds of thousands of stars. Let us say each location in the space has three stars; for each of the three available colors. For the sake of this discussion, assume there are a total of 10 locations. Thus, in this case, we have to create 90 star objects as shown below.

Flyweight Game Stars
Flyweight Game Stars

We assume the rest of the properties like mass, weight, gravity and brightness are same among the stars. If we consider all the red stars, they have the same set of values for all the properties except the location (x and y co-ordinates).

If we keep creating star objects as the player moves in the space (more locations), we would be keep creating more and more star objects. Such star objects are expensive in terms of storage and are costly. Remember, we have only two fields/properties values change among the different stars of the same color.

Sharing objects

The Flyweight design pattern allows us to share objects with reduced run-time overhead/cost. If we consider all stars of a given color, we know that all the values are the same except the location. So, we will create a new Star object (known as the flyweight) which will have only the common or shared properties i.e., it will have all the fields which remain same across objects. In our example, it will have all the fields except the fields location (x and y).

With this new representation, we will create only 9 star objects i.e., three star objects, one for each color (3 x 3). We load these into a common pool called the flyweight pool.

Then when we create a star object at a location, we will reuse the flyweight star objects from the pool. The location (x and y) will change anyway for the runtime star objects. But the thing to note here is that the location is not part of the flyweight star objects. 

Sharing using flyweight

Consider the below diagram using the flyweight object pool:

Flyweight Star Objects
Flyweight Star Objects

Considering the three red star objects A’ (represented with gradient style), they all point or refer to a single Red A star. But the location will change among them. But since we have moved the location value out of the Star representation, we can now create just one flyweight Star object and reuse it.

This is very useful given that creation of the StarFlyweight object is more costly than the actual Star objects in the location (especially when the Star objects have lots of other fields and data). Even with this we need 90 star objects (gradient styled ones), but those objects are lightweight when compared to the Star objects in the flyweight pool.

We have a constant number of StarFlyweight objects. As the player moves around in space, we will create a new Star object at a location which would have

  1. The location
  2. Just a reference to one of the StarFlyweight objects

Thus the flyweight object can be shared in many contexts i.e., we can use the same flyweight star object in many locations (context). A flyweight cannot make any assumption about the context in which they operate on.

Flyweight Terminologies

Having looked at an example of the application of the Flyweight pattern, let us look at a couple of terms.

Intrinsic state

We learnt that the flyweight pattern allows us to share objects and use it in multiple contexts. The flyweight objects act as an independent object with its own state. The state stored within a flyweight object is called as intrinsic state. In the above example, all fields in the StarFlyweight object are intrinsic state (name, color etc.,). These fields are not shared outside the object and are independent of the context in which the flyweight object is used.

Extrinsic state

An extrinsic state is a state that depends on the context. In the example, the location values (x and y) are the extrinsic state. The clients are responsible for passing the extrinsic state. When we create a star object we pick the right flyweight star object and pass the location (x and y) value to create a star in space.

Implementing Flyweight pattern

Let us implement the Flyweight pattern for the example we have seen.

Flyweight object

We create an interface which represents the behaviour on a star (extractGem). Note that unlike the first (normal) Star object, we pass the location value as a parameter. We will come to this later.

public interface Star {
    void extractGem(Location location);
}

Shown below is the StarFlyweight object which has the common or shared fields. We have used the Builder pattern to build the object.

public class StarFlyweight implements Star {
    private String name;
    private Color color;
    private long weight;
    private long mass;
    private long brightness;
    private long gravitationalForce;

    private StarFlyweight(Builder builder) {
        this.name = builder.name;
        this.color = builder.color;
        this.weight = builder.weight;
        this.mass = builder.mass;
        this.brightness = builder.brightness;
        this.gravitationalForce = builder.gravitationalForce;
    }

    @Override
    public void extractGem(Location location) {
        System.out.println("Extracting the gems of star " + name + " of color " + color
                + " at location (" + location.getX() + ", " + location.getY() + ")");
    }

    public static Builder builder(String name, Color color) {
        return new Builder(name, color);
    }

    public static class Builder {
        private String name;
        private Color color;
        //default values
        private long weight = 1000;
        private long mass = 2000;
        private long brightness = 200;
        private long gravitationalForce = 100;


        private Builder(String name, Color color) {
            this.name = name;
            this.color = color;
        }

        public Builder weight(long weight) {
            this.weight = weight;
            return this;
        }

        public Builder mass(long mass) {
            this.mass = mass;
            return this;
        }

        public Builder brightness(long brightness) {
            this.brightness = brightness;
            return this;
        }

        public Builder gravitationalForce(long gravitationalForce) {
            this.gravitationalForce = gravitationalForce;
            return this;
        }

        public StarFlyweight build() {
            return new StarFlyweight(this);
        }
    }
}

Flyweight factory

The clients will not know that there exists a pool of flyweight objects. We create a simple factory that is responsible for creating and managing flyweight objects.

public class StarFactory {
    private Map<String, Star> cache = new HashMap<>();
    
    public Star getStar(String name, Color color, long weight, long mass,
                        long brightness, long gravitationalForce) {
        return cache.computeIfAbsent(getKey(name, color),
                starName -> StarFlyweight.builder(name, color)
                        .weight(weight)
                        .mass(mass)
                        .brightness(brightness)
                        .gravitationalForce(gravitationalForce)
                        .build());
    }

    private String getKey(String name, Color color) {
        return name + "|" + color.name();
    }

}

For the example, we will assume the weight, mass, gravity and brightness values are the same among the stars. Thus the unique key for identifying a Star object is its name and the color. We create a unique String key with the combination of name and color and use a map to cache/store the stars. We are using Map’s computeIfAbsent method to create and insert a Star into the map if it doesn’t exist. 

Doing it in the old style looks like:
public Star getStar(String name, Color color, long weight, long mass,
                     long brightness, long gravitationalForce) {
    if (cache.containsKey(name)) {
        return cache.get(name);
    }
    Star star = StarFlyweight.builder(name, color)
            .weight(weight)
            .mass(mass)
            .brightness(brightness)
            .gravitationalForce(gravitationalForce)
            .build();
    cache.put(name, star);
    return star;
}

We add another method which ignores the other fields (uses only name and color) to keep the example simple.

public Star getStar(String name, Color color) {
    return cache.computeIfAbsent(getKey(name, color),
            starName -> StarFlyweight.builder(name, color)
                    .build());
}

The Context class

Finally comes the context class. This class encapsulates the runtime extrinsic value and a flyweight.

public class StarObject {
    private static final StarFactory STAR_FACTORY = new StarFactory();
    private Star starFlyWeight;
    private Location location;

    public StarObject(String starName, Color starColor, Location location) {
        this.starFlyWeight = STAR_FACTORY.getStar(starName, starColor);
        this.location = location;
    }

    public void extractGem() {
        starFlyWeight.extractGem(location);
    }

    public Star getStar() {
        return starFlyWeight;
    }

    public Location getLocation() {
        return location;
    }
}

The Location class is simply a wrapper around the x and y co-ordinates.

public class Location {
    private int x;
    private int y;

    public Location(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

The StarObject class uses the Flyweight Factory to get a flyweight Star object. To extract the gem, it simply calls the extractGem method on the flyweight reference it has by passing the location (extrinsic state).

Flyweight pattern Demo

Let us create 10 locations. We use nested Intstream to create locations (0,0), (0,1), (1,0), (1,1) up to (4,0) and (4,1).

List<Location> locations = IntStream.range(0, 5)
        .boxed()
        .flatMap(i -> IntStream.range(0, 2)
                .mapToObj(j -> new Location(i, j)))
        .collect(Collectors.toList());

System.out.println(locations.size()); //10

Let us declare three stars (A, B and C) and three colors.

List<String> starNames = List.of("A", "B", "C");
List<Color> starColors = List.of(Color.RED, Color.BLUE, Color.YELLOW);

Now, we will create 90 star objects as shown earlier. 

for each star name (name):
  for each star color (color):
     for each location:
         create StarObject(name, color, location)
List<StarObject> starObjects = starNames.stream()
        .flatMap(starName -> starColors.stream()
                .flatMap(starColor -> locations.stream()
                        .map(location -> new StarObject(starName, starColor, location))))
        .collect(Collectors.toList());
System.out.println(starObjects.size()); //90

Behind the scenes, the StarObject class will use the Flyweight factory to get StarFlyweight objects. Hence even though there are 90 starObjects at run-time, there are only 9 StarFlyweight objects. Let us confirm this:

Set<Star> uniqueStars = starObjects.stream()
        .map(StarObject::getStar)
        .collect(Collectors.toSet());
System.out.println(uniqueStars.size());//9

Let us finish this by calling the extractGem methods of some random star objects.

starObjects.get(0).extractGem();
starObjects.get(1).extractGem();
starObjects.get(11).extractGem();
starObjects.get(45).extractGem();
starObjects.get(79).extractGem();
starObjects.get(45).extractGem();

Prints,

Extracting the gems of star A of color RED at location (0, 0)
Extracting the gems of star A of color RED at location (0, 1)
Extracting the gems of star A of color BLUE at location (0, 1)
Extracting the gems of star B of color BLUE at location (2, 1)
Extracting the gems of star C of color BLUE at location (4, 1)
Extracting the gems of star B of color BLUE at location (2, 1)

Structure

Flyweight Design Pattern Class Diagram
Flyweight Design Pattern Class Diagram

Participants

Flyweight:

  • An interface for the behaviour of the flyweight objects.
  • The extrinsic state is passed as parameters.
ConcreteFlyweight:
  • Implements the Flyweight interface.
  • Has intrinsic state (common properties; independent of the context in which the flyweight object is used).
FlyweightFactory:
  • A factory for creating and managing flyweight objects (usually has a cache/pool of flyweight objects).
Context:
  • Maintains reference to the flyweight.
  • Stores extrinsic state of the flyweight.

Other variations

It is not mandatory to make all subclasses of the Flyweight interface as sharable. Thus we can have a concrete flyweight object that is not meant to be shared.

Related patterns

  • This pattern can be combined with Composite Pattern to implement a hierarchical structure as a directed-acyclic graph (DAG) where the leaf nodes are shared.
  • Sometimes the State and Strategy objects are implemented as flyweights.

Conclusion

We learnt the Flyweight design pattern. The flyweight helps us to share objects i.e., when the application creates many objects resulting in high storage cost. It does this by separating state that are common (and hence we can share it) and state that varies. The former is the intrinsic state and the latter is extrinsic state. The Flyweight object has the intrinsic state which is common and independent of the context and the client passes the extrinsic state at runtime.

References

  • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
  • Refactoring Guru
  • Design Patterns: Elements of Reusable Object-Oriented Software

Leave a Reply