0%

10 Design Patterns

This article will talk about some classical desgin patterns that every developer should know to level up.

10 Design Patterns

1. Category

  1. Creational Design Pattern — How objectes are created
    • Singleton Design Pattern
    • Prototype
    • Builder
    • Factory
  2. Structural Design Pattern — How objects are related to each other
    • Facade Design Pattern
    • Proxy
  3. Behavioral Design Pattern — How objects communicate with each other
    • Iterator Design Pattern
    • Observer
    • Mediator
    • State

2. Creational Pattern

2.1. Singleton

It’s a type of object that cam only be instantiated once.

A singleton eample in TS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Settings {
static instance: Settings;
public readonly mode = 'dark';

// prevent new with private constructor
private constructor() {}

static getInstance(): Settings {
if (!Settings.instance) {
Settings.instance = new Settings();
}

return Settings.instance;
}
}

const settings = Settings.getInstance();

This works too.

1
2
3
const settings = {
dark: 'true'
};

2.2. Prototype

It’s a fancy word for clone.

One problem with inheritance is that it can lead to a complex hierarchy of code.

The Prototype pattern is an alterantive way to implement inheritance but instead of inheriting functionality from a class, it comes from an object that’s already been created. This creates a flat prototype chain that makes it much easier to share functionalities between objects especially in a dynamic language like JavaScript, which supports prototypal inheritance out of box.

A prototype example in JS:

1
2
3
4
5
6
7
8
9
10
11
12
const zombie = {
eatBrains() {
return 'yum brain';
}
}

const chad = Object.create(zombie, { name: { value: 'chad' } });

console.log(chad); // { name: 'chad' }
chad.eatBrains(); // works

Object.getPrototypeOf(chad); // {eatBrains: f}

When it comes to classes in javascript, prototype refers to its constructor.

2.3. Builder

Imagine you are running a hot dog stand and when a custimer places an order they need to tell you everything they want in the sandwich in the constructor. That works, but it’s kind of hard to keep track of all these options and we might want to defer each step to a later point.

1
2
3
4
5
6
7
8
9
10
class HotDog {
constructor(
public bun: string,
public ketchup: boolean,
public mustard: boolean,
public kraut: boolean
) { }
}

new HotDog('wheat', false, true, true);

With the builder pattern, we create the object step by step using methods rather than the constructor and we can even delegate the building logic to an entirely different class.

In JavaScript, we’ll have each method return this which is a reference to the object instance that allows us to implement method chaining where we instantiate an object then chain methods to it but always get the object as the return value. You’ll come across this pattern frequently with libraries like jQuery but it’s gone a bit out of style in recent years.

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
class HotDog {
constructor(
public bread: string,
public ketchup?: boolean,
public mustard?: boolean,
public kraut?: boolean
) {}

addKetchup() {
this.ketchup = true;
return this;
}
addMustard() {
this.mustard = true;
return this;
}
addKraut() {
this.kraut = true;
return this;
}
}

const myLunch = new HotDog('gluten free');

myLunch
.addKetchup()
.addMustard()
.addKraut() // Method Chaining

2.4. Factory

Instead of using the new keyword to instantiate an object, you use a function or method to do it for you.

1
2
3
4
5
6
7
class IOSButton { }

class AndroidButton { }

// Without Factory, that's not very maintainable
const button1 = os === 'ios' ? new IOSButton() : new AndroidButton();
const button2 = os === 'ios' ? new IOSButton() : new AndroidButton();

Instead, we can create a subclass or function that will determine which object to instantiate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ButtonFactory {
createButton(os: string): IOSButton | AndroidButton {
if (os === 'ios') {
return new IOSButton();
} else {
return new AndroidButton();
}
}
}

// With Factory
const factory = new ButtonFactory();
const btn1 = factory.createButton(os);
const btn2 = factory.createButton(os);

3. Structural Pattern

3.1 Facade

A facade is the face of a building, inside that building, there’s a lot of complexity that the end user doesn’t need to know about.

A facade is basically just a simplified api to hide other low-level details in your code base.

A facade example in 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
class PlumbingSystem {
// low level access to plumbing system
setPressure(v: number) {}
turnOn() {}
turnOff() {}
}

class ElectricalSystem {
// low level access to electrical system
setVoltage(v: number) {}
turnOn() {}
turnOff() {}
}

