Flyweight Design Pattern

Pattern

It's one of the structural design patterns. This pattern helps to reduce the memory usage by sharing data among multiple objects.

This is a structural and optimization pattern.

When to Apply this Pattern

  1. When Memory is Limited.

  2. When Objects shared data.

    • Intrinsic data: Shared among objects and remain same once defined once value.

    • Extrinsic data: Changes based on client input and differs from one object to another.

  3. When Creation of Object is expensive.

Solution

Extracting Extrinsic Data: Here, extrinsic data is extracted, and the same data is passed as parameters to the object. This results in one type of object with exactly the same data, making the intrinsic data into one immutable object. (No of objects will be same)

Flyweight: The immutable intrinsic object is referred to as a flyweight.

Flyweight Factory (Caching of Flyweights): All created flyweights are cached and reused as necessary.

Examples: Car games with a lot of repeating objects trees, etc. e-commerce apps listing and selling thousands of items.

1 Object - 504KB

10 Objects - 4.92MB

10000 Objects - 4.81GB

Expalantion

Example 1 - Gaming Scenario

Let's consider the scenario of developing a game where two types of robots exist: humanoid and dog robots. These robots are constructed using sprites, which are 2D bitmap images used for animating them on the screen.

To display a robot on the screen, we require two sets of information: the type of robot, along with its associated sprites, and the (x, y) coordinates to position it on the screen. For a given type of robot, the sprites remain consistent because the animation for each type (humanoid or dog) remains the same across all instances of that type. However, the (x, y) coordinates will vary based on the specific context or location within the game world.

package Domain;

public class Robot {
    int x; //4b
    int y; //4b
    String type; //50b
    Sprites body; // small 2d bitmap (graphic element) // 30 kb

    public Robot(int x, int y, String type, Sprites body) {
        this.x = x;
        this.y = y;
        this.type = type;
        this.body = body;
    }
    //getters and setters...
}

Usage of Robot

package Domain;

public class Main {

    public static void main(String[] args) {
        int x = 0;
        int y = 0;

        for (int i=0; i<500000; i++){
            Sprites humanoidSprite = new Sprites();
            Robot humanoidRobotObject = new Robot(x+i,y+i, "HUMANOID", humanoidSprite);
        }

        for (int i=0; i<500000; i++){
            Sprites robotDogSprite = new Sprites();
            Robot robotDogObject = new Robot(x+i, y+i, "ROBOTIC_DOGS",robotDogSprite);
        }
    }
}

We have generated approximately 500,000 objects for each robot type, resulting in a total of 1 million objects, consuming a significant amount of memory. The primary component contributing to this memory usage is the sprites, which are shared among all instances of each robot type. It's important to note that sprites are identical for each type of robot.

Using Flyweight Design Pattern-

We employ the flyweight design pattern to optimize memory usage by identifying common object types and storing them in a flyweight factory. In this context, the intrinsic properties, such as type and sprites, are shared among similar objects, while extrinsic properties like (x, y) coordinates vary between instances.

In our scenario, we have two types of objects: humanoid and dog. By extracting the shared properties and caching them, we remove the need to duplicate these properties for each instance. Instead, we pass the extrinsic properties (x, y coordinates) as parameters to methods as required.

To implement this, we define a unified interface for all robot types, ensuring that each type implements the necessary methods, including those for handling extrinsic properties.

It's essential to note that flyweight objects are immutable; once created, they cannot be altered. Therefore, flyweight objects include constructors to initialize their state and getters to access their properties, but they lack setters to enforce immutability.

package Flyweight;

public interface IRobot {
    public void display(int x, int y);
}

Flyweight Dog Object

package Flyweight;

import Domain.Sprites;

public class RoboticDog implements IRobot{

    private String type;

    private Sprites body;

    public RoboticDog(String type, Sprites body) {
        this.type = type;
        this.body = body;
    }

    public String getType() {
        return type;
    }

    public Sprites getBody() {
        return body;
    }

    @Override
    public void display(int x, int y) {
        // use the Robotic Dog sprites object
        // and X and Y coordinate to render
    }
}

Flyweight Humanoid object

package Flyweight;

import Domain.Sprites;

public class HumanoidRobot implements IRobot{

    private String type;
    private Sprites body;

    public HumanoidRobot(String type, Sprites sprites) {
        this.type = type;
        this.body = sprites;
    }

    public String getType() {
        return type;
    }

    public Sprites getBody() {
        return body;
    }

