Skip to content

Flyweight Design Pattern

Video Lecture

Section Video Links
Flyweight Pattern Flyweight Flyweight Pattern  
Flyweight Use Case Flyweight Use Case Flyweight Use Case  

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 an 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 that describes the intrinsic properties of the flyweight.
  • Concrete Flyweight: The actual flyweight object that stores the intrinsic attributes and is instantiated when needed by the factory.
  • Flyweight Factory: Creates and manages the flyweights at runtime. It reuses flyweights or creates a new one on demand.
  • Context: Any object(s) within your application that will use the Flyweight Factory.
  • Client: The client application that contains contexts.

Flyweight UML Diagram

Flyweight Pattern UML Diagram

Source Code

A context is created using the string abracadabra.

As it is output, it asks the Flyweight factory for the next character. The Flyweight factory will either return an existing Flyweight, or create a new one before returning it.

abracadabra has many re-used characters, so only 5 flyweights needed to be created.

./src/flyweight/flyweight-concept.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 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

In this example, I create a dynamic table with 3 rows and 3 columns each. The columns are then filled with some kind of text, and also chosen to be left, right or center aligned.

The letters are the flyweights and only a code indicating the letter is stored. The letters and numbers are shared many times.

The column cells are the contexts, and they pass the extrinsic vales describing the combination of letters, the justification left, right or center, and the width of the table column that is then used for the space padding.

Example UML Diagram

Flyweight Pattern Use Case UML Diagram

Source Code

./src/flyweight/client.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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

1
2
3
4
5
6
7
export default class Flyweight {
    // The Concrete Flyweight
    code: number
    constructor(code: number) {
        this.code = code
    }
}

./src/flyweight/flyweight-factory.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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

  • Clients should access Flyweight objects only the through a FlyweightFactory object to ensure that they are shared.
  • Intrinsic values are stored internally in the Flyweight.
  • Extrinsic values are passed to the Flyweight and customize it depending on the context.
  • Implementing the flyweight is a balance between storing all objects in memory, versus storing small unique parts in memory, and potentially calculating extrinsic values in the context objects.
  • Use the flyweight to save memory when it is beneficial. The offset is that extra CPU may be required during calculating and passing extrinsic values to the flyweights.
  • The flyweight reduces memory footprint because it shares objects and allows the possibility of dynamically creating extrinsic attributes.
  • The contexts will generally calculate the extrinsic values used by the flyweights, but it is not necessary. Values can be stored or referenced from other objects if necessary.
  • When architecting the flyweight, start with considering which parts of a common object may be able to be split and applied using extrinsic attributes.