design-patterns-for-backend-engineers_6754f3d0
design-patterns-for-backend-engineers_6754f3d0
Patterns
For Backend Engineers
Solomon Eseme
Design Patterns 101
Table Of Contents
Singleton 6
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)
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
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.
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
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.
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;
}
}
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.
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.
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.
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.
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.
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.
class 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.");
}
}
}
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.
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.
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.
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.
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.
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.
Assuming you want your users to create UI components depending on the mode like Dark or Light
mode.
class UIFactory {
createButton()
createInput()
.}
// Parent Button
class Button {}
// Usage
const lightFactory = new LightThemeFactory();
const lightButton = lightFactory.
createButton();
const darkFactory = new DarkThemeFactory();
const darkButton = darkFactory.createButton();
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.
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.
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.
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.
For example, you have different methods to specify different con gurations
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.
class Request {
constructor(method, url, headers, body) {
this.method = method;
this.url = url;
this.headers = headers;
this.body = body;
}
}
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.
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.
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.
You can achieve that using the builder pattern as shown below:
class Database {
constructor(proto) {
// Complex database connections and computation
this.proto = proto;
}
// Database properties
const databaseProto = { connection: 'Postegre', username: "root", password:"passwor
d" };
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.
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.
When you need to create new types of objects or variants, you don’t modify the core logic; instead,
you just clone and adjust.
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.
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);
}
}
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.
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.
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:
class OldAPI {
request() {
return "Old API response";
}
}
class NewAPI {
constructor() {
this.oldAPI = new OldAPI();
}
request() {
return this.oldAPI.request();
}
}
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.
makePayment(amount) {
this.paymentAdapter.pay(amount);
}
}
pay(amount) {
this.thirdPartyGateway.
// 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.
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.
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.
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.
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.
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.
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 + ' '));
}
}
// Client code
const rootDir = new Directory('root');
const le1 = new File(' le1.txt');
const le2 = new File(' le2.txt');
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.
By using the Composite Pattern, you can build exible, scalable, and maintainable systems that
handle complex object structures with minimal effort.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 = {};
}
Next, we created a FontFactory class that allows the text editor to scale by sharing font styles
across multiple characters.
The last code snippet shows how to use the Flyweight Pattern in our client code.
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.
The Facade Pattern is particularly bene cial when dealing with large, complex systems that have
numerous interdependent classes or when integrating legacy systems.
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.
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();
// 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.
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.
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);
}
}
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 }];
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.
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.
Overall, the Facade Pattern helps manage complexity, improve code quality, and provide a cleaner
API for clients to interact with complex subsystems.
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.
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.
// Implementation interface
class DrawingAPI {
drawCircle(x, y, radius, type) {
console.log("Method must be implemented.");
}
}
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.
// Abstractionclass Shape {
constructor(drawingAPI) {
this.drawingAPI = drawingAPI;
}
draw(x,y,radius) {
this.drawingAPI.drawCircle(x, y, 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.
// Client code
const circle2D = new Circle(5, 10, 15, new DrawingAPI2D());
circle2D.draw(); // Output: Drawing 2D Circle at (5, 10, 15)
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.
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.
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.
// 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.
// 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.
// Concrete Component
// Concrete Decorators
class EmailNoti erDecorator extends Noti erDecorator {
send(message) {
super.send(message);
console.log(`Sending email 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.
// Client code
let noti er = new BasicNoti er();
noti er = new EmailNoti erDecorator(noti er);
noti er = new SMSNoti erDecorator(noti er);
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.
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.
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.
// 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.
// 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}"`);
}
}
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 .
// Client code
const messageBroadcaster = new MessageBroadcaster();
messageBroadcaster.addObserver(chatDisplay);
messageBroadcaster.addObserver(logger);
messageBroadcaster.broadcastMessage("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.
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.
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.
// 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.
Next, let’s create a PaymentProcessor class to handle the switching between individual payment
processors dynamically.
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.
The client can choose or switch payment methods dynamically at runtime, making the system
more exible and adaptable to change.
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.
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.
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.
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.
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.
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.runTasks();
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.
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.
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.
// Concrete Handlers
class AuthenticationHandler extends RequestHandler {
handle(request) {
if (request.isAuthenticated) {
console.log("Authentication passed.");
return super.handle(request);
} else {
console.log("Authentication failed.");
}
}
}
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);
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.
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.
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.
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.
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.");
}
}
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.
/ 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.
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();
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.
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.
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.
// 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.
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.
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.
// Colleague: Inventory
class Inventory {
setMediator(mediator) {
this.mediator = mediator;
}
checkStock() {
console.log("Checking stock.");
this.mediator.notify(this, "stockChecked");
}
}
processPayment() {
console.log("Processing payment.");
this.mediator.notify(this, "paymentProcessed");
}
}
arrangeShipping() {
console.log("Arranging shipping.");
this.mediator.
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();
orderProcessor.registerInventory(inventory);
orderProcessor.registerBilling(billing);
orderProcessor.registerShipping(shipping);
inventory.setMediator(orderProcessor);
billing.setMediator(orderProcessor);
shipping.setMediator(orderProcessor);
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.
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.
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.
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.
// 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.
// 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.
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.
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.
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.
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.
// Element Interface
class File {
accept(visitor) {
throw new Error("Method 'accept()' must be implemented.");
}
}
accept(visitor) {
visitor.visitTextFile(this);
}
}
accept(visitor) {
visitor.visitImageFile(this);
}
}
The code snippet above creates the le class and the child le objects such as the TextFile and
ImageFile.
// Visitor Interface
class Visitor {
visitTextFile( le) {
throw new Error("Method 'visitTextFile()' must be implemented.");
}
visitImageFile( le) {
throw new Error("Method 'visitImageFile()' must be implemented.");
}
}
visitImageFile( le) {
console.log(`Compressing image le: ${ le.name}`);
}
}
visitImageFile( le) {
console.log(`Encrypting image le: ${ le.name}`);
}
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();
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.
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.
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.
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.
processData() {
console.log("Processing CSV 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.
// Client code
const csvProcessor = new CSVDataProcessor();
csvProcessor.processData();
// Output:
// Loading CSV data...
// Processing CSV data...
// Saving processed data...
// 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.
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.
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.
// 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);
}
}
interpret(context) {
return this.left.interpret(context) && this.right.
interpret(context);
}
}
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);
By using the Interpreter Pattern, you can create systems that are more modular, maintainable, and
adaptable, especially when working with structured expressions or DSLs.
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.
// 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.
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);
backendweekly.dev