0% found this document useful (0 votes)
91 views

design-patterns-for-backend-engineers_6754f3d0

Fesc

Uploaded by

Majid Hajric
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
91 views

design-patterns-for-backend-engineers_6754f3d0

Fesc

Uploaded by

Majid Hajric
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 101

Design

Patterns
For Backend Engineers

Solomon Eseme
Design Patterns 101

Table Of Contents

What are Design Patterns? 3

What is Gangs of Four (GoF)? 6

Singleton 6

What are Factory Patterns? 9

What are Builder Patterns? 18

What are Prototype Patterns? 21

What is an Adapter Pattern? 25

What are Composite Patterns? 28

What is a Proxy Pattern 33

What is a Flyweight Pattern 37

What is a Facade Pattern? 39

What is a Bridge Design Pattern? 45

What is a DecoratorDesign Pattern? 49

Design Patterns 101


Design Patterns 101

What is an Observer Pattern? 53

What is a Strategy Pattern? 57

What is a Command Pattern? 60

What is a Chain of Responsibility Pattern? 64

What is State Pattern? 68

What is a Mediator Pattern? 73

What is an Iterator Pattern? 79

What is a Visitor Pattern? 83

What is a Template Method Pattern? 88

What is a Visitor Pattern? 92

What is a Memento Pattern? 95

Design Patterns 101


Design Patterns 101

In this book, I will explain Design Patterns, their types, the GoF design patterns, drawbacks, and
bene ts for backend engineers.

This is coming from my new Vue.js book I’m writing on “Vue Design Patterns”. However, I’m only
transferring the knowledge to backend engineers in this series.

Let’s start with the obvious questions for those who just like to know (over and over again)

What are Design Patterns?


Design patterns are reusable solutions to common problems that software developers encounter
during application design and development. They serve as templates that can be adapted to solve
speci c design challenges, offering a standardized way to build software that is maintainable,
scalable, and ef cient.

In backend development, design patterns provide a framework for structuring your code, ensuring
that it is both modular and robust. They are not about reinventing the wheel but about applying
established practices that have been proven to work in numerous scenarios.

That’s all there’s about Design Patterns. They are not frameworks, languages, or tools. They are
simply patterns or ways you could write or structure your code based on what has been proven to
work over time.

As prerequisites, as we progress in this Design Pattern series, you must know Object Oriented
Programming very well because Design Patterns seem to favor OOP more. Though the patterns are
adapted to any style of coding I will be using OOP concepts.

Also, my code snippet may be in Node.js or Java depending on which I’m comfortable with.
However, I will try to make it language-agnostic to help you adapt it to your programming
language.

With that out. Let’s start with the types of Design Patterns

Design Patterns 101


Design Patterns 101

Whenever you're ready


There are 4 ways we can help you become a great backend engineer:

1. The MB Platform:Join 1000+ backend engineers learning backend


engineering. Build real-world backend projects, learn from
expert-vetted courses and roadmaps, track your learnings and
set schedules, and solve backend engineering tasks, exercises,
and challenges. [https://app.masteringbackend.com/?
ref=designpattern_book]
2. The MB Academy: The “MB Academy” is a 6-month intensive
Advanced Backend Engineering BootCamp to produce great
backend engineers.
3. Join Backend Weekly: If you like post like this, you will absolutely
enjoy our exclusive weekly newsletter, Sharing exclusive backend
engineering resources to help you become a great Backend
Engineer. [https://newsletter.masteringbackend.com/?
ref=designpattern_book]
4. Get Backend Jobs: Find over 2,000+ Tailored International Remote
Backend Jobs or Reach 50,000+ backend engineers on the #1
Backend Engineering Job Board. [https://getbackendjobs.com/?
ref=designpattern_book]

Design Patterns 101


Design Patterns 101

Types of Design Patterns


Design patterns can be broadly classi ed into three categories:

Creational Patterns
These patterns deal with object-creation mechanisms. They abstract the instantiation process,
making the system independent of how its objects are created, composed, and represented.

There are ve well-known design patterns possible to implement in a wide scope of programming
languages:

1. Singleton
2. Factory Method
3. Abstract Factory
4. Builder
5. Prototype

Structural Patterns
These patterns concern the composition of classes or objects. They help ensure that parts of a
system can be made independent, yet work together as a cohesive whole.

1. Adapter
2. Composite
3. Proxy
4. Flyweight
5. Facade
6. Bridge
7. Decorator

Behavioral Patterns
These patterns focus on communication between objects, de ning the manner and ow of control
between them.

Design Patterns 101


Design Patterns 101

1. Observer Pattern
2. Strategy Pattern
3. Command Pattern
4. Chain of Responsibility Pattern
5. State Pattern
6. Mediator Pattern
7. Iterator Pattern
8. Visitor Pattern
9. Template Method Pattern
10. Interpreter Pattern
11. Memento Pattern

What is Gangs of Four (GoF)?


The term "Gang of Four" (GoF) refers to the four authors of the in uential book "Design Patterns:
Elements of Reusable Object-Oriented Software." The GoF categorized 23 design patterns into the
three types mentioned above. These patterns have become the cornerstone of object-oriented
design and are widely used in software development.

Now, let’s explore the rst Design Pattern that everyone should know which is under Creational
Design Pattern.

Singleton
It ensures a class has only one instance and provides a global point of access to it. The singleton
pattern restricts the initialization of a class to ensure that only one instance of the class can be
created.

The Singleton pattern is one of the simplest and most commonly used design patterns in software
development.

Here are some use cases of the singleton design patterns:

Design Patterns 101


Design Patterns 101

Database connections
Logging instances in Node.js applications.
Creating a new object
Connecting to mail servers

Let's break down a simple implementation of the Singleton pattern in JavaScript, line by line.

class Singleton {
let instance;
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}

const instance1 = new Singleton();


const instance2 = new Singleton();

console.log(instance1.value === instance2.value); // true

Below is a line-by-line explanation of the Singleton Design Pattern. First, we create a local or
private variable called instance.

1. class Singleton {:

This line de nes a new class called Singleton. In JavaScript, classes are templates for creating
objects, and they encapsulate data with methods.

2. constructor() {:

The constructor is a special method in a class that gets called when a new instance of the class is
created. It's typically used to initialize the object's properties.

Design Patterns 101


Design Patterns 101

3. if (!Singleton.instance) {:

This conditional checks whether the Singleton.instance property has already been de ned. If it
hasn't (meaning this is the rst time the class is being instantiated), the code inside the if block
will execute. This is crucial for ensuring that only one instance of the class is ever created.

4. this.value = Math.random(100);:

If the condition in the previous line is true, the value property of the instance is assigned a random
number between 0 and 1 (since Math.random() generates a oat between 0 and 1). This value will
be used to demonstrate that multiple instances of the Singleton class share the same value.

5. Singleton.instance = this;:

Here, the Singleton.instance property is assigned the current instance of the class (this). This
ensures that the next time an instance of the Singleton class is created, the previously created
instance will be returned instead of creating a new one.

7. return Singleton.instance;:

The constructor method returns the instance of the class stored in Singleton.instance. This means
that every time you create a new instance of the Singleton class, you're getting the same instance.

9. const instance1 = new Singleton();:

This line creates the rst instance of the Singleton class. Since it's the rst instance, the constructor
will execute its logic and store the instance in Singleton.instance.

10. const instance2 = new Singleton();:

Here, a second instance of the Singleton class is created. However, due to the Singleton pattern,
the constructor will return the already existing instance stored in Singleton.instance, rather than
creating a new one.

Design Patterns 101


Design Patterns 101

11. console.log(instance1.value === instance2.value); // true:

Finally, this line checks whether the value property of instance1 is equal to the value property of
instance2. Since both instance1 and instance2 reference the same Singleton instance, the values
are identical, and the comparison returns true.

Summary
Single Instance: The Singleton pattern ensures that a class has only one instance. Any
subsequent attempts to create a new instance will return the existing one.
Global Access Point: The pattern provides a global point of access to the instance, which can
be particularly useful in managing shared resources like database connections or
con guration settings.
Use Case Consideration: While the Singleton pattern is powerful, it should be used
judiciously. Overusing it can lead to tightly coupled code, making it harder to maintain and
test.

This example demonstrates how the Singleton pattern works in JavaScript, speci cally using
Node.js. Understanding this pattern is essential for backend engineers as it provides a way to
manage shared resources ef ciently and consistently across an application.

What are Factory Patterns?


The factory method is a design pattern under the creational design patterns that de nes an
interface for creating an object but lets subclasses alter the type of objects that will be created.

Here’s a real-world example:

Assuming you have an Animal class and within the Animal class you have many subclasses for
different types of Animals.

For instance, you have subclasses for Dog, Cat, etc. You can use the Factory method to create the
object of these subclasses within the Animal class.

Design Patterns 101


Design Patterns 101

Here’s a code example:

class Animal {}

class Dog extends Animal {}

class Cat extends Animal {}

class AnimalFactory {
static create(type) {
if (type === 'dog') {
return new Dog();
} else if (type === 'cat') {
return new Cat();
} else {
throw new Error("Animal not recognized.");
}
}
}

// Usageconst dog = AnimalFactory.create('dog');

const cat = AnimalFactory.create('cat');

How is the factory method useful?

The Factory Method design pattern is useful because it provides a way to delegate the
instantiation of objects to subclasses, promoting exibility and scalability in the creation of objects
without tightly coupling the client code to speci c implementations.

Here’s why it’s bene cial:

Decouples Code from Concrete Classes

Design Patterns 101


Design Patterns 101

The Factory Method allows your code to depend on abstract classes or interfaces rather than
speci c classes. This is crucial in large systems because it reduces tight coupling.

When you need to add new object types, you don't need to modify existing code, ensuring the
Open-Closed Principle (a class should be open for extension but closed for modi cation).

Example:

If you're building a logger, you might want to create different types of loggers (e.g., le logger,
console logger, cloud logger). Using a factory method, the client doesn’t need to worry about what
kind of logger is created, just that it will get a logger object.

Design Patterns 101


Design Patterns 101

class LoggerFactory {
static create(dbType) {
if (dbType === "console") {
return new ConsoleLogger();
} else if (dbType === " le") {
return new FileLogger();
} else {
throw new Error("Unsupported logger");
}
}
}

class ConsoleLogger {
log(message) {
console.log(`Console log: ${message}`);
}
}

class FileLogger {
log(message) {
// Code to log to a le
console.log(`File log: ${message}`);
}
}

// Client code
const logger = LoggerFactory.create('console');
logger.log("This is a log message.");

Why it’s useful: As you can see, the client only interacts with the LoggerFactory class.

If you decide to add a new type of logger in the future (like a cloud logger), you can do so without
the client knowing the implementation.

Design Patterns 101


Design Patterns 101

Encapsulates Object Creation


The Factory Method encapsulates the object creation process, so clients don’t need to worry about
the instantiation details. The factory hides the complexity behind the scenes and makes it easier to
switch or extend the creation logic.

Example:

In the case of database connections, the client doesn’t need to know whether it’s connecting to
MySQL, MongoDB, or Postgres. The factory method abstracts that away.

Design Patterns 101


Design Patterns 101

class DatabaseFactory {
static create(dbType) {
if (dbType === "MySQL") {
return new MySQLConnection();
} else if (dbType === "MongoDB") {
return new MongoDBConnection();
} else {
throw new Error("Unsupported database");
}
}
}

class MySQLConnection {
connect() {
console.log("Connected to MySQL.");
}
}

class MongoDBConnection {
connect() {
console.log("Connected to MongoDB.");
}
}

// Client code
const dbConnection = DatabaseFactory.create("MySQL");
dbConnection.connect();

Why it’s useful: By encapsulating the logic of how connections are created, you can easily switch
from MySQL to MongoDB without altering client code.

There are many other bene ts to using the Factory method to instantiate your classes.

Here are a few summaries. The Factory Method is useful for:

Design Patterns 101


Design Patterns 101

Decoupling object creation from its usage.


Promoting exibility and reusability.
Supporting the Open-Closed Principle.
Simplifying testing and extension.

It's especially handy when dealing with scenarios where you frequently need to instantiate
different kinds of objects based on speci c conditions or inputs.

Now, let’s move on to the Abstract Factory Method. However, if you have any questions regarding
the Factory Method just shoot me an email.

Abstract factory patterns are similar to Factory patterns and provide the same bene ts except that
they provide an interface for creating families of related or dependent objects without specifying
their concrete classes.

So in Abstract Factory Patterns, interfaces are used for creating related objects and not the
concrete classes of these objects.

However, since JavaScript does not support interfaces directly, we can use a class the group related
objects and extend the class.

Let’s take a look at Abstract Factory in action:

Assuming you want your users to create UI components depending on the mode like Dark or Light
mode.

We can do it this way:

Design Patterns 101


Design Patterns 101

class UIFactory {
createButton()
createInput()
.}

// Light theme Factoryclass LightTheme extends UIFactory {


createButton() {
return new LightThemeButton();
}

// implement other components here


}

// Dark theme Factoryclass DarkTheme extends UIFactory {


createButton() {
return new DarkThemeButton();
}
}

