Design Patterns
Design patterns are common approaches to solving similar problems
They are like pre-made blueprints that you can customize to solve a recurring design problem in your code
The 1995 book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, also know as Gang of Four (GoF), describes 23 patterns
Quote from the book:
A design pattern systematically names, motivates, and explains a general design that addresses a recurring design problem in object-oriented systems. It describes the problem, the solution,, when to apply the solution, and its consequences. It also gives implementation hints and examples. The solution is a general arrangement of objects and classes that solve the problem. The solution is customized and implemented to solve the problem in a particular context.
Why use design patterns?
Reusable solutions to common problems
Standardized terminology
Scalability
Maintainability
Performance
Documentation
Best practices
Cross-Domain Applicability
They are just guidelines that help us avoid bad design that are:
- Rigid
- Fragile
- Immobile
Some design patterns tend to cause more problems than they solve, and are thus commonly referred to as anti-patterns
Prerequisite: Knowing OOPs concepts
Need to Know
- [ ] SAGA
- [ ] 2-way pattern
- [ ] Add advantages, usage, and UML for each pattern
Criticism of Patterns
Kludges for a weak programming language: Revenge of the Nerds
For example, the Strategy pattern can be implemented with a simple anonymous (lambda) function in most modern programming languages
Leads to inefficient solutions
Unjustified use:
If all you have is a hammer, everything looks like a nail.
Elements of a Pattern
- Pattern Name: A meaningful name that describes the pattern
- Problem: Describes the problem and its context
- Solution: Describes the elements that make up the design, their relationships, responsibilities, and collaborations
- Consequences: Describes the results and trade-offs of applying the pattern
Classification of Patterns
All patterns can be categorized by their purpose or know how:
Creational Patterns: Deal with object creation mechanisms, trying to create objects in a manner suitable to the situation
Structural Patterns: Deal with object composition, and typically identify simple ways to realize relationships between different objects
Behavioural Patterns: Deal with object communication, how objects interact with each other and how to assign responsibilities between them
These pattern can also be divided based on their scope:
Class:
- Deal with relationships between classes and their subclasses
- These relationships are established through inheritance
- So they are static, fixed at compile-time
Object:
- Deal with object relationships
- Which can be changed at run-time and are more dynamic
The most basic and low-level patterns are often called idioms. They usually apply only to a single programming language
The most universal and high-level patterns are architectural patterns. Developers can implement these patterns in virtually any language. Unlike other patterns, they can be used to design the architecture of an entire application

