Skip to content

Flyweight Design Pattern

Overview

Fly in the term Flyweight means light/not heavy.

Instead of creating thousands of objects that share common attributes, and result in a situation where a large amount of memory or other resources are used, you can modify your classes to share multiple instances simultaneously by using some kind of reference to the shared object instead.

The best example to describe this is a document containing many words and sentences and made up of many letters. Rather than storing a new object for each individual letter describing its font, position, color, padding and many other potential things. You can store just a lookup id of a character in a collection of some sort and then dynamically create the object with its proper formatting etc., only as you need to.

This approach saves a lot of memory at the expense of using some extra CPU instead to create the object at presentation time.

The Flyweight pattern, describes how you can share objects rather than creating thousands of almost repeated objects unnecessarily.

A Flyweight acts as an independent object in any number of contexts. A context can be a cell in a table, or a div on a html page. A context is using the Flyweight.

You can have many contexts, and when they ask for a Flyweight, they will get an object that may already be shared amongst other contexts, or already within itself somewhere else.

When describing flyweights, it is useful to describe it in terms of intrinsic and extrinsic attributes.

Intrinsic (in or including) are the attributes of a flyweight that are internal and unique from the other flyweights. E.g., a new flyweight for every letter of the alphabet. Each letter is intrinsic to the flyweight.

Extrinsic (outside or external) are the attributes that are used to present the flyweight in terms of the context where it will be used. E.g., many letters in a string can be right aligned with each other. The extrinsic property of each letter is the new positioning of its X and Y on a grid.

Terminology

  • Flyweight Interface: An interface where a flyweight receives its extrinsic attributes.
  • Concrete Flyweight: The flyweight object that stores the intrinsic attributes and implements the interface to apply extrinsic attributes.
  • Unshared Flyweights: Not all flyweights will be shared, the flyweight enables sharing, not enforcing it. It also possible that flyweights can share other flyweights but still not yet be used in any contexts anywhere.
  • Flyweight Factory: Creates and manages flyweights at runtime. It reuses flyweights in memory, or creates a new one in demand.
  • Client: The client application that uses and creates the Flyweight.

Flyweight UML Diagram

Flyweight Pattern UML Diagram

Source Code

...Refer to Book or Videos for extra content.

./src/flyweight/flyweight-concept.ts

// The Flyweight Concept

interface IFlyweight {
    code: number
}

class Flyweight implements IFlyweight {
    // The Concrete Flyweight
    code: number
    constructor(code: number) {
        this.code = code
    }
}

class FlyweightFactory {
    // Creating the FlyweightFactory as a static class

    static flyweights: { [id: number]: Flyweight } = {}

    static getFlyweight(code: number): Flyweight {
        // A static method to get a flyweight based on a code
        if (!(code in FlyweightFactory.flyweights)) {
            FlyweightFactory.flyweights[code] = new Flyweight(code)
        }
        return FlyweightFactory.flyweights[code]
    }

    static getCount(): number {
        // Return the number of flyweights in the cache
        return Object.keys(FlyweightFactory.flyweights).length
    }
}

class AppContext {
    // An example context that holds references to the flyweights in a
    // particular order and converts the code to an ascii letter
    private codes: number[] = []

    constructor(codes: string) {
        for (let i = 0; i < codes.length; i++) {
            this.codes.push(codes.charCodeAt(i))
        }
    }

    output() {
        // The context specific output that uses flyweights
        let ret = ''
        this.codes.forEach((c) => {
            ret =
                ret +
                String.fromCharCode(FlyweightFactory.getFlyweight(c).code)
        })

        return ret
    }
}

// The Client
const APP_CONTEXT = new AppContext('abracadabra')

// use flyweights in a context
console.log(APP_CONTEXT.output())

console.log(`abracadabra has ${'abracadabra'.length} letters`)
console.log(
    `FlyweightFactory has ${FlyweightFactory.getCount()} flyweights`
)

Output

node ./dist/flyweight/flyweight-concept.js
abracadabra
abracadabra has 11 letters
FlyweightFactory has 5 flyweights

Flyweight Use Case

...Refer to Book or Videos for extra content.

Example UML Diagram

Flyweight Pattern Use Case UML Diagram

Source Code

./src/flyweight/client.ts

// The Flyweight Use Case Example

import Table from './table'
import FlyweightFactory from './flyweight-factory'

const TABLE = new Table(3, 3)
TABLE.rows[0].columns[0].data = 'abra'
TABLE.rows[0].columns[1].data = '112233'
TABLE.rows[0].columns[2].data = 'cadabra'
TABLE.rows[1].columns[0].data = 'racadab'
TABLE.rows[1].columns[1].data = '12345'
TABLE.rows[1].columns[2].data = '332211'
TABLE.rows[2].columns[0].data = 'cadabra'
TABLE.rows[2].columns[1].data = '445566'
TABLE.rows[2].columns[2].data = 'aa 22 bb'