// Parent Button
class Button {}

// Child Button for Light theme


class LightThemeButton extends Button {
constructor() {
console.log('Light theme button created');
}
}
// Child Button for Dark theme
class DarkThemeButton extends Button {
constructor() {

Design Patterns 101


Design Patterns 101

console.log('Dark theme button created');


}
}

// Usage
const lightFactory = new LightThemeFactory();
const lightButton = lightFactory.

createButton();
const darkFactory = new DarkThemeFactory();
const darkButton = darkFactory.createButton();

You can go ahead and add the code for Inputs.

From the code snippet above, you can see that we have a class called UIFactory which is supposed
to be an Abstract class or an interface in other programming languages like Java with the
following methods to be implemented.

Next, in each of the classes for each mode, we implemented the createButton function to return
the different types of Buttons we want depending on the mode.

Next, each type of Button extends the parent Button and makes some modi cations to suit the
mode.

Lastly, we use the createButton function to create each type of button without knowing the deep
implementation that happens.

Note that:

The Abstract Factory method and Factory method give almost the same bene ts and they are all
used to create objects. However, when you have a family of related or similar objects to create,
then Abstract Factory Pattern works better.

Design Patterns 101


Design Patterns 101

For example, if you have to create the object for a Button, Input, Tooltip, Checkbox, etc for a Dark
theme and a Button, Input, Tooltip, Checkbox, etc for a Light theme or something similar use
Abstract Factory.

What are Builder Patterns?


The builder pattern separates the construction of a complex object from its representation,
allowing the same construction process to create different representations.

Here’s a real-world example:

You can use it for creating complex objects like HTTP requests or con gurations. Assuming you
have an HTTP Request class and you want to be able to chain different operations to the class
before building or creating it.

You can achieve that using the builder pattern as shown below:

class Request {
constructor() {
this.method = 'GET';
this.url = '';
this.headers = {};
this.body = null;
}
}

The code snippet above creates a Request class with a default constructor for initializing required
properties.

Design Patterns 101


Design Patterns 101

class RequestBuilder {
constructor() {
this.request = new Request();
}

setMethod(method) {
this.request.method = method;
return this;
}

setURL(url) {
this.request.url = url;
return this;
}

setHeaders(headers) {
this.request.headers = headers;
return this;
}

setBody(body) {
this.request.body = body;
return this;
}

build() {
return this.request;
}
}

In the code snippet above, we created a RequestBuilder class to allow us to add or specify the
different con gurations we need to add before we create the Request object.

Design Patterns 101


Design Patterns 101

For example, you have different methods to specify different con gurations

const request = new RequestBuilder()


.setMethod('POST')
.setURL('https://api.example.com/data')
.setHeaders({ 'Content-Type': 'application/json' })
.setBody(JSON.stringify({ data: 'example' }))
.build();

Lastly, in this code snippet, we are using the builder pattern to specify the con guration that we
want before creating the Request object using the build method.

With the builder pattern, we can optionally add con gurations or remove the ones we don’t want.

The Builder Pattern is useful in software development because it allows for the creation of
complex objects step-by-step while hiding the internal details of their construction.

Here is how the builder pattern is useful:


Simpli es Object Construction
When an object has multiple parameters, especially optional or complex ones, constructing it can
become cumbersome. The Builder Pattern simpli es this by allowing you to create objects step-by-
step, with clear method calls for each con guration option.

Without the Builder Pattern:

Design Patterns 101


Design Patterns 101

class Request {
constructor(method, url, headers, body) {
this.method = method;
this.url = url;
this.headers = headers;
this.body = body;
}
}

const request = new Request('GET', '', {}, null);

From the code snippet above, it can kill readability and maintainability if you ever want to make
some parameters optional and if some parameters are longer.

The Builder Pattern is particularly useful in the following scenarios:

When an object has multiple parameters, especially when many are optional.
When constructing objects is complex and involves multiple steps.
When you want to avoid constructor pollution with too many parameters.
When immutability or exibility in object creation is needed.
When you want more readable and maintainable code.

What are Prototype Patterns?


The prototype pattern allows you to create new objects by copying an existing object, known as
the prototype, rather than creating from scratch.

Here’s a real-world example:

Consider a scenario where creating an object requires a time-consuming process, like retrieving
data from a database or running complex computations.

In such situations, using the Prototype pattern to clone an existing object, rather than building a
new one from the ground up, can signi cantly improve ef ciency.

Design Patterns 101


Design Patterns 101

You can achieve that using the builder pattern as shown below:

class Database {
constructor(proto) {
// Complex database connections and computation
this.proto = proto;
}

// Method to clone the database class


clone() {
const clone = Object.create(this.proto);
clone.connection = this.proto.connection;
clone.username = this.proto.username;
clone.password = this.proto.password;
return clone;
}
}

