POSA: design patterns with concrete sample code in Javascript/Typescript
With or without you realize, your everyday code is using one or many design patterns which are going to be introduced in this post.
If you google for Design Patterns, there will be a bunch of blog posts, books that talk about this topic. The recommended book is the canonical Design Patterns Elements of Reusable Object-Oriented Software. Another recommended book is Code Complete: A Practical Handbook of Software Construction, Second Edition 2nd Edition. Pattern-oriented Software Architecture, Patterns for Concurrent and Networked Objects, volume 2.
In this post I will briefly list all patterns mentioned in this book and provide a simple explanation/concrete example for each pattern
First, list of design patterns by their purpose and scope
For better readability, I will use typescript for the sample code.
I will also try to keep the sample code as clean as possible to emphasize the pattern's purpose.
1. Factory method (another name: Virtual Constructor)
create a class based on an input parameter
const createAnimalClass = (kind: 'dog' | 'cat') => kind === 'dog' ? Dog : Cat
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
2. Adapter (class, another name: Wrapper)
There is an existing class, you want to use it but the interface is not compatible, thus, you need an adapter/wrapper to create compatibility.
For example
class Logger {
writeLog(str: string){}
}
class ConsoleLogger {
log(kind: 'warn' | 'error' | 'log', str: string){}
}
class ConsoleLoggerAdapter {
constructor(){this.consoleLogger = new ConsoleLogger()}
writeLog(str: string){this.consoleLogger.log('log', str)}
}
Currently, there are many locations in the old code base that use interface of Logger
, now you migrate your system to use ConsoleLogger
but the interfaces are incompatible. ConsoleLoggerAdapter
is written to wrap new ConsoleLogger
and expose the interface as you want.
3. Interpreter
A class that can interpret a language and stores the input string in an internal state instead of the raw input.
Consider regular expression as a language, then RegExp is an interpreter.
const regExp = new RegExp('(0|[1-9][0-9]*)')
4. Template method
Superclass (parent class) defines the skeleton of an operation which, operation, includes one or more sub-operations. These sub-operations can be defined/re-defined by sub-classes. The skeleton keeps the same regardless of the sub-class implementation of the overwritten sub-operations.
class Buyer {
const unitPrice = 10
calculateTotal(amount: number){
return `the total is ${this.discount(amount * this.unitPrice)}`
}
}
class Agency extends Buyer {
discount(total: number){return total * 0.9 * 1.1} //discount and tax
}
class Guest extends Buyer {
discount(total: number){return total * 1.1} //tax
}
5. Abstract Factory (another name: Kit)
Similar to 1. Factory method. This pattern is used to initiate an instance from a class among a family of them, based on the parameter.
const createAnimal = (kind: 'dog' | 'cat', name: string) => kind === 'dog'
? new Dog(name)
: new Cat(name)
6. Builder
A class that is used to create instances of another class. However, this builder class provides many convenient methods to build the final instance step-by-step, and it remembers each method call internally to give the final or currently being built instance.
class StringBuilder {
getString(): string
reset(str: string): StringBuilder
append(str: string): StringBuilder
prepend(str: string): StringBuilder
replace(str: string): StringBuilder
space(str: string): StringBuilder
}
new StringBuilder().reset('hello').space().append('world').getString()
7. Prototype
Use object-keeping behaviors (methods) as a prototypical instance. Every time a new object is created, the new object copies behaviors from the prototypical instance. This prototype pattern is supported native in Javascript and it brings much strength to the Javascript language itself. Object.create
method takes the first parameter as the prototype and instantiates a new object.
class Human{name: string}
const me = Object.create(Human.prototype, {name: 'transang'})
8. Singleton
Ensure at most one instance of a class can be initiated.
let instance
const getMyClassInstance = () => instance || (instance = new MyClass())
9. Adapter (object)
Same as Adapter for class
10. Bridge (other names: Handle, Body)
Bridge design pattern is used to separate implementation and interface. One of the most application of this pattern is that it enables the implementation can be changed at runtime.
This pattern is similar but different from the inheritance pattern.
class Human{
const pet: IPet
setPet(pet: IPet): void
feed(amount: number){this.pet.eat(amount)}
}
interface IPet {eat(amount: number): void}
class Dog implements IPet {}
class Cat implements IPet {}
The Human
class keeps responsibility as a bridge class, separates the concern of the interface IPet
from its implementations ( Cat
and Dog
).
Difference from inheritance pattern:
In inheritance pattern, you would need to fix the implementation of IPet
in the Human
class, which make it difficult to extend the IPet
interface, or add a new implementation.
11. Composite
A composite pattern is used to represent part-whole hierarchies.
The image that you want to build a tree whose node can be a leaf or a parent of one or more children nodes. A real-life example of this tree structure is a menu with a multi-level sub-menus.
A left node is called an individual object, while a middle node (can be the root node) composes multiple children, thus, is called composition.
The composite pattern helps to treat individual objects and compositions uniformly, in order to avoid defining different classes for each. To achieve this purpose, the composite class should introduce an interface that allows the individual objects to directly implementing the interface while the compositions forward operations to their children.
class Menu { // the general object which exposes interface for both individuals and composites to follow
draw(): void
}
class CompositeMenu { // composite object which contains and forwards operations to its children
draw(){
for (const child of this.children) child.draw()
}
}
class TextOption extends Menu { // individual object which directly implement menu operations
}
class ButtonOption extends Menu { // similar to TextOption
}
12. Decorator (another name: Wrapper)
Decorator pattern attaches additional behavior, functionality to an existing object. It is a flexible replacement for sub-classing.
Object in the above statement can be an instance of a class or a class itself.
let car = new Car()
car = attachHandler(car)
car = addWheels(car)
car = fillGasoline(car)
car.start()
Another example for class decorator
class Printer {
print(kind: 'error' | 'warning' | 'log', str: string)
}
const setPrintType = (kind: 'error' | 'warning' | 'log') => (PrinterClass: typeof Printer) => {
class DecoratedPrinter extends PrinterClass {
print(str: string){super.print(kind, str)}
}
}
const LogPrinter = setPrintType('log')(Printer)
const WarningPrinter = setPrintType('warning')(Printer)
const ErrorPrinter = setPrintType('error')(Printer)
13. Facade (or Façade)
Facade pattern helps your life easier by providing a simple and user-friendly API hub for sub-system users without knowledge about the sub-system's internal process.
One of the best examples is jQuery. It wraps the DOM element and provides a more user-friendly API interface and handles all DOM operations and browser compatibility internally.
$(el).css()
$(el).animate()
14. Flyweight
Flyweight pattern shares object in order to save memory or cache(memoize) object to prevent re-creation, re-calculation.
const flyweight = (intensiveCalc: (arg: any) => any) => {
const results = new Map()
return (arg: any) => {
if (results.has(arg)) return results.get(arg)
const result = intensiveCalc(arg)
results.set(arg, result)
return result
}
}
The above implementation considers intensiveCalc
is a function which accepts only one argument, it also memoizes all entered parameters and returned result. Thus, it may catch memory overflow. Use it at your own risk.
15. Proxy (another name: Surrogate)
As the name suggested, a proxy is a placeholder to control other objects. Proxy is a very general concept, used in many other fields such as network connection, ...
class FileProxy{
read(){
//check permission
//order a system call to read file
}
write(){ // check permission, order a system call to write file
}
}
The File
class itself can be considered as a proxy for file objects in OS. The file object in OS may be another proxy which exposes an interface to connect user call and real data stored in the hard disk.
16. Chain of Responsibility
Allow more handlers can handle the object. The list of handlers should be able to be specified dynamically at run-time.
The syntax looks similar to the decorator, but they serve different purposes. The best suite sample is the Promise
object
Promise.resolve(3).then(x => x + 2).then(x => x - 3).then(console.log)
17. Command (other names: Action, Transaction)
Encapsulate a request as an object, help decoupling object which knows how to call a command, and, the object which handles how to execute the command.
This pattern is also able to store a list of executed commands, support undoable operations.
class Dispatcher{
dispatch(action: IAction): void
undo(): void
listActions(): IAction[]
}
18. Iterator (another name: cursor)
A way to access the elements of an aggregate object (list, array, map, set, ...) without exposing the underlying representation.
const it = [1, 2, 3][Symbol.iterator]()
while (true) {
const {value, done} = it.next()
console.log(value)
if (!done) break
}
19. Mediator
A bandmaster stays in the center to encapsulate the interaction between a set of components. Member component directly communicates with the mediator, instead of talking with other components.
class Mediator {
const boss: IHuman
const pets: IPet[]
const storage: IFood[]
feedPets(){
//tell this.boos to take food from this.storage, then feed this.pets
}
}
Compared to Façade:
- Façade only exposes existing functions
- Mediator add more logic (functions) to the system
- The mediator is more like a controller in an MVC pattern, while Façade is merely a way to hide method names behind an interface.
- Façade is a structural pattern, Mediator is a behavioral pattern.
20. Memento (another name: Token)
Memento pattern includes three components: originator, caretaker, and memento.
Caretaker: a subject (usually our application itself) wants to do some operations on an originator, which changes the originator's internal state. And the caretaker also wants to restore the originator's state later.
Originator: an object (usually a target class) is the target of the caretaker, which has its own internal state
Memento: like a snapshot of the originator's state, which helps protect the originator's encapsulation in design. Restoring/storing the originator's state must be done via memento.
The original design requires
- only the originator that produced the memento would be permitted to access the memento's internal state
- the caretaker can read meta information of the memento object (such as creation time, operation name, ...). However, the caretaker should not access the memento object's state directly.
class Originator {
class Memento {
value: number
constructor(value: number){this.value = value}
getValue(){return value}
}
value = 0
add(amount: number){this.value += amount}
subtract(amount: number){this.value -= mount}
inc(){this.add(1)}
dec(){this.subtract(1)}
restore(memento: Memento){this.value = memento.getValue()}
store(): Memento{return new Memento(this.value)}
}
//caretaker is our application
const originator = new Originator()
originator.add(10)
originator.dec()
const memento = originator.store()
originator.inc()
originator.restore(memento)
21. Observer (other names: Dependents, Publish-Subscribe)
A kindly well-known pattern in Javascript. There is a popular RxJs library which implements many features around observer pattern. Currently, Observable is in stage-1 of the ECMAScript. Hope that this proposal will be finalized soon.
The observer pattern is a one-to-many dependency between objects. There is one independent variable (called publisher), while there are many dependent variables (called subscribers). Once the publisher changes state, all its dependent subscribers are notified and update automatically
class Observer{
publishers: Publisher[] = []
subscribe(publisher: Publisher){
if (!this.publishers.includes(publisher)) {
this.publishers = [...this.publishers, publisher]
//also return an unsubscription
return () => {this.publishers = this.publishers.map(p => p !== publisher)}
}
}
change(){
for (const publisher of this.publishers) publisher.notify()
}
}
class Publisher {
notify(){}
}
22. State (another name: Objects for States)
An object has its own internal state and its behavior of the same method changes based on the state at the time of being called.
class Connection {
state: 'open' | 'connecting' | 'connected' | 'closed' = 'open'
open(){} // throw error if this.state is not 'open'
close(){} // throw error if this.state is 'closed'
send(data: string){} // throw error if this.state is not 'connected'
}
23. Strategy (another name: Policy)
When there is a family of algorithms. The strategy pattern lets the algorithm vary independently from clients that use it, encapsulates each algorithm, and makes them interchangeable.
Passportjs is a well-known nodejs library that implements this pattern.
class Connection {
constructor(public strategy: Strategy){}
send(data: string){this.strategy.send(data)}
}
class Strategy {
send(){throw new Error('not implemented')}
}
class TCPStrategy extends Strategy {
send(){}
}
class UDPStrategy extends Strategy {
send(){}
}
24. Visitor
In your application, there are multiple types of elements, and also multiple types of operations that each operation has its own different way to treat each element type.
Without visitor pattern, each concrete element class has to check the operation type being applied to behave. This increases the maintenance burden when a new operation is defined. Because all concrete element implementation has to modify its behavior on the new operation.
Visitor pattern makes an abstract for all operations via a Visitor
class whose concrete sub-class must define its own action for each element type. All elements delegate to a Element
class which simply calls accept(visitor)
when being visited.
This pattern should be applied when
- the
Element
class has multiple sub-classes and you want to perform operations on these elements that depend on concrete sub-class. - operations are distinct and unrelated. Visitor lets you keep related operations together by defining them in one class
Element
class is rarely changed or added new sub-class. Because whenever a new subclass is defined, all concrete implementations (sub-classes)Visitor
have to implement behavior on the newly defined element. A practice solution, in this case, is defining default behavior for unknown elements. However, it is not recommended because it is prone to forget to add the implementation of the new element in someVisitor
's sub-classes.
class Visitor{
visitA(a: ElementA){throw new Error('n_iplm')}
visitB(b: ElementB){throw new Error('n_iplm')}
}
class Element{
accept(visitor: Visitor){throw new Error('not implemented')}
}
class ElementA extends Element {
accept(visitor: Visitor){visitor.visitA(this)}
}
class ElementB extends Element {
accept(visitor: Visitor){visitor.visitB(this)}
}
After the Design Patterns Elements of Reusable Object-Oriented Software book had been published, there were many new design patterns have been invented. There is a list of various patterns from wikipedia page
25. Dependency injection
Injector
decides which Service
to be used in Client
.
Without dependency injection, client
initiates service
in its constructor.
Angular is a well-known framework that makes use of this pattern.
//main application is an injector
const service = new MyService()
const client = new Client(service)
client.run()
26. Lazy initialization
Similar to flyweight. Delay the creation of an object or calculation of a value or some expensive process until the first time it is needed.
let googleMapsInstancePromise: Promise<GoogleMaps>
const getGoogleMapsInstance = () => googleMapsInstancePromise || (googleMapsInstancePromise = (async () => {
const GoogleMaps = await import('./GoogleMaps')
return new GoogleMaps()
})())
Also, check my related post Promise-based semaphore pattern in Javascript.
27. Multiton
Generalization of singleton pattern, so-called registry of singleton
MyClass.getInstance(InstanceTypeA)
MyClass.getInstance(InstanceTypeB)
MyClass.getInstance(InstanceTypeC)
Demerit: be careful of memory leak because the initialized instances stay for the whole application life cycle.
28. Object pool
To increase performance, in some cases you might want to pay the cost of memory by storing initialized instances stead of creating/destroying them by demand.
const pool: {[key: number]: number} = {}
const fib = (n: number) => pool[n] !== undefined
? pool[n]
: (pool[n] = calculateFib(n))
}
29. Resource acquisition is initialization (RAII)
Used to describe a particular language behavior.
The resource must be acquired during the constructor and released in the destructor.
class Session {
file: File
constructor(path: string){this.file = new File(path)}
close(){this.file.close()}
}
30. Front controller
Is a popular pattern used in a web application (server) to handle all requests/navigation in a single controller.
The front controller is a specialized pattern of a mediator.
The front controller is a specialized pattern of a controller in the MVC pattern.
The opposite pattern of the front controller is the page controller.
In the front controller pattern, there is only a front controller handling all requests. In opposite, in the page controller pattern, there are many page controllers in which each page controller handles a specific request.
It is often to see apache config to forward all requests to index.php
or front-controller.php
via .htaccess
config
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php?path=$1 [NC,L,QSA]
Nodejs example
import express from 'express'
const app = express()
app.get('/login', loginMiddleware)
app.get('/favicon.ico', (req, res) => res.sendFile(path.resolve(__dirname, 'assets/favicon.ico')))
31. Marker
An interface to add meta information to an object in run-type.
There are many real-life cases where this pattern is applied.
In apollo graphql __typename
key is added for an object to be type-distinguished from others, helping typescript to correctly interpret the object's type.
In Java, Serializable
interface indicates class whose instances can be written to ObjectOutputStream
.
class Transportation {
speed: number
}
class Car extends Transporation {
__typename = 'Car'
}
class Bike extends Transporation {
__typename = 'Bike'
}
32. Module
In the module pattern, source code is separated by functionality to manage source code better.
In programming language which does not support module natively, developers can design their code to follow this design pattern.
//a.js
export const a = 1
//b.js
import {a} from './a'
console.log(a)
33. Twin
Simulate multiple inheritances in programming languages that do not support multiple inheritances natively.
Method 1: use multiple fields
class Parent1 {}
class Child1 extends Parent1 {
//overwrite Parent1's fields
}
class Parent2 {}
class Child2 extends Parent2 {
//overwrite Parent2's fields
}
class Parent3 {}
class Child3 extends Parent3 {
//overwrite Parent3's fields
}
class CommonChild { //multiple inheritance
child1: Child1
child2: Child2
child3: Child3
//other fields
}
Method 2: pivot one, and twin others
class Parent1 {}
class Parent2 {}
class Child2 extends Parent2 {
//overwrite Parent2's fields
}
class CommonChild extends Parent1 { //multiple inheritance
twin: Child2
//other fields
}
34. Blackboard
Firstly introduced and applied in speech recognition application by members of the HEARSAY-II project.
This design includes three components
- blackboard: store information results from many algorithms which run separately in knowledge component
- knowledge: a set of programs/algorithms which runs and post their result to the blackboard
- control: use results in the blackboard for further processing, or check the status and give the final output of the whole system
For example, in a speech recognition application: a tape of audio recording is cut into many small pieces. Each piece of the audio recording is recognized by a process called knowledge.
This knowledge process posts its output/result to the blackboard in an appropriate position.
The control component periodically checks the blackboard for the semantics of a group of words (sentence). If the sentence should be improved, the control lets the knowledge process run again to provide the next prediction. Or the control can fix the sentence and give the final result.
class BlackBoard {
update(result: number, pos: number){}
}
class Knowledge {
constructor(private board: BlackBoard, private pos: number)
run(data: Int8Array){
this.board.update(this.dataToResult(data), this.pos)
}
dataToResult(data: Int8Array){}
}
class Control {
intervalId: number
constructor(private board: BlackBoard){}
run(){
if (!this.intervalId) this.stop()
this.intervalId = setInterval(() => this.check(), 3000)
}
stop(){
if (this.intervalId) clearInterval(this.intervalId)
this.intervalId = undefined
}
}
35. Null object
Instead of returning a null reference, return a null object which implements default behavior. This reduces the complication of the caller in a way that the caller does not have to care about the nullity of the returned result from the callee.
class Animal {countLegs{throw new Error('n_impl')}}
class Human extends Animal {
countLegs(){return 2}
}
class Dog extends Animal {
countLegs(){return 4}
}
class NullAnimal extends Animal { // null object class
countLegs(){return 0}
}
const createAnimal = (type: string){
return type === 'human' ? new Human() : type === 'dog' ? new Dog() : new NullAnimal()
}
console.log(createAnimal())
36. Servant
Respecting the rule "Separation of Concerns", servant class in server pattern is a class that provides some behavior to a group of classes. Let's see an example
interface IHasTime {
setTime(time: number): void
getTime(): number
}
class TimeHandler {//servant class of IHasTime
constructor(private timer: IHasTime){}
nextDay()//move by 1 day next
nextHour()
resetTimeInADay()
convertToTimeZone()
}
//following classes are redudant to explain the servant class definition. They are defined for completeness sake
class Clock extends IHasTime {
//implement time in a day
}
class Calendar extends IHasTime {
//implement date only
}
class Timestamp extends IHasTime {
//implement date and time
}
Another well-known example is when we want some additional features on the default class, e.g. Date
, String
, .... It should never modify/add methods to these classes directly. Instead, we can define servant classes for our own purpose without affecting the global environment.
37. Specification
Encapsulate domain rules in objects. In a nutshell, this pattern is no more than the combinations of multiple predicates. The specification can be chained with other specifications, making a new specification easily maintainable and highly customizable.
//generic specification definition
class Predicate<T> { //or Specification. I prefer the 'Predicate' name
constructor(private predicate: (u: T) => boolean){}
and(other: Predicate){ return new AndPredicate(this, other)}
or(other: Predicate){return new OrPredicate(this, other)}
not(){return new NotPredicate(this)}
isSatisifiedBy(obj: T){return this.predicate(u)}
}
class AndPredicate<T> extends Predicate<T> {
constructor(private p1: Predicate<T>, private p2: Predicate<T>){}
isSatisifiedBy(obj: T){return this.p1.isSatisifiedBy(obj) && this.p2.isSatisifiedBy(obj)}
}
class OrPredicate<T> extends Predicate<T>{/*similar to AndPredicate*/}
class NotPredicate<T> extends Predicate<T>{
constructor(private predicate: Predicate<T>){}
isSatisifiedBy(obj: T){return !this.predicate.isSatisifiedBy(obj)}
}
//application of specification pattern
enum Role {
'Admin',
'Accountant',
'Shipper',
'Worker',
'Security',
'Manager',
'Sale'
}
class User {
public roles: Role[] //user can have multiple roles
}
const userHasEntranceKey = new Predicate((u: User) => /*check user role to have privilege to keep entrance key*/)
class CanDiscount extends Predicate<User> {
constructor(private order: Order){}
isSatisifiedBy(obj: User){
//more complicated logic to check if an order can be discount by the input user
//e.g. this.order.total > 1000
}
}
//check
userHasEntranceKey.and(new CanDiscount(order)).isSatisifiedBy(user)
38. Closure
The closure is a function with an associated environment.
const addX = x => y => x + y
const add3 = addX(3)
console.log(add3(1)) //print 4
Another more complicated and real-life example
import {AnyObject} from 'final-form'
import React, {ComponentType} from 'react'
import {Field, FieldRenderProps} from 'react-final-form'
export type IFieldsRenderProps<
FieldTypes extends {[key: string]: [any, HTMLElement]}
> = {
[key in keyof FieldTypes]: FieldRenderProps<FieldTypes[key][0], FieldTypes[key][1]>
}
const Fields = <
FieldTypes extends {[key: string]: [AnyObject, HTMLElement]},
TProps extends {},
>(
{
names, component: Comp, props = {} as TProps
}: {names: ReadonlyArray<keyof FieldTypes & string>, props?: Omit<TProps, keyof FieldTypes>}
& {component: ComponentType<TProps & IFieldsRenderProps<FieldTypes>>}
) => names.reduce((acc, cur) => p => <Field name={cur}>
{renderProps => acc({...p, [cur]: renderProps})}
</Field>,
(p: IFieldsRenderProps<FieldTypes>) => <Comp {...props} {...p}/>
)({} as unknown as IFieldsRenderProps<FieldTypes>)
Fields.displayName = 'Fields'
export default Fields
Without type definition the part which makes use of closure will be
const Fields = ({
names, component: Comp, props = {} as TProps
}) => names.reduce((acc, cur) => p => <Field name={cur}>
{renderProps => acc({...p, [cur]: renderProps})}
</Field>,
p => <Comp {...props} {...p}/>
)({})
The reducer parameter (first parameter of .reduce
) returns a closure, which helps aggregate value in an array from end to start.
One merit of using closure is that it supports type inference better than procedure programming.
39. Currying
...to be continued...
This post is getting much longer than expected.
Functional programming is one of my favorite topics. However, I have run out of time for this post.
Let me leave some references here for those who are interested can refer for further detail. I will go back to continue this post when I am sparse.
https://en.wikipedia.org/wiki/Currying
https://en.wikipedia.org/wiki/Function_composition_(computer_science)
https://en.wikipedia.org/wiki/Function_object
https://en.wikipedia.org/wiki/Monad_(functional_programming)
https://en.wikipedia.org/wiki/Generator_(computer_programming)
https://en.wikipedia.org/wiki/Continuation
https://en.wikipedia.org/wiki/Call-with-current-continuation
http://community.schemewiki.org/?call-with-current-continuation-for-C-programmers
https://marijnhaverbeke.nl/cps/
https://en.m.wikipedia.org/wiki/Setjmp.h
https://en.m.wikipedia.org/wiki/Coroutine#Implementations_for_C