Other Types of Patterns
Concurrency design patterns: When you are dealing with multi threading programming these are the patterns that you will want to use
Architectural design patterns: Design patterns that are used on the system's architecture, like MVC or MVVM
Creational Patterns
Creational patterns provide object creation mechanisms that increase flexibility and reuse of existing code: How objects are created
- When the regular object creation manner would cause more complexity or bring problems to the code
Patterns:
Abstract Factory: Creates an instance of several families of classes
Builder: Separates object construction from its representation
Factory Method: Creates an instance of several derived classes
Prototype: A fully initialized instance to be copied or cloned
Singleton: A class of which only a single instance can exist
Abstract Factory
| Popularity | Complexity | Scope | AKA |
|---|---|---|---|
| Important | 2 | Object | Kit |
Intent: Provide an interface for creating families of related or dependent objects without specifying their concrete classes
- Groups object factories that have a common theme
Builder Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Common | 2 | Object |
Intent: Separate the construction of a complex object from its representation so that the same construction process can create different representations
Example:
class HotDog {
constructor(bun, ketchup, mustard, kraut) {
this.bun = bun;
this.ketchup = ketchup;
this.mustard = mustard;
this.kraut = kraut;
}
}
// passing parameters to constructor becomes
// problematic if there are many
const snack = new HotDog("wheat", false, true, true);
// add properties to object
// via methods
class HotDog {
constructor(bun) {
this.bun = bun;
}
addKetchup() {
this.ketchup = true;
return this;
}
addMustard() {
this.mustard = true;
return this;
}
addKraut() {
this.kraut = true;
return this;
}
}
const snack = new HotDog("wheat");
// method chaining
snack.addKetchup().addKraut();
console.log(snack); // { bun: 'wheat', ketchup: true, kraut: true }Usage:
- May be very useful while writing Unit Tests
Factory Method
| Popularity | Complexity | Scope | AKA |
|---|---|---|---|
| Important | 2 | Class | Virtual Constructor |
Intent: The Factory Method pattern defines an interface for creating objects, but lets subclasses decide which class to instantiate
- Factory Method lets a class defer instantiation to subclasses
Applicability:
Use the Factory Method when you don't know beforehand the exact types and dependencies of the objects your code should work with
Use the Factory Method when you want to provide users of your library or framework with a way to extend its internal components
Simple Factory Design
In Simple Factory Design pattern, a client asks for an object without knowing where the object is coming from (that is, which class is used to generate it)
Example:
class IOSButton {}
class AndroidButton {}
// without factory
const button1 = os === "ios" ? new IOSButton() : new AndroidButton();
const button2 = os === "ios" ? new IOSButton() : new AndroidButton();
class ButtonFactory {
createButton(os) {
if (os === "ios") {
return new IOSButton();
}
return new AndroidButton();
}
}
// with factory
const factory = new ButtonFactory();
const btn1 = factory.createButton(os);
const btn2 = factory.createButton(os);Usage:
Consider we want to read data from a json or XML file as per the needs
The below two classes are used to read data from these files:
pythonimport json class JSONDataExtractor: def __init__(self, filepath): self.data = dict() with open(filepath, mode='r', encoding='utf-8') as f: self.data = json.load(f) @property def parsed_data(self): return self.data import xml.etree.ElementTree as etree class XMLDataExtractor: def __init__(self, filepath): self.tree = etree.parse(filepath) @property def parsed_data(self): return self.treeNow, write a factory method that returns an instance of JSONDataExtractor or XMLDataExtractor depending on the extension of the file:
pythondef data_extraction_factory(filepath): if filepath.endswith('json'): extractor = JSONDataExtractor elif filepath.endswith('xml'): extractor = XMLDataExtractor else: raise ValueError('Cannot extract the data from {}', format(filepath)) return extractor(filepath)A wrapper function can be added to handle exceptions:
pythondef extract_data_from(filepath): factory_obj = None try: factory_obj = data_extraction_factory(filepath) except ValueError as e: print(e) return factory_objThis can be used in any function to read data from either json or XML file
Prototype Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Not Common | 1 | Object |
Intent: Lets you copy existing objects without making your code dependent on their classes
We can create objects that will be used as prototypes for other objects to be created
Inheritance by prototypes ends up bringing a improvement in performance as well, because both objects have a reference to the same method that is implemented on the prototype, instead of being implemented on each one of them.
It enables us to extent the prototype with new functions that are immediately available to all the objects. This is not a best practice
Example:
// constructor function
function Zombie(name) {
this.name = name || "Zombie";
this.reAnimated = Date.now();
this.eatBrain = function () {
return `${this.name} is hungry for 🧠`;
};
}
Zombie.prototype.canRun = function () {
return this.name !== "Zombie";
};
const obj = new Zombie("🧟♂️ Jeff");
const genericZombie = new Zombie();
console.log(obj);
// { name: '🧟♂️ Jeff', reAnimated: 1664717547330, eatBrain: () }
Object.getPrototypeOf(obj);
// { canRun: () }
obj.eatBrain();
// 🧟♂️ Jeff is hungry for 🧠
genericZombie.eatBrain();
// Zombie is hungry for 🧠
obj.canRun();
// true
genericZombie.canRun();
// false
obj.eatBrain = () => "I am Vegan";
Zombie.prototype.canRun = () => "Error";
obj.eatBrain();
// I am Vegan
genericZombie.eatBrain();
// Zombie is hungry for 🧠
obj.canRun();
// Error
genericZombie.canRun();
// ErrorSingleton Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Important | 1 | Object |
Intent: Ensure a class only has one instance, and provide a global point of access to it
- Global point of access instead of encapsulation
- Hard to debug
Example:
let configurationSingleton = (() => {
// private value of the singleton initialized only once
let config;
const initializeConfiguration = (values) => {
this.randomNumber = Math.random();
values = values || {};
this.number = values.number || 5;
this.size = values.size || 10;
};
// We export the centralized method to return
// the singleton's value
return {
getConfig: (values) => {
// initialize the singleton only once
if (config === undefined) {
config = new initializeConfiguration(values);
}
// and always return the same value
return config;
},
};
})();
const configObject = configurationSingleton.getConfig({ size: 8 });
// prints number: 5, size: 8, randomNumber: someRandomDecimalValue
console.log(configObject);
const configObject1 = configurationSingleton.getConfig({ number: 8 });
// prints number: 5, size: 8, randomNumber: same randomDecimalValue // como no primeiro config
console.log(configObject1);- In JavaScript Singleton can basically be achieved by using object literals:
// app settings
class Settings {
static instance: Settings;
public readonly mode = "dark";
// prevent new with private constructor
private constructor() {}
static getInstance(): Settings {
if (!Settings.instance) {
Settings.instance = new Settings();
}
return Settings.instance;
}
}
const settings = Settings.getInstance();
// same behaviour can be achieved using object literal
const settings = {
dark: true,
};- Meyer's Singleton
Structural Patterns
Structural patterns explain how to assemble objects and classes into larger structures, while keeping these structures flexible and efficient: How objects relate to each other
- They guarantee that if a system's part change, nothing else has to change with it
Design Patterns:
Adapter: Match interfaces of different classes
Bridge: Separates an object's interface from its implementation
Composite: A tree structure of simple and composite objects
Decorator (Wrapper): Add responsibilities to objects dynamically
Facade: A single class that represents an entire subsystem (Simplified API)
Flyweight: A fine-grained instance used for efficient sharing
Proxy: An object representing another object (Substitution)
Adapter Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Important | 1 | Class & Object |
This pattern converts the interface of a class into another interface that clients expect. It allows classes to work together that couldn't otherwise because of incompatible interfaces
The Adapter Design Pattern, also known as the Wrapper, allows two classes to work together that otherwise would have incompatible interfaces
Intent: Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate
- You can create an adapter. This is a special object that converts the interface of one object so that another object can understand it
Applicability:
- Use the Adapter class when you want to use some existing class, but its interface isn't compatible with the rest of your code
Bridge Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Rare | 3 | Object |
Composite Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Important | 2 | Object |
Decorator Pattern
| Popularity | Complexity | Scope | AKA |
|---|---|---|---|
| Important | 2 | Object | Wrapper |
This pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to sub-classing for extending functionality
Intent: Decorator is a structural design pattern that lets you attach new behaviours to objects by placing these objects inside special wrapper objects that contain the behaviours
Applicability:
Use the Decorator pattern when you need to be able to assign extra behaviours to objects at runtime without breaking the code that uses these objects
Use the pattern when it's awkward or not possible to extend an object's behaviour using inheritance
Facade Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Important | 1 | Object |
The facade design pattern is used when we want to create an abstraction layer between what is show publicly and the internal implementation. It is used when we want to have a simpler interface.
This pattern is used, for example, on the DOM selectors of libraries as JQuery, Dojo and D3. These frameworks have powerful selectors that allow us to write complex queries on a very simple way. Something like jQuery(".parent .child div.span") seems simple, but it hides a complex query logic underneath.
Here again, every time we create an abstraction layer above the code, we might end up having a loss of performance. Mostly this loss is irrelevant, but is always good to be considered.
// facade is just simplified API
class PlumbingSystem {
// low level access to plumbing system
setPressure(value) {}
turnOn() {}
turnOff() {}
}
class ElectricalSystem {
// low level access to electrical system
setVoltage(value) {}
turnOn() {}
turnOff() {}
}
// create
class House {
constructor() {
// private
this.plumbing = new PlumbingSystem();
this.electrical = new ElectricalSystem();
}
turnOnSystems() {
this.electrical.setVoltage();
this.electrical.turnOn();
this.plumbing.setPressure();
this.plumbing.turnOn();
}
shutDown() {
this.electrical.turnOff();
this.plumbing.turnOff();
}
}
let client = new House();
client.turnOnSystems();Flyweight Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Object |
Proxy Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Important | 2 | Object |
Behavioural Patterns
Behavioural patterns take care of effective communication and the assignment of responsibilities between objects: How objects communicate with each other
- They help to guarantee that unrelated parts of the application have a synchronized information
- These patterns address communication, responsibility, and algorithmic issues in object-oriented software design
- They help in making the design more flexible, extensible, and maintainable by promoting better communication and separation of concerns between objects and classes in the system
Design Patterns:
Chain of Responsibility: A way of passing a request between a chain of objects
Command: Encapsulate a command request as an object
Interpreter: A way to include language elements in a program
Iterator: Sequentially access the elements of a collection
Mediator: Defines simplified communication between classes
Memento: Capture and restore an object's internal state
Observer: A way of notifying change to a number of classes
State: Alter an object's behaviour when its state changes
Strategy: Encapsulates an algorithm inside a class
Template Method: Defer the exact steps of an algorithm to a subclass
Visitor: Defines a new operation to a class without change
Chain of Responsibility
| Popularity | Complexity | Scope |
|---|---|---|
| Object |
Command Pattern
| Popularity | Complexity | Scope | AKA |
|---|---|---|---|
| 3 | 1 | Object | Action |
Command is behavioural design pattern that converts requests or simple operations into objects
Encapsulate a call as an object
- It is a way to keep separated the caller's context from the called
- An abstraction layer to separate the objects that call the API from the objects that determine when to call it
It encapsulates a request as an object, allowing you to parametrize clients with queues, requests, and operations
- It enables you to decouple the sender from the receiver, providing flexibility in the execution of commands and supporting undoable operations
Cons:
A problem that arises with this pattern is that it creates an additional abstraction layer, and it may impact the performance of an app. It is important to know how to balance performance and code legibility.
Example: Let us consider a simple Ligth class that has two methods: TurnOn and TurnOff. To control the light, we can create a RemoteControl class that has a PressButton method that receives a command to on or off the light
- The
RemoteControlclass is tightly coupled with theLightclass, and if we want to add a new command likeDimthe light, we would have to change theRemoteControlclass
To solve this problem, we can create a Command interface that has an Execute method. We can then create a TurnOnCommand and TurnOffCommand classes that implement the Command interface
- The
RemoteControlclass can then receive aCommandobject and call theExecutemethod - This way, we can add new commands without changing the
RemoteControlclass
+------------------------+ +--------------------------------+
| RemoteControl | | Command |
+------------------------+ +--------------------------------+
| PressButton() | <>-------------> | execute() |
+------------------------+ +--------------------------------+
|
|
+----------------------------+
| |
| |
V V
+----------------+ +----------------+ +----------------+
| Light | | TurnOnCommand | | TurnOffCommand |
+----------------+ +----------------+ +----------------+
| TurnOnCommand | | execute() | | execute() |
| TurnOffCommand | +----------------+ +----------------+
+----------------+ | |
^ | |
| | |
+-------------------------------------+----------------------------+// The object that knows how to execute the command
const invoker = {
add: (x, y) => {
return x + y;
},
subtract: (x, y) => {
return x - y;
},
};
// the object to be used as abstraction layer when
// we execute commands; it represents a interface
// to the caller object
let manager = {
execute: (name, args) => {
if (name in invoker) {
return invoker[name].apply(invoker, [].slice.call(arguments, 1));
}
return false;
},
};
// prints 8
console.log(manager.execute("add", 3, 5));
// prints 2
console.log(manager.execute("subtract", 5, 3));Interpreter Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Class |
Iterator Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| 3 | 2 | Object |
This pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation
- It provides a way of iterating over an object without having to expose the object's internal structure, which may change in the future
- Changing the internals of an object should not effect its consumers
Intent: Iterator is a behavioural design pattern that lets you traverse elements of a collection without exposing its underlying representation (list, stack, tree, etc.)
- Iterate pattern allows to traverse through a collection of object
Applicability:
Use the Iterator pattern when your collection has a complex data structure under the hood, but you want to hide its complexity from clients (either for convenience or security reasons)
Use the pattern to reduce duplication of the traversal code across your app
+------------------------+ +--------------------------------+
| Aggregate | | Iterator |
+------------------------+ +--------------------------------+
| createIterator() | <>------------> | next() |
+------------------------+ | currentItem() |
| hasNext() |
+--------------------------------+The Iterator pattern defines an interface for accessing the elements of a collection. The Iterator object keeps track of the current element and can compute the next element in the collection
- The 3 new methods help consumers to iterate over the object, without knowing the internal data structure
- The
next()method returns the next element in the collection - The
currentItem()method returns the current element in the collection - The
hasNext()method returnstrueif there are more elements in the collection
The Aggregate class is the object that holds the collection of elements. It has a method called createIterator() that returns an instance of the Iterator class
- This complies with the Single Responsibility Principle (SRP) as the
Aggregateclass is responsible for managing the collection of elements, while theIteratorclass is responsible for traversing the collection
Example:
function range(start, end, step = 1) {
return {
[Symbol.iterator]() {
return this;
},
next() {
if (start < end) {
start = start + step;
return { value: start, done: false };
}
return { value: end, done: true };
},
};
}
let sum = 0;
for (let num of range(0, 10)) {
sum += num;
}
console.log(sum);Some languages provide built-in iterators:
for (Animal a : animals) {
a.describe();
}for el in [9, 8, 7, 6, 5]:
print(el)for (let val of aggregate) {
console.log(val);
}Mediator Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Object |
Used a lot on decoupled system
When we have different parts of a system that need to communicate on a coordinated manner, a mediator can be the best option
It can have many-to-many relationship
Middlewares
Example:
class Airplane {
land() {}
}
class Runway {
constructor() {
this.clear = false;
}
}
class Tower {
clearForLanding(runway, plane) {
if (runway.clear) {
console.log(`Plane ${plane} is clear for landing`);
}
}
}
const runway25A = new Runway();
const runway25B = new Runway();
const runway101 = new Runway();
const air567 = new Airplane();
const air007 = new Airplane();
const air69 = new Airplane();Memento Pattern
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later
| Popularity | Complexity | Scope | AKA |
|---|---|---|---|
| 1 | 3 | Object | Snapshot |
Intent: Memento is a behavioural design pattern that lets you save and restore the previous state of an object without revealing the details of its implementation
It is used to restore state of an object to a previous state
- It delegates creating the state snapshots to the actual owner of that state
- Hence, the original class can make the snapshots since it has full access to its own state
- This pattern makes full copies of an object's state, which can be expensive in terms of memory
The Memento design pattern defines three distinct roles:
Originator: the object that knows how to save itself
- Produces snapshots of its own state, and restores its state from snapshots
- Sets and Gets values from the currently targeted Memento. Creates new Mementos and assigns current values to them
Caretaker: the object that knows why and when the Originator needs to save and restore itself
- Responsible for capturing and restoring the Originator's state
- Holds a list that contains all previous versions of the Memento. It can store and retrieve Mementos
Memento: the lock box that is written and read by the Originator, and shepherded by the Caretaker
- Acts as a snapshot of the Originator's state
- The basic object that is stored in different states
Motivation:
Applicability:
Use the Memento pattern when you want to produce snapshots of the object's state to be able to restore a previous state of the object
Consider a text editor that has an undo feature. The editor can save the state of the text editor at any point in time and restore it later. Undo feature is an example of the memento pattern
Use the pattern when direct access to the object's fields/getters/setters violates its encapsulation
Structure:
Participants:
Collaborations:
Consequences:
Pros:
- You can produce snapshots of the object's state without violating its encapsulation
- You can simplify the originator's code by letting the caretaker maintain the history of the originator's state
Cons:
- The app might consume lots of RAM if clients create mementos too often
- Caretakers should track the originator's lifecycle to be able to destroy obsolete mementos
- Most dynamic programming languages, such as JavaScript, Python, and Ruby, can implement the Memento pattern without the memento classes
Implementation:
Known Uses:
Related Patterns:
Example: Consider the following user interactions with a text editor:
- Add a title to the document: "The Memento Pattern"
- Add a paragraph: "The memento pattern is..."
- Change the title to: "The Behavioural Design Pattern"
To implement the undo feature, a single Editor class can be used to save the state of the document at each step. It can have a title and content properties and also fields that store each previous values for each of these properties
+------------------------+
| Editor |
+------------------------+
| title : string |
| content : string |
| previousTitle : List |
| previousContent : List |
+------------------------+Problem with this approach:
- It is not scalable (if more properties are added to the
Editorclass, the number of fields to store previous values will increase) - How would we implement the undo feature?
- If the user changed the title and then the content, then pressed undo, the current implementation has no knowledge of the order of changes
Finding a solution:
- Instead of having multiple fields in the
Editorclass, we can create aEditorStateclass that stores the state of theEditorclass at a given point in time
+------------------------+ +--------------------------------+
| Editor | | EditorState |
+------------------------+ +--------------------------------+
| title : string | <*>------------> | title : string |
| content : string | | content : string |
| previousStates : List | +--------------------------------+
+------------------------+- Composite relationship:
Editoris composed of, or has a field of, theEditorStateclass
This is a good solution as we can undo multiple times and we don't pollute the Editor class with many fields. However, this solution is violating the SRP, as the Editor class currently has multiple responsibilities:
- State management
- Providing the features that we need from an editor
We can move state management to a separate class, History, which will be responsible for managing the state of the Editor class
+------------------------+ +--------------------------------+
| Editor | | EditorState |
+------------------------+ +--------------------------------+
| title : string | ---------------> | title : string |
| content : string | | content : string |
+------------------------+ +--------------------------------+
| createState() | ^
| restore(state) | |
+------------------------+ |
|
^
*
V
+------------------------+
| History |
+------------------------+
| states : List |
| editor : Editor |
+------------------------+
| push(state) |
| pop() |
+------------------------+- The
createState()method returns anEditorStateobject, hence the dotted line arrow (dependency relationship).Historyhas a field with a list ofEditorState, hence the diamond arrow (composition relationship)
This is the Memento pattern in action:
- The
Editorclass is the originator - The
EditorStateclass is the memento - The
Historyclass is the caretaker
Observer Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Important | 2 | Object |
The observer pattern is very useful when we want to optimize the communication between separated parts of the system
Intent: Observer is a behavioural design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they're observing.
This pattern exemplifies loose coupling
It promotes an integration of the parts without making then too coupled
- Subjects and observers interact, but have little knowledge of each other
It has one-to-many relationship
This pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically
Different ways to implement this pattern, but the simpler case is when we have 1 emitter and lots of observers
One variant to this pattern is the publisher/subscriber pattern
- Parts of Subject:
registerObserverorsubscriberemoveObserverorunsubscribenotifyObserverornotifySubscribers
Applicability:
Use the Observer pattern when changes to the state of one object may require changing other objects, and the actual set of objects is unknown beforehand or changes dynamically
Use the pattern when some objects in your app must observe others, but only for a limited time or in specific cases
Example:
let publisherSubscriber = {};
// We pass an object to the container to manage subscriptions
((container) => {
// the id represents a subscription to the topic
let id = 0;
// the objects will subscribe to a topic by
// sending a callback to be executed when
// the event is fired
container.subscribe = (topic, f) => {
if (!(topic in container)) {
container[topic] = [];
}
container[topic].push({
id: ++id,
callback: f,
});
return id;
};
// Every subscription has it's own id, we will
// use it to remove the subscription
container.unsubscribe = (topic, id) => {
let subscribers = [];
for (var subscriber of container[topic]) {
if (subscriber.id !== id) {
subscribers.push(subscriber);
}
}
container[topic] = subscribers;
};
container.publish = (topic, data) => {
for (var subscriber of container[topic]) {
// when we execute a callback it is always
// good to read the documentation to know which
// arguments are passed by the object firing
// the event
subscriber.callback(data);
}
};
})(publisherSubscriber);
let subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", (data) => {
console.log("mouseClicked, data: " + JSON.stringify(data));
});
let subscriptionID2 = publisherSubscriber.subscribe(
"mouseHovered",
function (data) {
console.log("mouseHovered, data: " + JSON.stringify(data));
},
);
let subscriptionID3 = publisherSubscriber.subscribe(
"mouseClicked",
function (data) {
console.log("second mouseClicked, data: " + JSON.stringify(data));
},
);
// When we publish an event, all callbacks should
// be called and you will see three logs
publisherSubscriber.publish("mouseClicked", { data: "data1" });
publisherSubscriber.publish("mouseHovered", { data: "data2" });
// We unsubscribe an event
publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3);
// now we have 2 logs
publisherSubscriber.publish("mouseClicked", { data: "data1" });
publisherSubscriber.publish("mouseHovered", { data: "data2" });Signals
Signals are a simple implementation of the observer pattern that can be used to create reactive state management systems. They allow you to create stateful values that can be observed and updated, triggering side effects when the state changes
- Signals at the core are an event system, where you have publishers and subscribers. The publisher is the signal, and the subscribers are the effects that are triggered when the signal changes
- It utilizes the push-pull model, where the signal pushes updates to the subscribers, and the subscribers pull the current value of the signal when they are triggered. This allows for efficient and reactive state management
let activeEffect = null;
function effect(fn) {
//callback, dependencies) {
// let cleanup;
// const runEffect = () => {
// if (cleanup) cleanup();
// cleanup = callback();
// };
//
// runEffect();
//
// return () => {
// if (cleanup) cleanup();
// };
activeEffect = fn;
fn();
}
function get(signal) {
if (activeEffect) {
signal.subscribers.add(activeEffect);
}
return signal.value;
}
function set(signal, newValue) {
signal.value = newValue;
signal.subscribers.forEach((effect) => effect());
}
// Example usage:
const countSignal = state(0);
effect(() => {
console.log("Count: ", get(countSignal));
});
setInterval(() => {
set(countSignal, get(countSignal) + 1);
}, 1000);Another simple implementation of signals:
const context = [];
function getCurrentObserver() {
return context[context.length - 1];
}
function createSignla(value) {
let subscribers = new Set();
function read() {
const current = getCurrentObserver();
if (current) {
subscribers.add(activeEffect);
}
return value;
}
function write(newValue) {
value = newValue;
subscribers.forEach((subscriber) => subscriber());
}
return [read, write];
}
function createEffect(fn) {
function execute() {
context.push(wrappedEffect);
try {
fn();
} finally {
context.pop();
}
}
execute();
}State Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| 2 | 1 | Object |
Intent: State is a behavioural design pattern that lets an object alter its behaviour when its internal state changes. It appears as if the object changed its class
- The state pattern allows an object to behave differently depending on the state that it is in
- The state pattern is a solution to the problem of how to make behaviour dependent on state
- The state pattern suggests that you create a separate class for each possible state of an object and extract all state-specific behaviours into those classes
Classes and objects participating in the pattern:
- The context is a class that has a field for storing a reference to one of the state objects
- The state is an interface that defines a common method for all concrete states
- The concrete states implement the state interface and provide their own implementations for the state-specific behaviours
The State pattern is closely related to the concept of a Finite-State Machine
Example: When writing a blog post, the post can be in different states:
- Draft
- Moderation (under review by an admin)
- Published
There are 3 types of users:
- Author
- Admin
- Reader
Only the author can change the state of the post from draft to moderation, and only the admin can change the state from moderation to published
First, let's create a simple solution that uses if-else statements to check the current state of the document to see whether the state of the document should be changed and by whom
- This solution is not scalable and violates the Open/Closed principle as we need to modify the
Documentclass every time we add a new state or a new user type
The state pattern suggests that we should create a separate class for each state of the Document object, and extract all state-specific behaviours into those classes
- The
Documentclass will store a reference to one of the state classes to represent the current state - Then, instead of
Documentimplementing state-specific behaviour by itself, it delegates all the state-related work to the state object that has a reference to
+------------------------+ +--------------------------------+
| Document | | State |
+------------------------+ +--------------------------------+
| state:State | <>-------------> | publish() |
| currentUserRole:Roles | +--------------------------------+
+------------------------+ ^
| publish() | |
+------------------------+ |
|
+------------------------+
| DraftState |
+------------------------+
| document |
+------------------------+
| publish() |
+------------------------+Documentkeeps reference to (is composed of) aStateobject (using polymorphism)- In
Document, thepublish()method calls thepublish()method of theStateobject - delegates the work to the concrete state object - This satisfies the Open/Closed principle as we can add new states without modifying the
Documentclass
Strategy Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| 3 | 1 | Object |
This pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This lets the algorithm vary independently from clients that use it
Intent: Strategy is a behavioural design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable
- The Strategy pattern is used to pass different algorithms, or behaviours, to an object
The Strategy pattern suggests that you take a class that does something specific in a lot of different ways and extract all of these algorithms into separate classes called strategies
- The original class, called context, must have a field for storing a reference to one of the strategies. The context delegates the work to a linked strategy object instead of executing it on its own
Applicability:
- Use the Strategy pattern when you want to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime
Example: Lets consider an application that stores videos. Before storing a video, the video needs to be compressed using a specific compression algorithm, such as MOV or MP4. Then, if necessary, apply an overlay to the video, such as black and white or blur. Create a VideoStorage class that can store videos using different compression and overlay algorithms
- When a new compression or overlay algorithm is added, the
VideoStorageclass should be modified to support the new algorithm. This violates the Open/Closed principle - The Strategy pattern suggests that we should extract the compression and overlay algorithms into separate classes and pass them to the
VideoStorageclass - When we create a
VideoStorageobject, we pass it the concrete compressor and overlay objects that we want it to use - This is polymorphism in action:
VideoStoragecan accept many different forms of compressor and overlay objects
+------------------------+ +--------------------------------+
| VideoStorage | | CompressionStrategy |
+------------------------+ +--------------------------------+
| compressionStrategy | <*>------------> | compress() |
| overlayStrategy | +--------------------------------+
+------------------------+ ^ ^
| store() | | |
+------------------------+ | |
| |
+------------+ +------------+
| MP4 | | MOV |
+------------+ +------------+
| compress() | | compress() |
+------------+ +------------+
-- Same for overlay strategy- The
VideoStorageclass is known as the context class - The
CompressionStrategyclass is known as the strategy interface
Other examples:
- Ducks
- Algorithms used to show a route in map for different mode of transport differ
State Pattern vs. Strategy Pattern:
The two patterns are similar in practice, but they have different intents
- States store a reference to the context object that contains them, but strategies don't
- States are allowed to replace themselves (i.e., to change the state of the context object to something else), but strategies don't
- Strategies only handle a single, specific task, while states provide the underlying implementation for everything (or most things) that the context object does
State can be considered as an extension of the Strategy pattern
Both are based on composition: they change the behaviour of the context by delegating some work to helper objects
Strategy makes these objects completely independent and unaware of each other
However, State doesn't restrict dependencies between concrete states, letting them alter the state of the context at will
Pros:
- Satisfies the Open/Closed principle
- Eliminates conditional statements
Cons:
- Clients must be aware of the differences between strategies to choose the right one
- If you only have a couple of algorithms and they rarely change, there's no real reason to overcomplicate the program with new classes and interfaces
Template Method Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Important | 2 | Class |
Visitor Pattern
| Popularity | Complexity | Scope |
|---|---|---|
| Object |
Constructor Pattern
When we think on the classic implementation of object oriented languages, a constructor is a special function that initializes the class's values on default or with input from the caller
Module Pattern
Example:
// Basic structure
const fruitsCollection = (() => {
// private
const objects = [];
// public
return {
addObject(object) {
objects.push(object);
},
removeObject(object) {
let index = objects.indexOf(object);
if (index >= 0) {
objects.splice(index, 1);
}
},
getObjects() {
return JSON.parse(JSON.stringify(objects));
},
};
})();Revealing Module Pattern
This is an evolution of the module pattern
Main difference being that we write all object's logic on the private scope and then expose what we want trough an anonymous object
We can also change the private member's names when we map then to the public ones
Overriding object properties can lead to bug in this pattern
// we write the whole logic as private members
// and expose an anonymous object that maps the
// methods we want as their public counterparts
const fruitsCollection = (() => {
// private
const objects = [];
const addObject = (object) => {
objects.push(object);
};
const removeObject = (object) => {
let index = objects.indexOf(object);
if (index >= 0) {
objects.splice(index, 1);
}
};
const getObjects = () => JSON.parse(JSON.stringify(objects));
// public
return {
addName: addObject,
removeName: removeObject,
getNames: getObjects,
};
})();Publish-Subscribe Pattern
Messaging pattern where senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers
- Messaging pattern, provides framework for exchanging of messages
- Publisher publishes messages to channels/topics
- No constant polling for information, updates are pushed to subscribers
- Publishers do not send messages directly to subscribers, there is a message broker
- Loose coupling between publishers and subscribers