// Database properties
const databaseProto = { connection: 'Postegre', username: "root", password:"passwor
d" };

//Creating the rst instance with all the complex computation


const database = new Database(databaseProto);

// Create a clone for new connection


const databaseClone = database.clone();

The code snippet above is self-explanatory with all the comments. Let me know if you need more
clari cation.

The Prototype Pattern is useful because it allows you to create new objects by copying or "cloning"
existing ones, rather than constructing them from scratch.

Design Patterns 101


Design Patterns 101

This pattern is especially bene cial when the process of creating an object is resource-intensive or
when you need to avoid the complexities of using constructors with numerous parameters.

Here's why the Prototype Pattern is useful:

Follows Open/Closed Principle


The Prototype Pattern adheres to the Open/Closed Principle because you can extend or modify
object creation without altering the existing class structure.

When you need to create new types of objects or variants, you don’t modify the core logic; instead,
you just clone and adjust.

Works Well with Dynamic and Runtime Changes


In some applications, objects may be con gured or modi ed during runtime based on user input,
environment, or external factors.

The Prototype Pattern allows you to create a base object and modify it dynamically without
needing to know the exact details of how the object was initially constructed.

Example:
A user interface (UI) system where different themes or layouts need to be applied dynamically
based on user preferences or system states.

Design Patterns 101


Design Patterns 101

class Theme {
constructor(color, font, layout) {
this.color = color;
this.font = font;
this.layout = layout;
}

clone() {
return new Theme(this.color, this.font, this.layout);
}
}

// Base themeconst defaultTheme = new Theme('blue', 'Arial', 'grid');

// Clone and customize for dark mode


const darkModeTheme = defaultTheme.clone();
darkModeTheme.color = 'dark-gray';

console.log(defaultTheme, darkModeTheme);

You can clone an existing con guration and apply modi cations without altering the original
con guration. The pattern is exible and adaptable to runtime changes.

The Prototype Pattern is particularly useful in the following scenarios:

Design Patterns 101


Design Patterns 101

Performance and ef ciency: Cloning is faster and less resource-intensive than creating
objects from scratch, especially in scenarios where the object creation process is complex.
Reduces code complexity: It simpli es object creation when many parameters or
con gurations are involved.
Supports runtime modi cations: Objects can be dynamically cloned and modi ed based on
runtime conditions.
Avoids constructor clutter: The need for complex or overloaded constructors is reduced.
Handles complex, nested objects: The pattern simpli es the copying of objects with multiple
sub-objects.

The Prototype Pattern is a powerful and exible design pattern that can enhance performance,
maintainability, and exibility in your code, particularly when dealing with objects that require
complex construction.

What is an Adapter Pattern?


The adapter pattern converts the interface of a class into another interface that a client expects. It
allows classes to work together that couldn’t otherwise because of incompatible interfaces.

The Adapter Pattern is useful because it allows incompatible interfaces to work together by
providing a way to "adapt" one interface to another.

It acts as a bridge between two components that otherwise wouldn’t be able to interact. This
pattern is especially bene cial when integrating systems or classes that have different interfaces or
when working with legacy code that you cannot modify directly.

It is used to create a new system from a legacy system as seen in the code snippet below:

Design Patterns 101


Design Patterns 101

class OldAPI {
request() {
return "Old API response";
}
}

class NewAPI {
constructor() {
this.oldAPI = new OldAPI();
}

request() {
return this.oldAPI.request();
}
}

const api = new NewAPI();console.log(api.request()); // "Old API response"

Here’s a real-world example:

Let’s assume your Payment system supports an old payment provider in your legacy system.
However, you created a new system but you still want to support your old payment system but
also create an adapter to allow you to change the payment system anytime.

Here’s how you can achieve it with Adapter Pattern:

Design Patterns 101


Design Patterns 101

// Third-party payment gateway (incompatible with our system)


class ThirdPartyPaymentGateway {
processPayment(amount) {
console.log(`Payment of ${amount} processed by third-party gateway.`);
}
}

// Our system expects a "makePayment" method


class PaymentProcessor {
constructor(paymentAdapter) {
this.paymentAdapter = paymentAdapter;
}

makePayment(amount) {
this.paymentAdapter.pay(amount);
}
}

// Adapter to make the third-party gateway compatible


class PaymentAdapter {
constructor(thirdPartyGateway) {
this.thirdPartyGateway = thirdPartyGateway;
}

pay(amount) {
this.thirdPartyGateway.

Design Patterns 101


Design Patterns 101

processPayment(amount); // Adapts the method


}
}

// Client code
const thirdPartyGateway = new ThirdPartyPaymentGateway();
const paymentAdapter = new PaymentAdapter(thirdPartyGateway);
const paymentProcessor = new PaymentProcessor(paymentAdapter);

paymentProcessor.makePayment(100);

From the code snippet above, you can easily connect or disconnect the ThirdPartyPaymentGateway
using the PaymentAdapter. Therefore, making your application adaptable to other payment
systems in the future.

Here is how the builder pattern is useful:


Solves interface incompatibility: It allows two incompatible interfaces to communicate with
each other, bridging the gap between different systems.
Promotes code reusability: You can reuse existing or legacy systems without modifying their
code.
Provides exibility: It allows you to design your system independently of speci c
dependencies and easily extend it to support new integrations.
Facilitates legacy system integration: Legacy code can be integrated into modern systems
without altering the original code.
Follows Open/Closed Principle: The Adapter Pattern makes your system more maintainable
by allowing extensions through adapters rather than modifying existing code.

The Adapter Pattern is particularly useful in scenarios where you need to integrate different
systems, work with legacy code, or handle various third-party APIs that don’t conform to the
interface expected by your application. It helps create exible, reusable, and maintainable systems
while ensuring compatibility between different components.

What are Composite Patterns?

Design Patterns 101


Design Patterns 101

The composite pattern is used to compose objects into tree structures to represent part-whole
hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

It simpli es the management of hierarchical, tree-like structures, such as menus, le systems, or


organizational charts, by allowing you to compose objects into tree structures and work with them
as if they were individual objects.

This pattern is especially useful when dealing with recursive structures, where a group of objects
(composites) can contain other objects or groups, and both individual objects and composites can
be treated in the same way.

Here’s a code snippet to demonstrate:

First, we create a FileSystemComponent le and add the add, remove, and display methods to add,
remove, and display all components respectively.

class FileSystemComponent {
add(component) {
throw new Error("Method not implemented.");
}

remove(component) {
throw new Error("Method not implemented.");
}

display(indent = '') {
throw new Error("Method not implemented.");
}
}

Next, we created a File class that extends FileSystemComponent and overrides the display
methods.

Design Patterns 101


Design Patterns 101

class File extends FileSystemComponent {


constructor(name) {
super();
this.name = name;
}

display(indent = '') {
console.log(`${indent}File: ${this.name}`);
}
}

Next, we created a Directory folder that will allow us to add, remove, and display the components
(Files) within it.

Design Patterns 101


Design Patterns 101

class Directory extends FileSystemComponent {


constructor(name) {
super();
this.name = name;
this.children = [];
}

add(component) {
this.children.push(component);
}

remove(component) {
this.children = this.children. lter(child => child !== component);
}

display(indent = '') {
console.log(`${indent}Directory: ${this.name}`);
this.children.forEach(child => child.display(indent + ' '));
}
}

Lastly, this is how it will be used inside the client code:

Design Patterns 101


Design Patterns 101

// Client code
const rootDir = new Directory('root');
const le1 = new File(' le1.txt');
const le2 = new File(' le2.txt');

const subDir = new Directory('subdir');


const le3 = new File(' le3.txt');
subDir.add( le3);

rootDir.add( le1);
rootDir.add( le2);
rootDir.add(subDir);

rootDir.display();

Here, we create different les and add them to the directory that we have created. As you can see
from the code snippets above, we can treat Files and Directories as a whole instead of separate and
individual parts.

Here is how the composite pattern is useful:


The Composite Pattern is particularly bene cial in scenarios where you need to manage tree-like or
hierarchical structures. Its main advantages include:

Handling hierarchical structures: It simpli es working with recursive structures like le


systems, UI components, and organizational charts.
Uniformity: It allows you to treat individual objects and compositions (groups of objects)
consistently, reducing code complexity.
Extensibility: New components (either individual or composite) can be added without
modifying existing code.
Simpli ed client code: Client code interacts with the structure uniformly, avoiding the need
for complex type-checking logic.
Supports recursive composition: Complex hierarchies, where composites contain other
composites, can be easily managed.

Design Patterns 101


Design Patterns 101

By using the Composite Pattern, you can build exible, scalable, and maintainable systems that
handle complex object structures with minimal effort.

What is a Proxy Pattern


The proxy pattern provides a surrogate or placeholder for another object to control access to it. For
instance, managing access to an object that is expensive to instantiate.

It allows for additional functionality when accessing an object, such as controlling access,
managing resources, lazy loading, logging, or even modifying requests or responses.

The Proxy Pattern is especially helpful when working with expensive objects to create or manage,
or when you need to add behavior before or after interactions with the original object.

Here’s a code snippet:

class RealService {
performAction() {
console.log("Action performed by the real service.");
}
}

First, we created a service called RealService, the service that is used to perform expensive actions.

However, we need a way to enforce some security or protect sensitive resources without touching
the real service. That’s where the Proxy Pattern comes in.

Design Patterns 101


Design Patterns 101

class AuthProxy {
constructor(user, service) {
this.user = user;
this.service = service;
}

performAction() {
if (this.user.hasAccess) {
this.service.performAction();
} else {
console.log("Access denied: You do not have permission to perform this actio
n.");
}
}
}

Next, we created an AuthProxy service which will allow us to enforce security or permission rules
before allowing access to the original object. Also, we can use it to protect sensitive resources from
unauthorized access without changing the underlying service.

// Client codeconst user = { name: "John", hasAccess: false };


const realService = new RealService();
const proxy = new AuthProxy(user, realService);

proxy.performAction();

Above, shows how to utilize the AuthProxy and the RealService class in your code. Here, we
instantiate both classes and pass in the instance of the RealService as part of the parameter to the
AuthProxy class.

Loading an Image with Proxy Pattern

Design Patterns 101


Design Patterns 101

Here’s another example of loading an image. If you are working with a system that loads large
images, you might want to delay the actual loading of the image until it’s needed (e.g., when the
image is displayed on the screen).

class RealImage {
constructor( lename) {
this. lename = lename;
this.loadImage();
}

loadImage() {
console.log(`Loading image from le: ${this. lename}`);
}

display() {
console.log(`Displaying image: ${this. lename}`);
}
}

The code snippet above creates an image class with two methods to load and display images
respectively.

Design Patterns 101


Design Patterns 101

class ImageProxy {
constructor( lename) {
this. lename = lename;
this.realImage = null;
}

display() {
if (!this.realImage) {
this.realImage = new RealImage(this. lename); // Load image only when need
ed
}
this.realImage.display();
}
}

Next, we created a Proxy pattern to help us display an image only when it is needed and only
when the real image is loaded.

// Client code
const image = new ImageProxy("large-image.png");
image.display(); // Image loaded and displayed only when display() is called

The code snippet above shows how to use the Proxy Pattern in our code above.

Here is how the Proxy Pattern is useful:

Design Patterns 101


Design Patterns 101

Access control: The Proxy Pattern can restrict or manage access to sensitive resources,
making it ideal for authentication, security, and permission management.
Lazy loading: It can delay the creation of expensive objects until they're needed, improving
performance and resource ef ciency.
Logging and monitoring: A proxy can transparently log or monitor method calls, enabling
auditing or debugging without changing the original object.
Remote access: It can facilitate interaction with remote services or objects, abstracting the
communication details from the client.
Resource management: Proxies can control resource usage, such as managing connection
pools or limiting access to shared resources.
Placeholder for expensive objects: The Proxy Pattern provides a lightweight substitute for
expensive objects, delaying their creation until necessary.

Overall, the Proxy Pattern provides a exible and ef cient way to manage access to resources,
optimize performance, and add additional functionality to objects without changing their
underlying implementation.

What is a Flyweight Pattern


The Flyweight Pattern is useful because it helps reduce memory consumption and improve
performance by sharing common parts of objects across multiple instances.

It is especially bene cial when dealing with large numbers of similar objects that share common
data.

The Flyweight Pattern ensures that memory usage is optimized by avoiding the creation of
duplicate data and instead reusing shared states across different instances.

Let’s look at an example:

A text editor might contain millions of characters with different fonts, styles, and sizes. By using
the Flyweight Pattern, the editor can share common glyphs and styles across characters, reducing
memory usage.

Design Patterns 101


Design Patterns 101

class FontStyle {
constructor(font, size, color) {
this.font = font;
this.size = size;
this.color = color;
}
}

In the code snippet above, we created a FontStyle class to house all the properties related to fonts.

class FontFactory {
constructor() {
this.styles = {};
}

getFontStyle(font, size, color) {


const key = `${font}-${size}-${color}`;
if (!this.styles[key]) {
this.styles[key] = new FontStyle(font, size, color);
}
return this.styles[key];
}
}

Next, we created a FontFactory class that allows the text editor to scale by sharing font styles
across multiple characters.

Design Patterns 101


Design Patterns 101

// Client codeconst factory = new FontFactory();

const style1 = factory.getFontStyle('Arial', 12, 'Black');


const style2 = factory.getFontStyle('Arial', 12, 'Black'); // Reused

console.log(style1 === style2); // true (shared object)

The last code snippet shows how to use the Flyweight Pattern in our client code.

Here is how the Flyweight Pattern is useful:


The Flyweight Pattern is highly bene cial in scenarios where you need to manage a large number
of similar objects ef ciently. The main bene ts include:

Memory optimization: It reduces memory consumption by sharing common parts of objects,


minimizing duplication.
Performance improvement: It improves performance by reducing the number of objects in
memory and reusing existing instances.
Ef cient management of large-scale systems: The pattern makes it easier to handle systems
with millions of objects, such as in 3D rendering, text processing, or large datasets.
Clear separation of state: It allows for the separation of shared (intrinsic) and unique
(extrinsic) states, making the system easier to manage and optimize.
Supports caching and reuse: The Flyweight Pattern promotes caching of shared objects,
reducing the need to create new instances repeatedly.

Overall, the Flyweight Pattern is a powerful tool for optimizing memory usage and improving the
scalability of systems that deal with large numbers of similar objects.

What is a Facade Pattern?


The Facade pattern provides a simpli ed interface to a complex subsystem, making it easier for
clients to interact with that subsystem. It hides the complexities of the underlying system by
exposing a uni ed and easy-to-use interface, reducing the learning curve for developers and the
risk of errors.

Design Patterns 101


Design Patterns 101

The Facade Pattern is particularly bene cial when dealing with large, complex systems that have
numerous interdependent classes or when integrating legacy systems.

Real-world use case:


Imagine you have a video conversion library that requires multiple steps like le parsing, codec
selection, compression, and so on.

Instead of requiring clients to call these low-level methods individually, you can create a facade
that provides a simple convert() method to handle all the complexity internally.

Design Patterns 101


Design Patterns 101

// Complex subsystemsclass VideoFile {


constructor( lename) {
this. lename = lename;
}
}

class CodecFactory {
static extract( le) {
console.log(`Extracting codec from ${ le. lename}`);
return "Codec";
}
}

class VideoCompressor {
static compress(codec) {
console.log(`Compressing video with ${codec}`);
}
}

class AudioMixer {
static mix() {
console.log("Mixing audio tracks.");
}
}

// Facadeclass VideoConverterFacade {
convert( lename) {
const le = new VideoFile( lename);
const codec = CodecFactory.extract( le);
VideoCompressor.compress(codec);
AudioMixer.mix();

Design Patterns 101


Design Patterns 101

console.log(`Video conversion complete for ${ lename}`);


}
}

