Study Notes: Design Patterns

Design patterns are systems for solutions. Patterns give names and rules for solving types of problems. The solution is the implementation of objects and classes to solve the problem.
Solutions that implement design patterns tend to have these characteristics:
- Non-repetitive
- Modular
- Reusable
- Easier to maintain
- Easier to be understood at high level
Design patterns are grouped into three categories:
- Creational (factory, singleton, ...)
- Structural (proxy, facade, ...)
- Behavioral (observer, mediator, ...)
Creational Category
When we want to control how objects are created we use creational patterns
Factory Pattern
We use factory pattern, when we want to create multiple objects with the same structure.
In this example we want to create multiple movie objects that have title, year, comment, and addComment function as their properties. Instead of typing the objects one by one we make us of factory pattern.
function createMovie(title, year, comment = "") {
return {
title: title,
year: year,
comment: comment,
addComment(str) {
this.comment = str
}
}
}
const inception = new createMovie("Inception", "20xx")
const interstellar = new createMovie("Interstellar", "20xx")
Singleton Pattern
We use the Singleton design pattern when we need exactly one instance of a class.
Here are a few real-world use cases:
- Caches
- Configuration settings
- Database connections: reuse existing connections instead of creating new ones
class Singleton {
constructor() {
if (Singleton.instance) return Singleton.instance
Singleton.instance = this
}
static getInstance() {
return this.instance;
}
}
const singletonInstance = new Singleton()
Object.freeze(singletonInstance)
export default singletonInstance
Structural Category
When we want to control objects structure and composition we use structural patterns. They define how objects and classes can be composed to provide new functionality to objects or create larger structures.
Proxy Pattern
The Proxy Pattern protects access to an object by acting as a placeholder that intercepts and redefines the operations of the target object. This pattern is particularly useful for things like network requests, as it can help avoid redundant requests. Also to avoid accessing resource-hungry objects unless its really needed.
Keywords:
- Proxy: There is a Proxy object built into ES6 that can be used to implement the proxy pattern. This object has two parameters: target & handler
- target: the object that is being proxied
- handler: a definition of any custom behaviour handled by the proxy object
- traps: built-in handler function objects, used to call the target object.
- Reflect: an object that used alongside Proxy object that has methods with the exact same name as the Proxy's object's traps. The difference is the Reflect methods forward default operations to the target object.
In this example, a user is trying to access a video from the server using ids. The proxy intercepts the request and checks if the video is cached. If it is, the request will not go through instead returning data from cache.
const target = {
1: "Video 1",
2: "Video 2"
}
const cache = {}
const handler = {
get(target, prop, receiver) {
if (prop in cache) return cache[prop]
console.log("from server")
cache[prop] = Reflect.get(...arguments)
return cache[prop]
}
}
const proxy = new Proxy(target, handler)
console.log(proxy[1]);
console.log(proxy[1]);
In this example the proxy acts as a validator
let target = []
let handler = {
set: function(target, prop, val) {
if (typeof val !== 'number') return false
target[prop] = val
return true
}
}
let proxy = new Proxy(target, handler)
target.push("2") // works
proxy.push("2") // error
Facade Pattern
The Facade Pattern is a single class that takes all of the complexity of a subsystem, and hides it. Use this pattern to create an easier interface for end users.
In this example, input string will be converted to an encrypted message with Caesar cipher and reverse cipher. Instead of having to access two different classes EncryptString class simplify the user interaction.
class EncryptString {
constructor() {}
static encrypt(str) {
return ReverseCipher.encrypt(CaesarCipher.encrypt(str, 10))
}
}
class ReverseCipher {
constructor() {}
static encrypt(str) {
let result = ""
for (let i = str.length - 1; i >= 0; i--) {
result += str[i]
}
return result
}
}
class CaesarCipher {
constructor() {}
static encrypt(str, shift) {
shift %= 26
let result = ""
for (let char of str) {
let newChar = char.charCodeAt() + shift
if (newChar > 122) {
newChar = 96 + (newChar - 122)
}
if (newChar < 97) {
newChar = 122 - (96 - newChar)
}
result += String.fromCharCode(newChar)
}
return result
}
}
console.log(EncryptString.encrypt("abc"))
Behavioral Category
When we want to control how objects behavior while interacting with another objects we use behavioral patterns. It streamline messages between unrelated objects in your code by delegating how objects can communicate. They encapsulate the communication behavior to decouple messages between senders and receivers.
Observer Pattern
Objects can have dependencies that “subscribe” to view changes to another object.
In this example, the logic is very similar to YouTube subscription. Every time an account post a video, it will be pushed to subscribers feed.
class Account {
constructor(name) {
this._id = Date.now().toString()
this._name = name
this._subscribers = []
this._feed = []
}
get feed() {
return this._feed
}
set feed(arr) {
this._feed = arr
}
addToFeed(str) {
this._feed.push(str)
}
addToSubscribers(acc) {
this._subscribers.push(acc)
}
postVideo(str) {
this._subscribers.map(subscriber => subscriber.addToFeed(str))
}
}
const footballCh = new Account("FC")
const user1 = new Account("user1")
footballCh.addToSubscribers(user1)
footballCh.postVideo("Chelsea vs Tottenham")
console.log(user1)
Mediator Pattern
Acts as a central interface to encapsulate how different parts of codebase can communicate with each other.
This pattern helps prevent having too many direct relationships between different classes or components and helps disparate components know about changes in application state. As a benefit, it also makes your code more reusable and easier to modify down the line since classes are not tightly coupled.
In this example, rather than interacting directly with Driver, Passenger uses RideHailingApp as a mediator.
class Passenger {
constructor(name) {
this._name = name
}
getDriver(app) {
app.assignDriver(this._name)
}
}
class Driver {
constructor(name) {
this._name = name
}
goOnline(app) {
app.addDriver(this._name)
}
}
class RideHailingApp {
constructor() {
this._drivers = []
}
addDriver(driver) {
this._drivers.push(driver)
}
assignDriver(passenger) {
let driver = this._drivers.splice(Math.floor(Math.random() * this._drivers.length), 1)
console.log(`${passenger} will be riding with ${driver}`)
}
}
const john = new Driver("John")
const doe = new Passenger("Doe")
const iber = new RideHailingApp()
john.goOnline(iber)
doe.getDriver(iber)
How to Select the Right Design Pattern
- Think about the interface of each object and how it will interact with other objects. Are you encapsulating the right information in each object, or should you create a new type of object?
- Consider the specifications for each object and how you will handle each property. What other objects need awareness of this object’s properties? How will you handle updates?
- Remember the high-level intent of each group of design patterns. Are we designing how an object behave or how it can be composed?
- After you pick a design, review the design to see if there’s any reason you should pick a different design. Is there something you need to refactor, or a problem that seems messy to handle?
- Remember that you can use multiple different design patterns in the same code base.
I made this to help me understand the concept while learning on codecademy.com

