Skip to content

Composite Design Pattern

Video Lecture

Section Video Links
Composite Pattern Composite Composite Pattern  
Composite Use Case Composite Use Case Composite Use Case  

Overview

The Composite design pattern is a structural pattern useful for hierarchical management.

The Composite design pattern,

  • Allows you to represent individual entities(leaves) and groups of leaves as the same.
  • Is a structural design pattern that lets you compose objects into a changeable tree structure.
  • Is great if you need the option of swapping hierarchical relationships around.
  • Allows you to add/remove components to the hierarchy.
  • Provides flexibility of structure

Examples of using the Composite Design Pattern can be seen in a file system directory structure where you can swap the hierarchy of files and folders, and also in a drawing program where you can group, ungroup, transform objects and change multiple objects at the same time.

Terminology

  • Component Interface: The interface that all leaves and composites should implement.
  • Leaf: A single object that can exist inside or outside a composite.
  • Composite: A collection of leaves and/or other composites.

Composite UML Diagram

Composite Pattern UML Diagram

Source Code

In this concept code, two leaves are created, LEAF_A and LEAF_B, and two composites are created, COMPOSITE_1 and COMPOSITE_2.

LEAF_A is attached to COMPOSITE_1.

Then I change my mind and attach LEAF_A to COMPOSITE_2.

I then attach COMPOSITE_1 to COMPOSITE_2.

LEAF_B is not attached to composites.

./src/composite/composite-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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
// The Composite pattern concept

interface ICompositeComponent {
    // A component interface describing the common
    // fields and methods of leaves and composites
    name: string // A name for this component
    referenceToParent?: Composite
    method(): void // A method each Leaf and composite container should implement
    detach(): void // Called before a leaf is attached to a composite
}

class Leaf implements ICompositeComponent {
    // A Leaf can be added to a composite, but not a leaf
    referenceToParent?: Composite = undefined
    name: string
    constructor(name: string) {
        this.name = name
    }

    method(): void {
        const parent = this.referenceToParent
            ? this.referenceToParent.name
            : 'none'
        console.log(`<Leaf>\t\tname:${this.name}\tParent:\t${parent}`)
    }

    detach(): void {
        'Detaching this leaf from its parent composite'
        if (this.referenceToParent) {
            this.referenceToParent.delete(this)
        }
    }
}

class Composite implements ICompositeComponent {
    // A composite can contain leaves and composites

    referenceToParent?: Composite
    components: ICompositeComponent[]
    name: string

    constructor(name: string) {
        this.name = name
        this.components = []
    }

    method(): void {
        const parent = this.referenceToParent
            ? this.referenceToParent.name
            : 'none'
        console.log(
            `<Composite>\tname:${this.name}\tParent:\t${parent}\tComponents:${this.components.length}`
        )
        this.components.forEach((component) => {
            component.method()
        })
    }

    attach(component: ICompositeComponent): void {
        // Detach leaf / composite from any current parent reference and
        // then set the parent reference to this composite
        component.detach()
        component.referenceToParent = this
        this.components.push(component)
    }

    delete(component: ICompositeComponent): void {
        // Removes leaf/composite from this composite this.components
        const index = this.components.indexOf(component)
        if (index > -1) {
            this.components.splice(index, 1)
        }
    }

    detach(): void {
        // Detaching this composite from its parent composite
        if (this.referenceToParent) {
            this.referenceToParent.delete(this)
            this.referenceToParent = undefined
        }
    }
}

// The Client
const LEAF_A = new Leaf('leaf-a')
const LEAF_B = new Leaf('leaf-b')
const COMPOSITE_1 = new Composite('comp-1')
const COMPOSITE_2 = new Composite('comp-2')

// Attach LEAF_A to COMPOSITE_1
COMPOSITE_1.attach(LEAF_A)

// Instead, attach LEAF_A to COMPOSITE_2
COMPOSITE_2.attach(LEAF_A)

// Attach COMPOSITE1 to COMPOSITE_2
COMPOSITE_2.attach(COMPOSITE_1)

// Run the methods that
LEAF_B.method() // not in any composites
COMPOSITE_2.method() // COMPOSITE_2 contains both COMPOSITE_1 and LEAF_A

Output