// Client code
const converter = new VideoConverterFacade();
converter.convert("movie.mp4");

Looking at the code snippet above, you can see that we have independent classes handling speci c
complex processing such as CodecFactory, VideoCompressor , AudioMixer , and VideoFile .

However, we don’t want developers to have to instantiate and call each class before converting a
video each time.

Therefore, we provide the VideoConverterFacade Facade with a convert method that calls all the
classes and implements all the complexities so you don’t have to do it again but simply call the
convert method to convert a video.

Helps Integrate Legacy Systems


The Facade Pattern can act as an intermediary interface for legacy systems, providing a modern,
simpli ed interface for new clients to interact with the legacy system. This allows for gradual
refactoring or replacement of the legacy system without breaking existing functionality.

Let’s take a look at an example: If you have a legacy billing system with a complex API, a facade
can offer a simpli ed API for new clients.

Design Patterns 101


Design Patterns 101

// Legacy billing system


class LegacyBillingSystem {
calculateTotal(items) {
console.log("Calculating total cost.");
return items.reduce((total, item) => total + item.price, 0);
}

generateInvoice(total) {
console.log(`Generating invoice for ${total} USD.`);
}

sendInvoice(invoice) {
console.log(`Sending invoice: ${invoice}`);
}
}

Above is a legacy billing system that we want to support in our system. Therefore, we need to
create a facade pattern to help easily integrate or remove the legacy system.

// Facade
class BillingFacade {
constructor() {
this.billingSystem = new LegacyBillingSystem();
}

processBilling(items) {
const total = this.billingSystem.calculateTotal(items);
const invoice = this.billingSystem.generateInvoice(total);
this.billingSystem.sendInvoice(invoice);
}
}

Design Patterns 101


Design Patterns 101

The code snippet above shows a legacy billing system that we still want to support. However, we
created a Facade class to help us integrate and support the legacy billing system without having to
communicate with it every time.

// Client code
const items = [{ name: 'Item 1', price: 50 }, { name: 'Item 2', price: 30 }];

const billingFacade = new BillingFacade();

billingFacade.processBilling(items);

Here’s how the developer will use the Facade pattern to handle payment and billing everywhere in
their code.

The Facade Pattern provides a modern, easy-to-use interface (processBilling()) for interacting with
the legacy billing system. It allows for smooth integration of legacy systems with new clients
without changing the legacy code.

Why the Facade Pattern is Useful


The Facade Pattern is bene cial in the following scenarios:

Simpli es complex systems: It provides a uni ed and simpli ed interface to interact with
complex subsystems.
Reduces coupling: It decouples client code from the intricate details of subsystems, making
the system easier to modify and maintain.
Improves readability and maintenance: A single, well-de ned interface improves code
readability and simpli es maintenance.
Facilitates integration with legacy systems: It allows new clients to interact with legacy
systems using a simpli ed interface.
Provides a high-level interface: The Facade Pattern presents a high-level interface that
abstracts away the low-level details of subsystem interactions.

Design Patterns 101


Design Patterns 101

Overall, the Facade Pattern helps manage complexity, improve code quality, and provide a cleaner
API for clients to interact with complex subsystems.

Next, let’s look at the Bridge Design Pattern:

What is a Bridge Design Pattern?


The Bridge Pattern is used to decouple an abstraction from its implementation, allowing them to
vary independently.

This pattern is especially bene cial when you want to separate different aspects of a class so that
both can evolve independently, promoting exibility, scalability, and reusability.

It’s commonly used when there are multiple dimensions of variations, such as when a class could
have several implementations or operate on multiple platforms. The Bridge Pattern helps you
avoid a combinatorial explosion of classes by using composition over inheritance.

Real-world example: Drawing Shapes with Di erent Drawing APIs


Imagine a system that draws shapes like circles and rectangles using different drawing APIs (e.g., a
2D API and a 3D API).

Without the Bridge Pattern, you would need to create separate classes for every combination, such
as Circle2D, Rectangle2D, Circle3D, and Rectangle3D.

With the Bridge Pattern, you can separate the shape (abstraction) from the drawing API
(implementation), allowing both to vary independently.

Design Patterns 101


Design Patterns 101

// Implementation interface
class DrawingAPI {
drawCircle(x, y, radius, type) {
console.log("Method must be implemented.");
}
}

// Concrete implementation 1: 2D Drawing API


class DrawingAPI2D extends DrawingAPI {
drawCircle(x, y, radius){
this.drawCircle(x, y, radius, '2d')
console.log(`Drawing 2D Circle at (${x}, ${y},${radius}`);
}
}

// Concrete implementation 2: 3D Drawing API


class DrawingAPI3D extends DrawingAPI {
drawCircle(x, y, radius){
this.drawCircle(x, y, radius, '3d')
console.log(`Drawing 3D Circle at (${x}, ${y},${radius}`);
}
}

The code snippet above separates the implementation by creating the DrawingAPI with a
drawCircle method to call the external drawing API allowing the two implementations (
DrawingAPI2D and DrawingAPI3D )to inherit the drawCircle method to perform either 2D or 3D
drawings.

Design Patterns 101


Design Patterns 101

// Abstractionclass Shape {
constructor(drawingAPI) {
this.drawingAPI = drawingAPI;
}

draw(x,y,radius) {
this.drawingAPI.drawCircle(x, y, radius);
}
}

// Re ned Abstraction: Circle Shape


class Circle extends Shape {
constructor(x, y, radius, drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}

this.draw(this.x, this.y, this.radius)


}

The code snippet above shows the abstraction of different shapes that we can create using the two
implementations ( DrawingAPI2D and DrawingAPI3D ) that we have created above.

With this in place, we can create more shapes using the implementation such as Circle , Triangle,
etc simply by inheriting the Shape abstraction.

Design Patterns 101


Design Patterns 101

// Client code
const circle2D = new Circle(5, 10, 15, new DrawingAPI2D());
circle2D.draw(); // Output: Drawing 2D Circle at (5, 10, 15)

const circle3D = new Circle(20, 30, 25, new DrawingAPI3D());


circle3D.draw(); // Output: Drawing 3D Circle at (20, 30, 25)

The code snippet here shows how you will use the pattern above, you can create two varieties of
each shape that you implement. For example, here we created the 2D and 3D versions of the Circle
class.

The shape abstraction (e.g., Circle) is decoupled from the drawing implementation (DrawingAPI2D
or DrawingAPI3D). You can change the drawing API without affecting the shape class, and vice-
versa, enabling independent variation and extension.

Why the Bridge Pattern is Useful


The Bridge Pattern is particularly bene cial in scenarios where you have multiple dimensions of
variation or when you want to separate abstraction from implementation to promote exibility
and reusability. Its main advantages include:

Decouples abstraction from implementation: Promotes loose coupling, allowing


independent evolution of both parts.
Avoids combinatorial explosion: Prevents the need for numerous classes by separating
concerns and using composition over inheritance.
Supports multiple variations: Easily supports multiple implementations, platforms, or
algorithms without duplicating code.
Improves exibility and scalability: The system can be extended in various directions
without breaking existing code.
Simpli es testing and maintenance: Facilitates testing and maintenance by reducing
dependencies and providing a clear separation of concerns.

By using the Bridge Pattern, you can build systems that are more exible, scalable, and easier to
maintain, making it an essential pattern in software design.

Design Patterns 101


Design Patterns 101

What is a DecoratorDesign Pattern?


The Decorator Pattern allows you to dynamically add new behavior or responsibilities to an object
at runtime without altering its structure.

This pattern provides a exible alternative to subclassing for extending functionality. Instead of
creating numerous subclasses for each possible combination of behaviors, the Decorator Pattern
allows you to "wrap" objects with additional functionality as needed, making your code more
modular, reusable, and easy to maintain.

Real-world use case:


Suppose you want to send noti cations to users via different channels, such as email, SMS, and
push noti cations. Using the Decorator Pattern, you can create separate decorators for each
noti cation type.

// Component Interface
class Noti er {
send(message) {
console.log("Method 'send()' must be implemented.");
}
}