class House {
private plumbing = new PlumbingSystem();
private electrical = new ElectricalSystem();

public turnOnSystems() {
this.electrical.setVoltage(120);
this.electrical.turnOn();
this.plumbing.setPressure(500);
this.plumbing.turnOn();
}

public shutDown() {
this.plumbing.turnOff();
this.electrical.turnOff();
}
}

const client = new House();
client.turnOnSystems();
client.shutDown(); // Ugly details hidden

Almost every package that you install with javascript could be considered a facade in some way.

3.2 Proxy

A fancy word for substitute. You can replace a target object with a proxy.

A great case study is the reactivity system in Vue.js. In Vue, you create data but the framework itself needs a way to intercept that data and update the ui whenever that data changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const original = { name: 'jeff' };

const reactive = new Proxy(original, {
get(target, key) {
console.log('Tracking: ', key);
return target[key];
},
set(target, key, value) {
console.log('Updating UI...');
return Reflect.set(target, key, value);
},
});

reactive.name; // logs 'tracking name'
reactive.name = 'bob'; //logs 'updating UI...'

The way Vue handles that is by replacing the original object with a proxy. A proxy takes the original object as the first argument then a handler as the second argument, inside of which, we can override methods like get and set which allows us to run code whenever a property is accessed on the object or changed.

The end user can now work with a proxy just like the original object but it can trigger the side effects behind the scenes.

Proxies are also commonly used when you have a very large object that would be expensive to duplicate in memory.

4. Behavioral Pattern

4.1 Iterator — Pull-Based System

The iterator pattern allows you to traverse through a collection of objects. Modern languages already provide abstractions for the iterator pattern like the for loop. When you loop over an array of items, you’re using the iterator pattern.

Implement our own iterator pattern to achieve “range function”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function range(start: number, end: number, step=1) {
return {
[Symbol.iterator]() {
return this;
},
next() {
if (start < end) {
start = start + step;
return { value: start, done: false };
}
return { done: true, value: end };
}
}
}

for (const n of range(0, 20, 5))) {
console.log(n);
}

4.2 Observer — Push-Based System

The observer pattern allows many objects to subscribe to events that are broadcast by another object. It’s a one-to-mant relationship.

This pattern is used all over the place in app development. Like in firebase, when your data changes on the server, all your client apps are subscribed to it and automatically updated with the latest data.

rijx library example for observer

1
2
3
4
5
6
7
8
9
10
import { Subject } from 'rxjs';

const news = new Subject();

const tv1 = news.subscribe(v => console.log(v + 'via Den TV'));
const tv2 = news.subscribe(v => console.log(v + 'via Batcave TV'));
const tv3 = news.subscribe(v => console.log(v + 'via Airport TV'));

news.next('Breaking new: ');
news.next('The war is over ');

Personally, I like to think of this as a loop that unfolds over the dimension of time. (Haven’t got the idea yet)

4.3 Mediator

A mediator is like a middleman or broker.

A practical example, in the express.js web framework, there’s a middleware system, you have incoming requests and outgoing responses. Middleware sits in the middle by intercepting every request and transform it into the proper format for the response. It provides a separation of concerns and eliminates code duplication.

4.4 State

State Pattern is where an object behaves differently based on a finite number of states.

Without state pattern, you’ve likely used conditional logic or switch statements to handle a bunch of different possibilities based on the state or data in your application. Code like this generally doesn’t scale very well.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Without State Pattern
class Human {
think(mood) {
switch (mood) {
case "happy":
return "I am happy";
case "sad":
return "I am sad";
default:
return "I am neutral";
}
}
}

The state pattern allows you to start with one base class then provide it with different functionality based on its internal state. The idea is related to finite state machines and libraries like xstate where the goal is to make the object’s behavior predictable based on its underlying state.

Another way to go about it would be to create a separate class for each possible state, inside each class, we will have an identical method that behaves differently. Now in the Human Class, we set the state as a property and whenever that method is called, we delegate it to its current state. That means whenever the state changes, the object will behave in a completely different way, but at the same time, we don’t have to change the api or use a bunch of conditional logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface State {
think(): string;
}

class Human {
state: State;

constructor() {
this.state = new HappyState();
}

think() {
return this.state.think();
}

changeState(state) {
this.state = state;
}
}

Reference

“10 Design Patterns Explained in 10 Minutes” — https://www.youtube.com/watch?v=tv-_1er1mWI