    @Override
    public void display(int x, int y) {
        // use the humanoid sprites object
        // and X and Y coordinate to render the image.
    }
}

Flyweight Factory

package Flyweight;

import Domain.Sprites;

import java.util.HashMap;
import java.util.Map;

public class RoboticFactory {
    private static Map<String, IRobot> roboticObjectCache = new HashMap<>();

    public static IRobot createRoboto(String robotType){

        if (roboticObjectCache.containsKey(robotType)){
            return roboticObjectCache.get(robotType);
        }
        else{
            if(robotType == "HUMANNOID"){
                Sprites humanoidSprite = new Sprites();
                IRobot humanoidObject = new HumanoidRobot(robotType, humanoidSprite);
                roboticObjectCache.put(robotType, humanoidObject);
                return humanoidObject;
            }
            else if(robotType == "ROBOTICDOG"){
                Sprites roboticDogSprite = new Sprites();
                IRobot roboticDogObject = new RoboticDog(robotType, roboticDogSprite);
                roboticObjectCache.put(robotType, roboticDogObject);
                return roboticDogObject;
            }
            return null;
        }

    }
}

Usage

package Flyweight;

public class FlyMain {

    public static void main(String[] args) {
        IRobot humanoidRobot1 = RoboticFactory.createRoboto("HUMANOID");
        humanoidRobot1.display(1, 2);

        IRobot humanoidRobot2 = RoboticFactory.createRoboto("HUMANOID");
        humanoidRobot2.display(10,30);

        IRobot roboDog1 = RoboticFactory.createRoboto("ROBOTICDOG");
        roboDog1.display(2,9);

        IRobot roboDog2 = RoboticFactory.createRoboto("ROBOTICDOG");
        roboDog2.display(11, 19);
    }
}

Example 2: Word Document

Within a Word document, we encounter numerous letters, each characterized by its font type and size. Each letter may possess a unique combination of font type and size. Additionally, to render these letters on the screen, we require their respective coordinates, denoted by rows and columns. Consequently, every letter is associated with two distinct sets of properties: (character, font type, size) and (row, column).

package WordProcessor.Domain;

public class Character {
    char character;
    String fontType;
    int size;

    int row;
    int column;

    public Character(char character, String fontType, int size, int row, int column) {
        this.character = character;
        this.fontType = fontType;
        this.size = size;
        this.row = row;
        this.column = column;
    }
    //getter and setters
}

Usage

package WordProcessor.Domain;

public class CharMain {
    public static void main(String[] args) {
        /*
            this is the data we want to write into the word processor.

            Total = 58 characters
            t = 7 times
            h = 3 times
            a = 3 times and so on ...
         */

        Character object1 = new Character('t', "Arial", 10, 0, 0);
        Character object2 = new Character('h', "Arial", 10, 0, 1);
        Character object3 = new Character('i', "Arial", 10, 0, 1);
        Character object4 = new Character('s', "Arial", 10, 0,3);
    }
}

We notice that many objects share identical properties, barring the coordinates (row, column). In documents, font type and size often remain consistent within paragraphs. Consequently, generating numerous objects could lead to memory concerns. Given that these objects share identical data across instances, employing the flyweight design pattern proves beneficial.

Using Flyweight Design Pattern -

To address this, we isolate the intrinsic properties into an object known as a flyweight. Also we establish a flyweight interface encompassing all methods where extrinsic properties are transmitted as parameters.

This issue presents two distinct property sets:

Intrinsic - (character, fontType, size)

Extrinsic - (row, column)

Flyweight Interface

package WordProcessor.Flyweight;

public interface ILetter {
    public void display(int row, int column);
}

Flyweight Object

package WordProcessor.Flyweight;

public class DocumentCharacter implements ILetter{

    private char character;
    private String fontType;
    private int size;

    public DocumentCharacter(char character, String fontType, int size) {
        this.character = character;
        this.fontType = fontType;
        this.size = size;
    }

    @Override
    public void display(int row, int column) {
        // Display the character of particular font and size
        //at given location
    }
}

Flyweight factory

package WordProcessor.Flyweight;

import java.util.HashMap;
import java.util.Map;

public class LetterFactory {
    private static Map<Character, ILetter> characterCache= new HashMap<>();

    public ILetter createLetter(char characterValue){

        if ((characterCache.containsKey(characterValue))){
            return characterCache.get(characterValue);
        }
        else {
            DocumentCharacter characterObj = new DocumentCharacter(characterValue, "Arial", 10);
            characterCache.put(characterValue,characterObj);
            return characterObj;
        }
    }
}