First, we created a Noti er class to send any noti cation to the user. You will need to implement
the send method.

Design Patterns 101


Design Patterns 101

// Abstract Decorator
class Noti erDecorator extends Noti er {
constructor(noti er) {
super();
this.noti er = noti er;
}

send(message) {
this.noti er.send(message);
}
}

Next, I created a Noti erDecorator class which extends the Noti er object with the send method to
send noti cations to the user. Now, the important thing here is that the noti cation that is sent is
dependent on the type of Noti er object that is passed to the constructor.

Design Patterns 101


Design Patterns 101

// Concrete Component

class BasicNoti er extends Noti er {


send(message) {
console.log(`Sending basic noti cation: ${message}`);
}
}

// Concrete Decorators
class EmailNoti erDecorator extends Noti erDecorator {
send(message) {
super.send(message);
console.log(`Sending email noti cation: ${message}`);
}
}

class SMSNoti erDecorator extends Noti erDecorator {


send(message) {
super.send(message);
console.log(`Sending SMS noti cation: ${message}`);
}
}

Next, I created all the different types of noti cations that I wanted to send to users. Here, I have
created BasicNoti er , EmailNoti erDecorator , and SMSNoti erDecorator . Each of these
noti cation types extends Noti erDecorator and overrides the send method.

Except the BasicNoti er which is a base noti er or default noti er which extends the Noti er class
directly.

Design Patterns 101


Design Patterns 101

// Client code
let noti er = new BasicNoti er();
noti er = new EmailNoti erDecorator(noti er);
noti er = new SMSNoti erDecorator(noti er);

noti er.send("Your order has been shipped.");

Lastly, here is how we use the decorator pattern we have created above to send different
noti cations to users. We can add as many different types of noti cations as possible without
changing the decorator.

Each decorator class (EmailNoti erDecorator, SMSNoti erDecorator) handles a speci c


responsibility, such as sending email or SMS, adhering to the Single Responsibility Principle. You
can easily add or remove noti cation channels without modifying the existing noti er classes.

Why the Decorator Pattern is Useful


The Decorator Pattern is particularly bene cial in scenarios where you want to extend the
functionality of an object dynamically without modifying its structure. Its main advantages
include:

Dynamic behavior modi cation: Allows you to add or remove behavior dynamically at
runtime.
Promotes single responsibility: Each decorator class handles a speci c behavior or
functionality.
Avoids class explosion: Reduces the number of subclasses needed for different combinations
of behaviors.
Supports Open/Closed Principle: You can extend the behavior of objects without modifying
their code.
Enables exible and reusable designs: Decorators can be combined, applied, and reused
independently, making the system modular and adaptable.

By using the Decorator Pattern, you can create exible, maintainable, and easily extensible systems
that allow you to extend functionality without modifying the core structure of objects.

Design Patterns 101


Design Patterns 101

What is an Observer Pattern?


The Observer pattern de nes a one-to-many dependency between objects so that when one object
changes state, all its dependents are noti ed and updated automatically.

The Observer Pattern is useful because it establishes a one-to-many relationship between objects,
allowing an object (the subject) to notify other dependent objects (the observers) about changes in
its state.

This pattern is particularly bene cial when you want to maintain consistency between related
objects or when changes in one object should automatically propagate to others.

As an example, in a chat application, a message broadcast system can notify multiple observers
(e.g., UI components, logging services, and noti cation systems) whenever a new message is
received. The message broadcaster does not need to know the details of each observer, promoting
loose coupling.

Design Patterns 101


Design Patterns 101

// Subject Interface
class MessageBroadcaster {
constructor() {
this.observers = [];
}

addObserver(observer) {
this.observers.push(observer);
}

removeObserver(observer) {
this.observers = this.observers. lter(obs => obs !== observer);
}

broadcastMessage(message) {
console.log(`Broadcasting message: "${message}"`);
this.observers.forEach(observer => observer.receiveMessage(message));
}
}

The above code snippet created a MessageBroadcaster class using the observer method. Within this
class, we can add, remove, and broadcast messages to different observers.

Design Patterns 101


Design Patterns 101

// Observer Interface
class ChatObserver {
receiveMessage(message) {
throw new Error("Method 'receiveMessage()' must be implemented.");
}
}

// Concrete Observers
class ChatDisplay extends ChatObserver {
receiveMessage(message) {
console.log(`ChatDisplay: New message - "${message}"`);
}
}

class Logger extends ChatObserver {


receiveMessage(message) {
console.log(`Logger: Logging message - "${message}"`);
}
}

The code snippet above creates the ChatObserver class that sends a message to all the objects that
implement it. Also, we have created two different components that we want to notify another
there’s a message namely ChatDisplay and Logger .

Design Patterns 101


Design Patterns 101

// Client code
const messageBroadcaster = new MessageBroadcaster();

const chatDisplay = new ChatDisplay();


const logger = new Logger();

messageBroadcaster.addObserver(chatDisplay);
messageBroadcaster.addObserver(logger);

messageBroadcaster.broadcastMessage("Hello, World!");

// Output: ChatDisplay: New message - "Hello, World!"


// Logger: Logging message - "Hello, World!"

The last code snippet shows how to utilize the observer pattern in your codebase. Here we created
the instance of MessageBroadcaster to add all the components that we want to be noti ed of in
every message sent or received.

As you can see, the MessageBroadcaster does not depend on the concrete classes of the observers.
It only interacts with them through the common ChatObserver interface. New observers can be
added or removed without affecting the message broadcasting logic, making the system highly
exible and maintainable.

Why the Observer Pattern is Useful


The Observer Pattern is particularly bene cial in scenarios where multiple objects need to stay in
sync or react to changes in another object. Its main advantages include:

Design Patterns 101


Design Patterns 101

Decouples subject from observers: Allows both to vary independently without tight
coupling.
Automatic state synchronization: Ensures consistent and synchronized state across multiple
components.
Loose coupling: Promotes exibility by using a common interface for interactions between
subjects and observers.
Supports event-driven programming: Facilitates event-based systems where components can
subscribe to events and react to changes.
Broadcast communication: Allows for ef cient communication between one subject and
multiple observers.

By using the Observer Pattern, you can build systems that are more exible, maintainable, and
easier to extend, making it an essential pattern for managing dependencies between objects.

What is a Strategy Pattern?


The Strategy Pattern de nes a family of algorithms, encapsulates each one, and makes them
interchangeable. Strategy lets the algorithm vary independently from the clients that use it.

This allows clients to choose the appropriate algorithm or behavior at runtime without altering the
context or client code.

The Strategy Pattern is particularly bene cial when you want to de ne multiple ways of
performing an operation and want to be able to swap out these ways dynamically.

Now let’s take a real-world example of a payment system, and consider an e-commerce application
that supports different payment methods such as credit card, PayPal, and Bitcoin.

Using the Strategy Pattern, each payment method can be implemented as a separate strategy, and
you can switch between them at runtime based on the user's choice.

First, let’s create a PaymentStrategy class that will inherited by all payment processors and
implement the payment method inside.

Design Patterns 101


Design Patterns 101

// Strategy Interface
class PaymentStrategy {
pay(amount) {
throw new Error("Method 'pay()' must be implemented.");
}
}

Next, let’s create individual payment processors such as Paypal, CreditCard, Bitcoin, etc, and extend
the PaymentStrategy class. Each of these payment processors overrides the pay method and
implements different ways of handling payments.

// Concrete Strategy: Credit Card Payment


class CreditCardPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paying $${amount} using Credit Card.`);
}
}

// Concrete Strategy: PayPal Payment


class PayPalPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paying $${amount} using PayPal.`);
}
}

// Concrete Strategy: Bitcoin Payment

class BitcoinPayment extends PaymentStrategy {


pay(amount) {
console.log(`Paying $${amount} using Bitcoin.`);
}
}

Design Patterns 101


Design Patterns 101

Next, let’s create a PaymentProcessor class to handle the switching between individual payment
processors dynamically.

// Context: Payment Processor


class PaymentProcessor {
setStrategy(strategy) {
this.strategy = strategy;
}

executeStrategy(amount) {
this.strategy.pay(amount);
}
}

Lastly, let’s utilize the Strategy Pattern in our codebase to handle payments using different
payment processors.

// Client code
const paymentProcessor = new PaymentProcessor();

paymentProcessor.setStrategy(new CreditCardPayment());
paymentProcessor.executeStrategy(100); // Output: Paying $100 using Credit Card.

paymentProcessor.setStrategy(new PayPalPayment());
paymentProcessor.executeStrategy(200); // Output: Paying $200 using PayPal.

paymentProcessor.setStrategy(new BitcoinPayment());
paymentProcessor.executeStrategy(300); // Output: Paying $300 using Bitcoin.

Here, we are just setting the payment processor we want at runtime and calling the
executeStrategy method to process each payment.

Design Patterns 101


Design Patterns 101

Each payment method is encapsulated as a separate strategy (CreditCardPayment, PayPalPayment,


BitcoinPayment), making it easy to add or change payment methods without modifying the
existing context (PaymentProcessor).

The client can choose or switch payment methods dynamically at runtime, making the system
more exible and adaptable to change.

Why the Strategy Pattern is Useful


The Strategy Pattern is particularly bene cial in scenarios where you have multiple ways of
performing a task or where behaviors need to be selected dynamically. Its main advantages
include:

Encapsulates algorithms: Each strategy encapsulates a speci c algorithm, making it


interchangeable and easy to manage.
Promotes Open/Closed Principle: You can extend the functionality of a class by adding new
strategies without modifying existing code.
Supports dynamic selection of behaviors: Strategies can be selected or switched at runtime
based on different conditions or user input.
Simpli es testing and maintenance: Each strategy can be tested independently, and changes
to one strategy do not affect others.
Improves code reusability: Strategies are reusable components that can be used in different
contexts or systems.

By using the Strategy Pattern, you can create systems that are more exible, maintainable, and
extensible, making it an essential pattern for managing complex behaviors and algorithms.

What is a Command Pattern?


The command pattern encapsulates a request as an object, thereby allowing for the
parameterization of clients with queues, requests, and operations.

The Command Pattern is useful because it encapsulates requests or actions as objects, allowing
you to parameterize methods with different requests, queue or log operations, and support
undoable operations.

Design Patterns 101


Design Patterns 101

It helps decouple the object that invokes the operation from the object that performs it. This
pattern is particularly bene cial when you need to execute, delay, undo, or store operations,
making it a powerful tool for implementing features like transaction handling, macro operations,
or task scheduling.

