Flyweight - Design Pattern

Flyweight - Design Pattern

Design Patterns: Elements of Reusable Object-Oriented Software

If get to know something new by reading my articles, don't forget to endorse me on LinkedIn

What is Flyweight Design Pattern?

Use sharing to support large numbers of fine-grained objects efficiently.

Designing objects to the lowest levels of system “granularity” promote flexibility in the application but as a good programmer you think about performance of the application and resource required to run it. Creating objects required more memory to store and CPU cycles to initialize the objects.

Imagine you need 1000 car objects in an online car racing game, and the car objects vary only in their current position on the race track. Instead of creating 1000 car objects and adding more as users join in, you can create a single car object with common data and a client object to maintain the state (position of a car). The Flyweight Pattern reduces repeated data, thus reduces memory consumption when dealing with large numbers of objects.

To become familiar with the Flyweight pattern, a concept to understand is how the internal state of a flyweight is represented. You can categorize the internal state into:

Intrinsic data: Information that is stored in the flyweight object. The information is independent of the flyweight’s context, thereby making it sharable. While applying the pattern, you take all of the objects that have the same intrinsic data and replace them with a single flyweight.

Extrinsic data: Information that depends on the flyweight’s context and hence cannot be shared. Client objects stores and computes extrinsic data and passes it to the flyweight when it needs it.

Think about a RaceCar object with three fields name : String, speed : Int, horsepower : Int and we have two cars : Midget and Sprint.

abstract class RaceCar {
    /**
     * Intrinsic state  stored and shared in the Flyweight object
     */
    var name: String? = null
    var speed = 0
    var horsePower = 0

    /**
     * Extrinsic state is stored or computed by client objects, and passed to the Flyweight.
     */
    abstract fun moveCar(currentX: Int, currentY: Int, newX: Int, newY: Int)
}

This means, these values can be shared between various FlyweightMidgetCar objects, and so we consider them as intrinsic data. This is common data that will not change and can be shared with other objects.

Consider in the base class, we have a moveCar(int currentX, int currentY, int newX ,int newY) method that we override in the FlyweightMidgetCar subclass. This method defines the position of each FlyweightMidgetCar object at a given time, and hence cannot be shared. So we consider the car position passed to moveCar() as extrinsic data.

Now that we have categorized the internal states of FlyweightMidgetCar into intrinsic and extrinsic data, we can make it a flyweight object. Similarly, we can make FlyweightSprintCar or any other object that extend RaceCar as flyweight objects.

class FlyweightMidgetCar : RaceCar() {
    init {
        num++
    }

    /**
     * This method accepts car location (extrinsic). No reference to current
     * or new location is maintained inside the flyweight implementation
     */
    override fun moveCar(currentX: Int, currentY: Int, newX: Int, newY: Int) {
        println("New location of $name is X$newX - Y$newY")
    }

    companion object {
        /**
         * Track number of flyweight instantiation
         */
        var num = 0
    }
}
class FlyweightSprintCar : RaceCar() {
    init {
        num++
    }

    /**
     * This method accepts car location (extrinsic). No reference to current
     * or new location is maintained inside the flyweight implementation
     */
    override fun moveCar(currentX: Int, currentY: Int, newX: Int, newY: Int) {
        println("New location of $name is X$newX - Y$newY")
    }

    companion object {
        /**
         * Track number of flyweight instantiation
         */
        var num = 0
    }
}

To manage our flyweight objects, we need a factory – a class that uses a pool (implemented as some data structure) to store flyweight objects. When client request a flyweight object for the first time, the factory instantiates a flyweight, initializes it with its intrinsic data, and puts it in the pool, before returning the flyweight to the client. For subsequent requests, the factory retrieves the flyweight from the pool and returns it to the client. We will name the class CarFactory.

object CarFactory {
    private val flyweights: MutableMap<String, RaceCar> = HashMap()

    /*
    If key exist, return flyweight from Map
     */
    fun getRaceCar(key: String): RaceCar? {
        if (flyweights.containsKey(key)) {
            return flyweights[key]
        }
        val raceCar: RaceCar
        when (key) {
            "Midget" -> {
                raceCar = FlyweightMidgetCar()
                raceCar.name = "Midget Car"
                raceCar.speed = 140
                raceCar.horsePower = 400
            }
            "Sprint" -> {
                raceCar = FlyweightSprintCar()
                raceCar.name = "Sprint Car"
                raceCar.speed = 160
                raceCar.horsePower = 1000
            }
            else -> throw IllegalArgumentException("Unsupported car type.")
        }
        flyweights[key] = raceCar
        return raceCar
    }
}

We will also need a client class, RaceCarClient that will retrieve a flyweight (already initialized with its intrinsic data) from the factory and pass the extrinsic data to the flyweight. In the context of our example, the client will manage and pass the position (extrinsic data) of a car by calling the moveCar(int currentX, int currentY, int newX ,int newY) method of the flyweight.

class RaceCarClient(name: String?) {
    private val raceCar: RaceCar?

    /**
     * The extrinsic state of the flyweight is maintained by the client
     */
    private var currentX = 0
    private var currentY = 0

    init {
        /**
         * Ask factory for a RaceCar
         */
        raceCar = getRaceCar(name!!)
    }

    fun moveCar(newX: Int, newY: Int) {
        /**
         * Car movement is handled by the flyweight object
         */
        raceCar!!.moveCar(
            currentX,
            currentY, newX, newY
        )
        currentX = newX
        currentY = newY
    }
}

Let's recap:

Flyweight (RaceCar) : Abstract class or interface for flyweight objects. Declares method through which flyweights can receive and act on extrinsic state. Although Flyweight enables sharing, it is not mandatory that all Flyweight subclasses must be shared.

ConcreteFlyweight(FlyweightMidgetCar and FlyweightSprintCar): Extends/Implements Flyweight to represent flyweight objects that can be shared.

FlyweightFactory(CarFactory) : Creates and manages flyweight objects. When a client requests a flyweight object, FlyweightFactory provides an existing one or creates a new one, if it does not exists.

Client(RaceCarClient) : Requests FlyweightFactory for a flyweight object, and then computes and passes the extrinsic data to it.

Let's take a look at the full implementation in Kotlin:

Follow Me on LinkedIn