Skip to content

Decorator Design Pattern

Video Lecture

Section Video Links
Decorator Pattern Decorator Decorator Pattern  
Decorator Use Case Decorator Use Case Decorator Use Case  

Overview

The decorator pattern is a structural pattern, that allows you to attach additional responsibilities to an object at runtime.

The decorator pattern is used in both the Object-Oriented and Functional paradigms.

The decorator pattern adds extensibility without modifying the original object.

The decorator forwards requests to the enclosed object and can perform extra actions.

You can nest decorators recursively.

Terminology

  • Component Interface: An interface for objects.
  • Component: The object that may be decorated.
  • Decorator: The class that applies the extra responsibilities to the component being decorated. It also implements the same component interface.

Decorator UML Diagram

Decorator Pattern UML Diagram

Source Code

./src/decorator/decorator-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
// Decorator Concept Sample Code

interface IComponent {
    method(): string
}

class Component implements IComponent {
    method(): string {
        return 'Component Method'
    }
}

class Decorator implements IComponent {
    #object: IComponent

    constructor(object: IComponent) {
        this.#object = object
    }

    method(): string {
        return `Decorator Method(${this.#object.method()})`
    }
}

// The Client
const COMPONENT = new Component()
console.log(COMPONENT.method())

// The component can be decorated
const Decorated = new Decorator(COMPONENT)
console.log(Decorated.method())

// The decorated component can be decorated again
const Decorated2 = new Decorator(Decorated)
console.log(Decorated2.method())

Output

node ./dist/decorator/decorator-concept.js
Component Method
Decorator Method(Component Method)
Decorator Method(Decorator Method(Component Method))

Decorator Use Case

Let's create a custom class called Value that will hold a number.

Then add decorators that allow addition (Add) and subtraction (Sub) to a number (Value).

The Add and Sub decorators can accept numbers directly, a custom Value object or other Add and Sub decorators.

Add, Sub and Value all implement the IValue interface and can be used recursively.

Note that in this example use case, I have created the Add, Sub and Value as functions that return new instances of classes _Add, _Sub and _Value. This was not necessary, but it means that I can use the Add, Sub and Value in a recursive manner without needing to prefix the new keyword in front of each usage all the time.

E.g,

console.log(Add(Sub(Add(C, B), A), 100).value)

Alternatively, I could have named my classes as Add, Sub and Value and then used them recursively directly as

console.log(new Add(new Sub(new Add(C, B), A), 100).value)

Example UML Diagram

Decorator Pattern in Context

Source Code

./src/decorator/client.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Decorator Use Case Example Code

import Value from './value'
import Add from './add'
import Sub from './sub'

const A = Value(1)
const B = Value(2)
const C = Value(5)

console.log(Add(A, B).value)
console.log(Add(A, 100).value)
console.log(Sub(C, A).value)
console.log(Sub(Add(C, B), A).value)
console.log(Sub(100, 101).value)
console.log(Add(Sub(Add(C, B), A), 100).value)
console.log(Sub(123, Add(C, C)).value)
console.log(Add(Sub(Add(C, 10), A), 100).value)
console.log(A.value)
console.log(B.value)
console.log(C.value)

./src/decorator/value.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export interface IValue {
    value: number
}

class _Value implements IValue {
    value: number
    constructor(value: number) {
        this.value = value
    }
}

export default function Value(value: number): IValue {
    return new _Value(value)
}

./src/decorator/add.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { IValue } from './value'

class _Add implements IValue {
    value: number
    constructor(val1: IValue | number, val2: IValue | number) {
        const left = Object.prototype.hasOwnProperty.call(val1, 'value')
            ? (val1 as IValue).value
            : (val1 as number)
        const right = Object.prototype.hasOwnProperty.call(val2, 'value')
            ? (val2 as IValue).value
            : (val2 as number)
        this.value = left + right
    }
}

export default function Add(
    val1: IValue | number,
    val2: IValue | number
): IValue {
    return new _Add(val1, val2)
}

./src/decorator/sub.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { IValue } from './value'

class _Sub implements IValue {
    value: number
    constructor(val1: IValue | number, val2: IValue | number) {
        const left = Object.prototype.hasOwnProperty.call(val1, 'value')
            ? (val1 as IValue).value
            : (val1 as number)
        const right = Object.prototype.hasOwnProperty.call(val2, 'value')
            ? (val2 as IValue).value
            : (val2 as number)
        this.value = left - right
    }
}

export default function Sub(
    val1: IValue | number,
    val2: IValue | number
): IValue {
    return new _Sub(val1, val2)
}

Output

node ./dist/decorator/client.js
3
101
4
6
-1
106
113
114
1
2
5

Summary

  • Use the decorator when you want to add responsibilities to objects dynamically without affecting the inner object.
  • You want the option to later remove the decorator from an object in case you no longer need it.
  • It is an alternative method to creating multiple combinations of subclasses. I.e., Instead of creating a subclass with all combinations of objects A, B, C in any order, and including/excluding objects, you could create 3 objects that can decorate each other in any order you want. E.g., (C(A(C))) or (B(C)) or (A(B(A(C))))
  • The decorator, compared to extending, is more flexible since you can easily add/remove the decorators at runtime. E.g., use in a recursive function.
  • A decorator supports recursive composition. E.g., halve(halve(number))
  • A decorator shouldn't modify the internal objects data or references. This allows the original object to stay intact if the decorator is later removed.