As an example, in a task scheduler, you might queue commands for execution at speci c times or
under certain conditions. The Command Pattern allows you to store and execute these tasks at the
right time.

// Command Interface
class Command {
execute() {
throw new Error("Method 'execute()' must be implemented.");
}
}

First, we start by creating the command interface with the execute method to be implemented.

// Receiver: Task Handler

class TaskHandler {
runTask(taskName) {
console.log(`Executing task: ${taskName}`);
}
}

Next, we create a task handler class that will run all our tasks in the background.

Design Patterns 101


Design Patterns 101

// Concrete Command: Run Task


class RunTaskCommand extends Command {
constructor(taskHandler, taskName) {
super();
this.taskHandler = taskHandler;
this.taskName = taskName;
}

execute() {
this.taskHandler.runTask(this.taskName);
}
}

Next, we created the RunTaskCommand class and extended the Command class we had created
before. This will allow us to override the execute function and perform individual tasks.

Design Patterns 101


Design Patterns 101

// Invoker: Task Scheduler


class TaskScheduler {
constructor() {
this.queue = [];
}

scheduleTask(command) {
this.queue.push(command);
}

runTasks() {
while (this.queue.length > 0) {
const command = this.queue.shift();
command.execute();
}
}
}

Next, we create the TaskScheduler class with the scheduleTask and runTasks methods which are
responsible for pushing tasks to queue and running the individual tasks using the command
pattern.

// Client code
const taskHandler = new TaskHandler();const scheduler = new TaskScheduler();

scheduler.scheduleTask(new RunTaskCommand(taskHandler, "Backup Database"));


scheduler.scheduleTask(new RunTaskCommand(taskHandler, "Send Reports"));
scheduler.scheduleTask(new RunTaskCommand(taskHandler, "Run Security Scan"));

scheduler.runTasks();

Design Patterns 101


Design Patterns 101

// Output: Executing task: Backup Database


// Executing task: Send Reports
// Executing task: Run Security Scan

Lastly, this is how you can use the command pattern in your client code to run scheduled tasks.

The Command Pattern allows you to schedule tasks for execution and maintain a queue of
commands, making it easy to delay, batch, or retry operations. It provides exibility in executing
commands at speci c times or under certain conditions, making it ideal for task-scheduling
systems.

Why the Command Pattern is Useful


The Command Pattern is particularly bene cial in scenarios where you need to decouple the sender
of a request from the object that performs it. Its main advantages include:

Decouples sender from receiver: It separates the invoker from the receiver, promoting loose
coupling and exibility.
Supports undo/redo functionality: It makes it easy to implement undoable operations by
storing commands and their states.
Enables queuing and logging: Commands can be queued, logged, or scheduled for later
execution, supporting features like task scheduling and transaction management.
Encapsulates requests: Commands encapsulate requests as objects, making it easier to
parameterize objects with different operations.
Supports macro commands: You can group multiple commands into a single macro
command, allowing for composite operations.

By using the Command Pattern, you can build systems that are more modular, exible, and easier
to extend, especially when handling complex operations or user interactions.

What is a Chain of Responsibility Pattern?


The chain of responsibility pattern allows an object to pass a request along a chain of potential
handlers until the request is handled.

Design Patterns 101


Design Patterns 101

The Chain of Responsibility Pattern is useful because it allows multiple objects to handle a request
in a exible and loosely coupled way.

Instead of sending a request to a single handler, the request is passed along a chain of handlers,
where each handler either processes the request or passes it to the next handler in the chain.

This pattern is particularly bene cial when you need to decouple the sender of a request from the
receivers, making it easier to extend, modify, or reorder the handlers without changing the client
code.

Let’s take an example, In a web application, user requests often need to pass through a series of
validation checks (e.g., authentication, authorization, input validation). Each validation check can be
represented as a handler in the chain, and new validation rules can be added without modifying
the existing system.

// Handler Interface
class RequestHandler {
setNext(handler) {
this.nextHandler = handler;
}

handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}

To handle the validation pipeline with the chain of responsibility pattern, we created a
RequestHandler class and add two methods called setNext and handle respectively.

The simply shows the implementation of a LinkedList data structure.

Design Patterns 101


Design Patterns 101

// Concrete Handlers
class AuthenticationHandler extends RequestHandler {
handle(request) {
if (request.isAuthenticated) {
console.log("Authentication passed.");
return super.handle(request);
} else {
console.log("Authentication failed.");
}
}
}

class AuthorizationHandler extends RequestHandler {


handle(request) {
if (request.isAuthorized) {
console.log("Authorization passed.");
return super.handle(request);
} else {
console.log("Authorization failed.");
}
}
}

class InputValidationHandler extends RequestHandler {


handle(request) {
if (request.isValidInput) {
console.log("Input validation passed.");
return super.handle(request);
} else {
console.log("Input validation failed.");
}
}
}

Design Patterns 101


Design Patterns 101

Next, we created the three validation pipelines that we need, you can add more pipelines as you
need but in our example, these three are great. Each pipeline handles a speci c type of validation
and responds to the result accordingly.

// Client code
const authHandler = new AuthenticationHandler();
const authzHandler = new AuthorizationHandler();
const validationHandler = new InputValidationHandler();

authHandler.setNext(authzHandler);
authzHandler.setNext(validationHandler);

const request = { isAuthenticated: true, isAuthorized: true, isValidInput: true };

authHandler.handle(request); // Output: Authentication passed. Authorization passe


d. Input validation passed.

Lastly, the code snippet above shows how the chain of responsibility pattern will be used in your
code.

We instantiated the three pipelines and used the setNext method to add the pipeline to be
processed after each one.

Next, we sent a request that requires all the pipelines to be processed before passing the result.

Lastly, we use the handle method to handle all the validations that we speci ed.

The Chain of Responsibility Pattern makes the validation process easier to manage and extend. You
can add new validation steps (e.g., rate limiting, and input sanitization) without changing the
existing handlers. Each handler has a single responsibility, making the system more maintainable.

Why the Chain of Responsibility Pattern is Useful

Design Patterns 101


Design Patterns 101

The Chain of Responsibility Pattern is particularly bene cial when you need to decouple the sender
of a request from the receiver and when multiple handlers might need to process the request. Its
main advantages include:

Decouples sender from receiver: The sender doesn’t need to know which handler will
process the request, promoting loose coupling.
Provides exibility: The chain of handlers can be dynamically con gured, reordered, or
extended without affecting the client code.
Enhances maintainability and extensibility: Each handler focuses on a speci c responsibility,
making the system more maintainable and easier to extend.
Handles different requests in different ways: Different types of requests can be processed by
different handlers, ensuring that each request is handled appropriately.
Supports dynamic changes: You can modify the chain of handlers at runtime, providing
exibility in how requests are processed.

By using the Chain of Responsibility Pattern, you can build systems that are exible, maintainable,
and easy to extend, especially when multiple objects may need to process a request in different
ways.

What is State Pattern?


The state pattern allows an object to alter its behavior when its internal state changes. The object
will appear to change its class.

This pattern is particularly bene cial when you have an object that can be in different states and
its behavior changes depending on its current state.

The State Pattern promotes exibility and maintainability by encapsulating state-speci c behavior
into separate classes, avoiding complex conditional statements, and adhering to the Open/Closed
Principle.

Design Patterns 101


Design Patterns 101

Let's take an example to help us understand better, consider a document work ow system where a
document can be in different states: draft, submitted for review, and approved. Using the State
Pattern, each state’s behavior (e.g., editing, submitting, or approving) is encapsulated in its own
class.

Let’s start with creating the document state in a class called DocumentState . Here we have all the
states that we want such as edit, submit, and approve. Make sure to implement these methods.

// State Interface
class DocumentState {
edit() {
throw new Error("Method 'edit()' must be implemented.");
}

submit() {
throw new Error("Method 'submit()' must be implemented.");
}

approve() {
throw new Error("Method 'approve()' must be implemented.");
}
}

Next, we have to create a concrete state for all the actions we want like Draft, Submitted, and
Approved. These states implement the DocumentState and implement all the methods individually.

Design Patterns 101


Design Patterns 101

// Concrete State: Draft


class DraftState extends DocumentState {
constructor(document) {
super();
this.document = document;
}

edit() {
console.log("Editing document in draft state.");
}

submit() {
console.log("Document submitted for review.");
this.document.setState(this.document.getSubmittedState());
}

approve() {
console.log("Cannot approve document in draft state.");
}
}

// Concrete State: Submitted Her


// Concrete State: Approved Here

Next, we create our document concrete le and all the methods we need to manipulate a
document such as setState, edit, getDraftState, etc. The constructor instantiates all the different
states that we can represent our document.

Design Patterns 101


Design Patterns 101

/ Context: Document
class Document {
constructor() {
this.draftState = new DraftState(this);
this.submittedState = new SubmittedState(this);
this.approvedState = new ApprovedState();
this.state = this.draftState;
}

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

getDraftState() {
return this.draftState;
}

getSubmittedState() {
return this.submittedState;
}

getApprovedState() {
return this.approvedState;
}

edit() {
this.state.edit();
}

submit() {
this.state.

Design Patterns 101


Design Patterns 101

submit();
}

approve() {
this.state.approve();
}
}

Lastly, here’s in our client code, we instantiated the Document object and called the different states
that we need to perform on the document.

// Client code
const document = new Document();

document.edit(); // Output: Editing document in draft state.


document.submit(); // Output: Document submitted for review.
document.edit(); // Output: Cannot edit document in submitted state.
document.approve(); // Output: Document approved.
document.submit(); // Output: Cannot submit approved document.

The State Pattern eliminates the need for complex conditional logic in the Document class by
delegating state-speci c behavior to individual state classes. Each state class handles behavior for
that speci c state, making the system easier to maintain and modify.

Why the State Pattern is Useful


The State Pattern is particularly bene cial in scenarios where an object’s behavior changes
depending on its internal state. Its main advantages include:

Design Patterns 101


Design Patterns 101

Encapsulates state-speci c behavior: Each state’s behavior is encapsulated in its own class,
making the system easier to manage and extend.
Eliminates complex conditional logic: The pattern replaces conditional statements with
polymorphism, where each state class handles its own behavior.
Improves maintainability and extensibility: New states can be added easily without
modifying the existing system, adhering to the Open/Closed Principle.
Supports state transitions: Each state knows how to transition to other states, simplifying
the management of state transitions.
Enhances exibility: You can easily modify, add, or remove states without affecting the rest
of the system.