TABLE.rows[0].columns[0].justify = 1
TABLE.rows[1].columns[0].justify = 1
TABLE.rows[2].columns[0].justify = 1
TABLE.rows[0].columns[2].justify = 2
TABLE.rows[1].columns[2].justify = 2
TABLE.rows[2].columns[2].justify = 2
TABLE.rows[0].columns[1].width = 15
TABLE.rows[1].columns[1].width = 15
TABLE.rows[2].columns[1].width = 15

TABLE.draw()

console.log(
    `FlyweightFactory has ${FlyweightFactory.getCount()} flyweights`
)

./src/flyweight/flyweight.ts

export default class Flyweight {
    // The Concrete Flyweight
    code: number
    constructor(code: number) {
        this.code = code
    }
}

./src/flyweight/flyweight-factory.ts

import Flyweight from './flyweight'

export default class FlyweightFactory {
    // Creating the FlyweightFactory as a static class

    static flyweights: { [id: number]: Flyweight } = {}

    static getFlyweight(code: number): Flyweight {
        // A static method to get a flyweight based on a code
        if (!(code in FlyweightFactory.flyweights)) {
            FlyweightFactory.flyweights[code] = new Flyweight(code)
        }
        return FlyweightFactory.flyweights[code]
    }

    static getCount(): number {
        // Return the number of flyweights in the cache
        return Object.keys(FlyweightFactory.flyweights).length
    }
}

./src/flyweight/column.ts

// A Column that is used in a Row

import FlyweightFactory from './flyweight-factory'

export default class Column {
    // The columns are the contexts.
    // They will share the Flyweights via the FlyweightsFactory.
    // `data`, `width` and `justify` are extrinsic values. They are outside
    // of the flyweights.
    data = ''
    width = 10
    justify = 0

    getData(): string {
        // Get the flyweight value from the factory, and apply the extrinsic values
        const codes = []
        for (let i = 0; i < this.data.length; i++) {
            codes.push(this.data.charCodeAt(i))
        }
        let ret = ''
        Array.from(codes).forEach((c) => {
            ret =
                ret +
                String.fromCharCode(FlyweightFactory.getFlyweight(c).code)
        })

        switch (this.justify) {
            case 1:
                ret = this.leftAlign(this.width, ret, ' ')
                break
            case 2:
                ret = this.rightAlign(this.width, ret, ' ')
                break
            default:
                ret = this.center(this.width, ret, ' ')
        }

        return ret
    }

    center(width: number, string: string, padding: string): string {
        return width <= string.length
            ? string
            : this.centerAlternate(width, padding + string, padding)
    }
    centerAlternate(
        width: number,
        string: string,
        padding: string
    ): string {
        return width <= string.length
            ? string
            : this.center(width, string + padding, padding)
    }
    leftAlign(width: number, string: string, padding: string): string {
        return width <= string.length
            ? string
            : this.leftAlign(width, string + padding, padding)
    }
    rightAlign(width: number, string: string, padding: string): string {
        return width <= string.length
            ? string
            : this.rightAlign(width, padding + string, padding)
    }
}

./src/flyweight/row.ts

// A Row in the Table

import Column from './column'

export default class Row {
    columns: Column[]

    constructor(column_count: number) {
        this.columns = []
        for (let i = 0; i < column_count; i++) {
            this.columns.push(new Column())
        }
    }
    getData(): string {
        // Format the row before returning it to the table
        let ret = ''
        this.columns.forEach((column) => {
            ret = `${ret}${column.getData()}|`
        })
        return ret
    }
}

./src/flyweight/table.ts

// A Formatted Table

import Row from './row'

export default class Table {
    rows: Row[]

    constructor(row_count: number, column_count: number) {
        this.rows = []
        for (let i = 0; i < row_count; i++) {
            this.rows.push(new Row(column_count))
        }
    }

    draw(): void {
        // Draws the table formatted in the console
        let maxRowLength = 0
        const rows: string[] = []
        this.rows.forEach((row) => {
            const rowData = row.getData()
            rows.push(`|${rowData}`)
            const rowLength = rowData.length + 1
            if (maxRowLength < rowLength) {
                maxRowLength = rowLength
            }
        })
        console.log('-'.repeat(maxRowLength))
        rows.forEach((row) => {
            console.log(row)
        })
        console.log('-'.repeat(maxRowLength))
    }
}

Output

node ./dist/flyweight/client.js
---------------------------------------
|abra      |     112233    |   cadabra|
|racadab   |     12345     |    332211|
|cadabra   |     445566    |  aa 22 bb|
---------------------------------------
FlyweightFactory has 12 flyweights

Summary

...Refer to Book or Videos for extra content.