This is a recap from the talk 'Design Patterns in Javascript' by Tim Winfred. Check out the full talk on Youtube!
Design patterns are a bit of a controversial topic in the dev community. While some developers believe they are overly complicated, others are dogmatic about using them. As a JavaScript engineer, there's a good chance you'll need to know them at some point in your career, whether you subscribe to design patterns or not. Let's unpack a few commonly-used JavaScript design patterns together, and discuss how they can make your code cleaner and easier to maintain.
What is a design pattern?
In software engineering, a __ design pattern__ is a general repeatable solution to a commonly occurring problem in software design. A design pattern isn't a finished design that can be transformed directly into code. It is a description or template for how to solve a problem that can be used in many different situations.
In addition, patterns allow developers to communicate using well-known, well understood names for software interactions. Common design patterns can be improved over time, making them more robust than ad-hoc designs.
Types of JavaScript Design Patterns
- Creational Design Patterns: Situation-specific patterns that reduce complexity by controlling object creation.
- Structural Design Patterns: Realizing relationships among entities to simplify design.
- Behavioral Design Patterns: Identify common communication patterns among objects to increase flexibility in carrying out communication.
There is a fourth group called the Concurrey Design Patterns, but these are patterns that deal with the multi-thread programming pattern.
Factory Design Pattern (Creational)
Define an interface for creating a single object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
/**
* The Creator class declares the factory method that is supposed to return an
* object of a Product class. The Creator's subclasses usually provide the
* implementation of this method.
*/
abstract class Creator {
public abstract factoryMethod(): Product;
public someOperation(): string {
const product = this.factoryMethod();
return `Creator: The same creator's code has just worked with ${product.operation()}`;
}
}
class ConcreteCreator1 extends Creator {
public factoryMethod(): Product {
return new ConcreteProduct1();
}
}
interface Product {
operation(): string;
}
class ConcreteProduct1 implements Product {
public operation(): string {
return '{Result of the ConcreteProduct1}';
}
}
function clientCode(creator: Creator) {
console.log('Client: I\'m not aware of the creator\'s class, but it still works.');
console.log(creator.someOperation());
}
/**
* The Application picks a creator's type depending on the configuration or
* environment.
*/
console.log('App: Launched with the ConcreteCreator1.');
clientCode(new ConcreteCreator1());
console.log('');
Facade Design Pattern (Structural)
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
export default class User {
private firstName: string
private lastName: string
private bankDetails: string | null
private age: number
private role: string
private isActive: boolean
constructor({firstName,lastName,bankDetails,age,role,isActive} : IUser){
this.firstName = firstName
this.lastName = lastName
this.bankDetails = bankDetails
this.age = age
this.role = role
this.isActive = isActive
}
getBasicInfo() {
return {
firstName: this.firstName,
lastName: this.lastName,
age : this.age,
role: this.role
}
}
activateUser() {
this.isActive = true
}
updateBankDetails(bankInfo: string | null) {
this.bankDetails= bankInfo
}
getBankDetails(){
return this.bankDetails
}
deactivateUser() {
this.isActive = false
}
}
export default interface IUser {
firstName: string
lastName: string
bankDetails: string
age: number
role: string
isActive: boolean
}
export default class UserFacade{
protected user: User
constructor(user : User){
this.user = user
}
activateUserAccount(bankInfo : string){
this.user.activateUser()
this.user.updateBankDetails(bankInfo)
return this.user.getAllDetails()
}
deactivateUserAccount(){
this.user.deactivateUser()
this.user.updateBankDetails(null)
}
}
Strategy Design Pattern (Behavioral)
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm exist very independently from clients that use it.
/**
* The Context defines the interface of interest to clients.
*/
class Context {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
public setStrategy(strategy: Strategy) {
this.strategy = strategy;
}
public doSomeBusinessLogic(): void {
console.log('Context: Sorting data using the strategy (not sure how it\'ll do it)');
const result = this.strategy.doAlgorithm(['a', 'b', 'c', 'd', 'e']);
console.log(result.join(','));
}
}
interface Strategy {
doAlgorithm(data: string[]): string[];
}
class ConcreteStrategyA implements Strategy {
public doAlgorithm(data: string[]): string[] {
return data.sort();
}
}
class ConcreteStrategyB implements Strategy {
public doAlgorithm(data: string[]): string[] {
return data.reverse();
}
}
const context = new Context(new ConcreteStrategyA());
console.log('Client: Strategy is set to normal sorting.');
context.doSomeBusinessLogic();
console.log('');
console.log('Client: Strategy is set to reverse sorting.');
context.setStrategy(new ConcreteStrategyB());
context.doSomeBusinessLogic();
Decorator Design Pattern (Structural)
Attach additional responsabilities to an object, dynamically keeping the same interface. Decorators provide a flexible alternative to subclassing for extending functionality.
Command Design Pattern (Behavioral)
Encapsulate a request as an object, thereby allowing for the paramerization of clients with different requests, and the queuing or logging the requests.
Singleton Design Pattern (Creational)
Ensure a class has only one instance, and provide a global point of access to it.