By using the State Pattern, you can build systems that are more exible, maintainable, and easier
to extend, especially when dealing with objects that have multiple, changing states.

What is a Mediator Pattern?


The mediator pattern de nes an object that encapsulates how objects interact. It promotes loose
coupling by keeping objects from referring to each other explicitly.

The Mediator Pattern is useful because it centralizes and manages interactions between multiple
objects, promoting loose coupling and reducing dependencies among them. Instead of objects
communicating directly with each other, they interact through a mediator, which coordinates their
interactions.

This pattern is particularly bene cial when you have many interconnected objects whose
interactions need to be simpli ed. By centralizing the control logic, the Mediator Pattern enhances
modularity, maintainability, and exibility.

Let’s take a look at an example, in an e-commerce system, multiple components like inventory,
billing, and shipping might need to communicate during order processing. With a Mediator, you
can add or modify these components without altering existing code.

Design Patterns 101


Design Patterns 101

// Mediator Interface
class OrderMediator {
notify(sender, event) {
throw new Error("Method 'notify()' must be implemented.");
}
}

First, we created a mediator interface called OrderMediator with a notify method inside for
notifying users when they make an order.

Design Patterns 101


Design Patterns 101

// Concrete Mediator: Order Processor


class OrderProcessor extends OrderMediator {
constructor() {
super();
this.inventory = null;
this.billing = null;
this.shipping = null;
}

registerInventory(inventory) {
this.inventory = inventory;
}

registerBilling(billing) {
this.billing = billing;
}

registerShipping(shipping) {
this.shipping = shipping;
}

notify(sender, event) {
if (event === "orderPlaced") {
this.inventory.checkStock();
this.billing.processPayment();
this.shipping.arrangeShipping();
}
}
}

Next, we created a concrete class of the mediator pattern that extends the OrderMediator class and
override the notify method.

Design Patterns 101


Design Patterns 101

Inside this class, we registered different methods to perform the activities that we wanted such as
registerInventory, registerBilling, and registerShipping.

The notify method is called to send out different noti cations once a user places an order.

Design Patterns 101


Design Patterns 101

// Colleague: Inventory
class Inventory {
setMediator(mediator) {
this.mediator = mediator;
}

checkStock() {
console.log("Checking stock.");
this.mediator.notify(this, "stockChecked");
}
}

// Colleague: Billingclass Billing {


setMediator(mediator) {
this.mediator = mediator;
}

processPayment() {
console.log("Processing payment.");
this.mediator.notify(this, "paymentProcessed");
}
}

// Colleague: Shippingclass Shipping {


setMediator(mediator) {
this.mediator = mediator;
}

arrangeShipping() {
console.log("Arranging shipping.");
this.mediator.

Design Patterns 101


Design Patterns 101

notify(this, "shippingArranged");
}
}

The code snippet above creates all the activities we want to happen when a user places an order
such as Inventory, Billing, and Shipping.

// Client code
const orderProcessor = new OrderProcessor();

const inventory = new Inventory();


const billing = new Billing();
const shipping = new Shipping();

orderProcessor.registerInventory(inventory);
orderProcessor.registerBilling(billing);
orderProcessor.registerShipping(shipping);

inventory.setMediator(orderProcessor);
billing.setMediator(orderProcessor);
shipping.setMediator(orderProcessor);

orderProcessor.notify(null, "orderPlaced"); // Output: Checking stock... // Processing


payment... // Arranging shipping...

Lastly, here’s how you will implement the mediator pattern in your client code. Here, we registered
all the activities and set the mediator for each activity respectively.

Adding new components (e.g., noti cations) or modifying existing ones (like shipping rules) only
requires changes in the OrderProcessor mediator. The pattern makes the system more exible and
extensible, allowing it to handle complex processes without extensive modi cations.

Design Patterns 101


Design Patterns 101

Why the Mediator Pattern is Useful


The Mediator Pattern is particularly bene cial in systems with multiple interacting objects. Its main
advantages include:

Reduces coupling: Components interact only with the mediator, reducing dependencies and
simplifying relationships.
Simpli es many-to-many relationships: Centralizes interactions, making the system easier to
maintain.
Promotes the Single Responsibility Principle: Each component focuses on its primary role,
while the mediator manages interactions.
Supports extensibility: New components or interactions can be added by modifying the
mediator, without changing existing components.
Facilitates reuse of components: Components are independent of each other and can be
reused in other contexts.

By using the Mediator Pattern, you can build systems that are more modular, maintainable, and
easier to extend, especially when dealing with complex component interactions.

What is an Iterator Pattern?


The iterator pattern provides a way to access the elements of an aggregate object sequentially
without exposing its underlying representation.

The Iterator Pattern is useful because it provides a standardized way to access elements in a
collection sequentially without exposing the underlying structure of the collection.

This pattern decouples the traversal logic from the collection, making it easier to add new types of
collections or modify existing ones without changing the code that interacts with them.

It also supports various ways of traversing collections (e.g., forward, backward, ltering). The
Iterator Pattern is particularly bene cial when working with complex or varied data structures
where different traversal strategies might be needed.

Design Patterns 101


Design Patterns 101

In a music application, there might be different types of playlists, such as albums, artist-based
playlists, or custom playlists. The Iterator Pattern allows users to navigate these playlists
consistently, even if they’re implemented differently.

// Iterator Interface
class Iterator {
next() {
throw new Error("Method 'next()' must be implemented.");
}

hasNext() {
throw new Error("Method 'hasNext()' must be implemented.");
}
}

The code snippet above creates an Iterator class with a next and hasNext method to be
implemented.

Design Patterns 101


Design Patterns 101

// Concrete Iterator
class PlaylistIterator extends Iterator {
constructor(playlist) {
super();
this.playlist = playlist;
this.index = 0;
}

next() {
return this.playlist[this.index++];
}

hasNext() {
return this.index < this.playlist.length;
}
}

Next, we created a PlaylistIterator class that extends and overrides the methods of the Iterator
class.

Design Patterns 101


Design Patterns 101

// Collection Interface
class Playlist {
createIterator() {
throw new Error("Method 'createIterator()' must be implemented.");
}}

// Concrete Collection
class SongPlaylist extends Playlist {
constructor(songs) {
super();
this.songs = songs;
}

createIterator() {
return new PlaylistIterator(this.songs);
}
}

Next, we created a Playlist and SongPlaylist classes which shows how songs are added to the
playlist and an iterator that plays each song sequentially.

// Client codec
onst playlist = new SongPlaylist(["Song A", "Song B", "Song C"]);
const iterator = playlist.createIterator();

while (iterator.hasNext()) {
console.log(iterator.next());
}
// Output:// Song A// Song B// Song C

Lastly, this is how the iterator pattern is implemented in our client’s code. First, we added songs to
the playlist using the SongPlaylist class and use the createIterator method to iterate through each
of the songs.

Design Patterns 101


Design Patterns 101

The PlaylistIterator provides a standardized way to traverse the SongPlaylist, hiding the collection’s
internal details. Client code can traverse any playlist in the same way, regardless of the speci c
collection structure, promoting consistency.

Why the Iterator Pattern is Useful


The Iterator Pattern is particularly bene cial in scenarios where you need a exible and
standardized way to traverse different types of collections. Its main advantages include:

Standardizes access to collections: Provides a consistent way to traverse collections,


regardless of internal structure.
Promotes the Single Responsibility Principle: Separates traversal from data storage,
enhancing modularity.
Supports different traversal strategies: Allows multiple ways of traversing collections
without modifying them.
Enhances exibility and extensibility: New traversal methods can be added easily by creating
new iterators.
Supports multiple simultaneous iterations: Enables concurrent, independent iterations over
the same collection.

By using the Iterator Pattern, you can build systems that are more modular, exible, and
maintainable, especially when working with complex data structures that require various traversal
strategies.

What is a Visitor Pattern?


The visitor’s pattern allows you to add further operations to objects without having to modify
them.

The Visitor Pattern is useful because it allows you to de ne new operations on a set of objects
without modifying their classes.

This pattern is particularly bene cial when you need to perform multiple unrelated operations
across a collection of objects that have a shared interface or hierarchy.

Design Patterns 101


Design Patterns 101

Let’s take a look at this example, imagine a le system where different types of les (e.g., text les,
image les) may need various operations like compression, encryption, or analysis. The Visitor
Pattern allows you to create these operations as separate visitors, applying them to les without
modifying the le classes.

Design Patterns 101


Design Patterns 101

// Element Interface
class File {
accept(visitor) {
throw new Error("Method 'accept()' must be implemented.");
}
}

// Concrete Element: TextFile


class TextFile extends File {
constructor(name) {
super();
this.name = name;
}

accept(visitor) {
visitor.visitTextFile(this);
}
}

// Concrete Element: ImageFileclass ImageFile extends File {


constructor(name) {
super();
this.name = name;
}

accept(visitor) {
visitor.visitImageFile(this);
}
}

The code snippet above creates the le class and the child le objects such as the TextFile and
ImageFile.

Design Patterns 101


Design Patterns 101

// Visitor Interface
class Visitor {
visitTextFile( le) {
throw new Error("Method 'visitTextFile()' must be implemented.");
}

visitImageFile( le) {
throw new Error("Method 'visitImageFile()' must be implemented.");
}
}

// Concrete Visitor: Compression


class CompressionVisitor extends Visitor {
visitTextFile( le) {
console.log(`Compressing text le: ${ le.name}`);
}

visitImageFile( le) {
console.log(`Compressing image le: ${ le.name}`);
}
}

// Concrete Visitor: Encryption