node ./dist/composite/composite-concept.js
<Leaf>          name:leaf-b     Parent: none
<Composite>     name:comp-2     Parent: none    Components:2
<Leaf>          name:leaf-a     Parent: comp-2
<Composite>     name:comp-1     Parent: comp-2  Components:0

Composite Use Case

Demonstration of a simple in memory hierarchical file system.

A root object is created that is a composite.

Several files (leaves) are created and added to the root folder.

More folders (composites) are created, and more files are added, and then the hierarchy is reordered.

Composite Example UML Diagram

Composite Pattern Use Case UML Diagram

Source Code

./src/composite/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
// A use case of the composite pattern.

import File from './file'
import Folder from './folder'

const FILESYSTEM = new Folder('root')
const FILE_1 = new File('abc.txt')
const FILE_2 = new File('123.txt')
FILESYSTEM.attach(FILE_1)
FILESYSTEM.attach(FILE_2)
const FOLDER_A = new Folder('folder_a')
FILESYSTEM.attach(FOLDER_A)
const FILE_3 = new File('xyz.txt')
FOLDER_A.attach(FILE_3)
const FOLDER_B = new Folder('folder_b')
const FILE_4 = new File('456.txt')
FOLDER_B.attach(FILE_4)
FILESYSTEM.attach(FOLDER_B)
FILESYSTEM.dir('')

// now move FOLDER_A and its contents to FOLDER_B
console.log()
FOLDER_B.attach(FOLDER_A)
FILESYSTEM.dir('')

./src/composite/file.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
import IComponent from './icomponent'
import Folder from './folder'

export default class File implements IComponent {
    // The File Class. The files are the leaves

    name: string
    referenceToParent?: Folder = undefined

    constructor(name: string) {
        this.name = name
    }

    dir(indent: string): void {
        console.log(`${indent}<FILE> ${this.name}`)
    }

    detach(): void {
        'Detaching this leaf from its parent composite'
        if (this.referenceToParent) {
            this.referenceToParent.delete(this)
        }
    }
}

./src/composite/folder.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
import IComponent from './icomponent'

export default class Folder implements IComponent {
    // A composite can contain leaves and composites

    referenceToParent?: Folder
    name: string
    components: IComponent[]

    constructor(name: string) {
        this.name = name
        this.components = []
    }

    dir(indent: string): void {
        console.log(`${indent}<DIR>  ${this.name}`)

        this.components.forEach((component) => {
            component.dir(indent + '..')
        })
    }

    attach(component: IComponent): void {
        // Detach leaf / composite from any current parent reference and
        // then set the parent reference to this composite
        component.detach()
        component.referenceToParent = this
        this.components.push(component)
    }

    delete(component: IComponent): void {
        // Removes leaf/composite from this composite this.components
        const index = this.components.indexOf(component)
        if (index > -1) {
            this.components.splice(index, 1)
        }
    }

    detach(): void {
        // Detaching this composite from its parent composite
        if (this.referenceToParent) {
            this.referenceToParent.delete(this)
            this.referenceToParent = undefined
        }
    }
}

./src/composite/icomponent.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import Folder from './folder'

export default interface IComponent {
    // A component interface describing the common
    // fields and methods of leaves and composites

    referenceToParent?: Folder

    dir(indent: string): void
    // A method each Leaf and composite container should implement

    detach(): void
    // Called before a leaf is attached to a composite
}

Output

node ./dist/composite/client.js
<DIR>  root
..<FILE> abc.txt
..<FILE> 123.txt
..<DIR>  folder_a
....<FILE> xyz.txt
..<DIR>  folder_b
....<FILE> 456.txt

<DIR>  root
..<FILE> abc.txt
..<FILE> 123.txt
..<DIR>  folder_b
....<FILE> 456.txt
....<DIR>  folder_a
......<FILE> xyz.txt

Summary

  • The Composite design pattern allows you to structure components in a manageable hierarchical order.
  • It provides flexibility of structure since you can add/remove and reorder components.
  • File explorer on Windows is a very good example of the composite design pattern in use.
  • Any system where you need to offer at runtime the ability to group, ungroup, modify multiple objects at the same time, would benefit from the composite design pattern structure. Programs that allow you to draw shapes and graphics will often also use this structure as well.