class EncryptionVisitor extends Visitor {
visitTextFile( le) {
console.log(`Encrypting text le: ${ le.name}`);
}

visitImageFile( le) {
console.log(`Encrypting image le: ${ le.name}`);
}

Design Patterns 101


Design Patterns 101

Next, we created the visitor’s pattern, here, we created the compression and encryption visitors that
will be used on each type of le.

// Client code
const les = [new TextFile("document.txt"), new ImageFile("photo.jpg")];
const compressionVisitor = new CompressionVisitor();
const encryptionVisitor = new EncryptionVisitor();

les.forEach( le => le.accept(compressionVisitor));


// Output:// Compressing text le: document.txt// Compressing image le: photo.jp
g

les.forEach( le => le.accept(encryptionVisitor));


// Output:// Encrypting text le: document.txt// Encrypting image le: photo.jpg

The code snippet shows how to use the visitor’s pattern in your codebase. Here, we used the
different visitors we have created to perform actions with the le.

You can add new operations (CompressionVisitor, EncryptionVisitor) without modifying the TextFile
or ImageFile classes, preserving the existing class design. Each visitor represents a speci c
operation, making the system more modular and allowing the operations to vary independently.

Why the Visitor Pattern is Useful


The Visitor Pattern is particularly bene cial when you need to perform multiple operations across a
set of related classes. Its main advantages include:

Design Patterns 101


Design Patterns 101

Adds functionality without modifying existing classes: New operations can be added by
creating new visitors, keeping the original classes unchanged.
Centralizes operations on related classes: Grouping operations in visitors makes the code
more organized and modular.
Simpli es adding new operations: New visitors can be created independently, promoting
extensibility.
Supports complex structures: The pattern is well-suited for composite structures or object
hierarchies where you need to perform operations across different types of elements.
Promotes Open/Closed Principle: Classes remain closed for modi cation but open for
extension, enhancing system stability and maintainability.

By using the Visitor Pattern, you can build systems that are more exible, modular, and easier to
extend, especially when working with complex object structures that require multiple, varied
operations.

What is a Template Method Pattern?


The template method de nes the skeleton of an algorithm, with one or more steps being deferred
to subclasses. It lets subclasses rede ne certain steps of an algorithm without changing the
algorithm's structure.

This pattern is particularly bene cial when you have multiple variations of an algorithm with
shared behavior.

By de ning the invariant parts of the algorithm in a superclass and delegating customizable steps
to subclasses, the Template Method Pattern promotes code reuse, consistency, and adherence to the
Open/Closed Principle.

Let’s explore this example, and consider a data processing pipeline where multiple types of data
need to be loaded, processed, and saved. Using the Template Method Pattern, the base class can
de ne the skeleton of the pipeline while subclasses implement data-speci c steps.

Design Patterns 101


Design Patterns 101

// Abstract Base Class


class DataProcessor {
processData() {
this.loadData();
this.processData();
this.saveData();
}

loadData() {
throw new Error("Method 'loadData()' must be implemented.");
}

processData() {
throw new Error("Method 'processData()' must be implemented.");
}

saveData() {
console.log("Saving processed data...");
}
}

The code snippet above creates the base class for data processing, within it, we have loadData,
processData and saveData methods.

Design Patterns 101


Design Patterns 101

// Concrete Class: CSV Data Processor


class CSVDataProcessor extends DataProcessor {
loadData() {
console.log("Loading CSV data...");
}

processData() {
console.log("Processing CSV data...");
}
}

// Concrete Class: JSON Data Processor


class JSONDataProcessor extends DataProcessor {
loadData() {
console.log("Loading JSON data...");
}

processData() {
console.log("Processing JSON data...");
}
}

Next, we created the concrete types of les we wanted to process and extend the base data
processor class.

Design Patterns 101


Design Patterns 101

// Client code
const csvProcessor = new CSVDataProcessor();
csvProcessor.processData();

// Output:
// Loading CSV data...
// Processing CSV data...
// Saving processed data...

const jsonProcessor = new JSONDataProcessor();


jsonProcessor.processData();

// Output:
// Loading JSON data...
// Processing JSON data...
// Saving processed data...

Lastly, in our client code, we instantiate the type of data classes and call the processData method
from each instance to process the speci c data.

The skeleton of the algorithm (load, process, save) is encapsulated in the base class
(DataProcessor), ensuring consistency across different types of data processors. Subclasses
(CSVDataProcessor, JSONDataProcessor) implement only the speci c steps needed, avoiding code
duplication.

Why the Template Method Pattern is Useful


The Template Method Pattern is particularly bene cial in scenarios where multiple variations of an
algorithm share common steps but differ in speci c details. Its main advantages include:

Design Patterns 101


Design Patterns 101

Encapsulates invariant parts of an algorithm: Keeps the algorithm’s structure consistent


while allowing for customizable steps.
Promotes code reuse and reduces duplication: Shared behavior is centralized, minimizing
code duplication across subclasses.
Allows subclasses to vary speci c steps: Supports variations without modifying the base
algorithm, adhering to the Open/Closed Principle.
Enhances readability and understandability: Clearly separates the high-level structure of the
algorithm from the details, improving readability.
Simpli es maintenance and extension: Centralizing shared logic makes the codebase easier
to update, extending shared behavior across all subclasses.

By using the Template Method Pattern, you can build systems that are more modular, maintainable,
and consistent, especially when dealing with algorithms that have multiple variations with
common steps.

What is a Visitor Pattern?


The Interpreter Pattern is useful because it allows you to de ne the grammar of a language and
interpret sentences in that language by implementing an interpreter. This pattern is particularly
bene cial when you need to process structured, domain-speci c languages or symbolic
expressions.

By creating classes that represent each rule in the grammar, the Interpreter Pattern enables parsing,
evaluating, and executing sentences within the language, making it ideal for tasks like
mathematical expression evaluation, rule-based logic systems, and programming language
compilers.

Let’s take a look at an example, in an access control system, you might have rules that de ne
whether a user has access based on their roles or permissions. The Interpreter Pattern allows you
to evaluate these rules dynamically and adjust access based on different conditions.

Design Patterns 101


Design Patterns 101

// Expression Interface
class PermissionExpression {
interpret(context) {
throw new Error("Method 'interpret()' must be implemented.");
}
}

// Role Expression
class RoleExpression extends PermissionExpression {
constructor(role) {
super();
this.role = role;
}

interpret(context) {
return context.roles.includes(this.role);
}
}

// And Expression for Permissions


class AndExpression extends PermissionExpression {
constructor(left, right) {
super();
this.left = left;
this.right = right;
}

interpret(context) {
return this.left.interpret(context) && this.right.

Design Patterns 101


Design Patterns 101

interpret(context);
}
}

// Or Expression for Permissions


class OrExpression extends PermissionExpression {
constructor(left, right) {
super();
this.left = left;
this.right = right;

interpret(context) {
return this.left.interpret(context) || this.right.interpret(context);

}
}

// Client code
const context = { roles: ["Admin", "Editor"] };
const adminAccess = new RoleExpression("Admin");
const editorAccess = new RoleExpression("Editor");
const andAccess = new AndExpression(adminAccess, editorAccess);

console.log(andAccess.interpret(context)); // Output: true

The rule-based logic is encapsulated in individual classes (RoleExpression, AndExpression,


OrExpression), making it easy to adjust and evaluate complex access rules. You can add more roles
or permissions without modifying the existing rule structure, enhancing exibility.

Design Patterns 101


Design Patterns 101

Why the Interpreter Pattern is Useful


The Interpreter Pattern is particularly bene cial for scenarios where you need to parse and evaluate
expressions in a de ned grammar. Its main advantages include:

Simpli es the implementation of DSLs: Provides a structured way to de ne and interpret


domain-speci c languages.
Makes complex grammar easy to represent and modify: Allows you to encapsulate each
grammar rule in a separate class, promoting modularity.
Enables exible parsing and evaluation: Facilitates dynamic parsing and evaluation, making
it adaptable to user input or custom expressions.
Supports rule-based logic systems: Ideal for systems that rely on evaluating rule-based
expressions.
Promotes reusability and extensibility: New expressions can be added without altering
existing classes, making the language extensible.

By using the Interpreter Pattern, you can create systems that are more modular, maintainable, and
adaptable, especially when working with structured expressions or DSLs.

What is a Memento Pattern?


The Memento Pattern is a behavioral design pattern that is particularly useful when you want to
capture and restore the state of an object without exposing its internal details. This pattern is
commonly used to implement undo and redo functionalities in applications, allowing you to roll
back to a previous state when needed.

Why the Memento Pattern is Useful

Design Patterns 101


Design Patterns 101

1. State Restoration: The Memento pattern allows saving an object’s state at a particular
moment so that it can be restored later. This is very useful in applications such as text
editors, games, or complex simulations where users may need to undo/redo changes.
2. Encapsulation of State: It encapsulates the state of an object in a way that prevents external
objects from accessing the internal details of the object whose state is being saved. This
encapsulation maintains the principles of object-oriented design by preserving the integrity
of the object being restored.
3. Simpli es Complexity for Clients: Clients can store or restore states without being concerned
about the internal workings of the objects. The object’s state can be rolled back without
direct interference, which simpli es handling complex object structures and work ows.
4. Non-Intrusive to Business Logic: The Memento pattern separates state-saving functionality
from the primary business logic of the object. This keeps the logic clean and focused, while
the caretaker class (which requests saving/restoring state) manages state transitions as
needed.

Here an example, where the Memento pattern is used to save and restore states, providing an
intuitive way to manage and revert object states as needed without directly exposing or modifying
their internals.

Design Patterns 101


Design Patterns 101

// Memento Class
class Memento {
constructor(state) {
this.state = state;
}
getState() {
return this.state;
}
}

// Originator Class
class Originator {
constructor() {
this.state = '';
}
setState(state) {
this.state = state;
}
saveStateToMemento() {
return new Memento(this.state);
}
restoreStateFromMemento(memento) {
this.state = memento.getState();
}
}

// Caretaker Class
class Caretaker {
constructor() {
this.mementoList = [];
}
add(memento) {
this.mementoList.

Design Patterns 101


Design Patterns 101

push(memento);
}
get(index) {
return this.

mementoList[index];
}
}

// Usage
const originator = new Originator();
const caretaker = new Caretaker();
originator.setState('State #1');
caretaker.add(originator.saveStateToMemento());
originator.setState('State #2');
caretaker.add(originator.

saveStateToMemento());

originator.setState('State #3');
console.log('Current State:', originator.state);

// Restore to previous state


originator.restoreStateFromMemento(caretaker.get(0));
console.log('Restored State:', originator.state);

Design Patterns 101


Design Patterns 101

Other Examples and Use Cases:


Undo/Redo Functionality in Text Editors: In a text editor, whenever a user makes changes,
the state can be stored as a memento. When the user clicks "Undo," the previous state can be
restored.
State Restoration in Games: When a player reaches a checkpoint, the game's state is saved. If
the player fails later, the game can revert to that checkpoint.
Transactional Systems: In applications where changes need to be committed or rolled back
(e.g., banking, booking systems), the Memento pattern can be used to save a state before a
transaction and revert it if the transaction fails.

Design Patterns 101


Learn more
backend
engineering at

backendweekly.dev

You might also like