Monday, 10 February 2025

Single Responsibility Principle (SRP) in React

The Single Responsibility Principle (SRP) is one of the five SOLID principles of object-oriented design, but it also applies in React, especially when thinking about how to structure components. It suggests that a component (or function) should have one reason to change, meaning it should be responsible for a single part of the functionality.

Applying SRP in React:

In the context of React, SRP encourages the creation of small, focused components that handle only one responsibility. This makes components easier to understand, test, and maintain, and improves reusability and flexibility. Here's how you can implement SRP in React:

1. Component Design:

  • A component should only handle a specific feature or task. For example, if you are building a UI for displaying a user’s profile, a UserProfile component should only be responsible for displaying the user's details. It should not also be responsible for handling network requests, managing form logic, or handling unrelated state.
  • If your component needs to do more than one thing (e.g., displaying data and fetching data), you should consider breaking it down into smaller components or separating the concerns by using hooks or external services.

Example:

  • Bad SRP: A component that displays user data and also fetches user data from an API.

const UserProfile = () => {

  const [user, setUser] = useState(null);

 

  useEffect(() => {

    fetch('/api/user')

      .then(response => response.json())

      .then(data => setUser(data));

  }, []);

 

  return (

    <div>

      {user && <h1>{user.name}</h1>}

      <p>{user?.email}</p>

    </div>

  );

};

  • Good SRP: Separate the data fetching logic and display logic.
    • UserProfile.js: A component responsible for displaying the user data.

const UserProfile = ({ user }) => (

  <div>

    <h1>{user.name}</h1>

    <p>{user.email}</p>

  </div>

);

    • UserContainer.js: A component responsible for fetching the data and passing it down.

const UserContainer = () => {

  const [user, setUser] = useState(null);

 useEffect(() => {

    fetch('/api/user')

      .then(response => response.json())

      .then(data => setUser(data));

  }, []);

 

  return user ? <UserProfile user={user} /> : <p>Loading...</p>;

};

2. Use Custom Hooks for Reusable Logic:

When you find yourself repeating similar logic across components (like fetching data, managing forms, etc.), it’s a good practice to encapsulate that logic into a custom hook. This follows SRP by separating the business logic from the UI components.

Example:

  • useFetch.js (Custom hook for data fetching):

const useFetch = (url) => {

  const [data, setData] = useState(null);

  const [loading, setLoading] = useState(true);

 

  useEffect(() => {

    fetch(url)

      .then((response) => response.json())

      .then((data) => {

        setData(data);

        setLoading(false);

      });

  }, [url]);

 

  return { data, loading };

};

  • UserContainer.js (Using the custom hook):

const UserContainer = () => {

  const { data: user, loading } = useFetch('/api/user');

 

  if (loading) {

    return <p>Loading...</p>;

  }

 

  return <UserProfile user={user} />;

};

3. Separation of Concerns with Styles:

Keeping the presentation logic (styling) and functionality (data manipulation) separate also aligns with SRP. In React, you can achieve this by using styled components or CSS modules to manage styles separately from your core logic.

Example:

  • UserProfile.module.css (CSS module for styling):

.userName {

  font-size: 24px;

  font-weight: bold;

}

.userEmail {

  font-size: 16px;

}

  • UserProfile.js:

import styles from './UserProfile.module.css';

 

const UserProfile = ({ user }) => (

  <div>

    <h1 className={styles.userName}>{user.name}</h1>

    <p className={styles.userEmail}>{user.email}</p>

  </div>

);


Benefits of SRP in React:

  • Easier Maintenance: When each component is focused on a single responsibility, it becomes easier to make changes without affecting other parts of the application.

  • Better Testability: Components that follow SRP are easier to test because they perform fewer actions and have fewer dependencies.

  • Reusability: Smaller, focused components are easier to reuse across different parts of your app.

  • Improved Readability: Code that follows SRP is generally more readable, as each component’s purpose is clear.

 

Conclusion:

In React, applying the Single Responsibility Principle means designing components that focus on a specific task, making them easier to maintain, test, and extend. By separating concerns, using custom hooks, and dividing complex components into smaller ones, you can keep your codebase clean and efficient.

 

Thursday, 12 December 2024

Flyweight Design Pattern – JavaScript Design Pattern

The Flyweight Design Pattern is a structural design pattern that aims to reduce memory usage by sharing common data among similar objects. This pattern is particularly useful when an application needs to create many objects that are similar but not identical, as it allows the system to reuse existing objects rather than creating new ones, thereby optimizing memory usage.

The Flyweight pattern achieves this by separating the object's intrinsic state (shared data) from its extrinsic state (unique data). The intrinsic state is stored in the flyweight object, and the extrinsic state is passed in when needed.

Key Concepts:

  1. Flyweight: The object that stores the intrinsic (shared) state.

  2. Intrinsic State: Data that can be shared between many objects (e.g., properties that do not change between objects).

  3. Extrinsic State: Data that is unique to a specific object and may change between different objects (e.g., properties that vary).

  4. Flyweight Factory: A factory that manages the flyweights and ensures that objects are shared when possible.

Example of Flyweight Pattern in JavaScript

Let's consider a scenario where we are managing a large number of characters for a word processor. Each character might have a few shared properties (e.g., font, size, color), but each character has a unique position (extrinsic state).

Without Flyweight Pattern (Naive Approach):

class Character {

    constructor(char, font, size, color) {

        this.char = char;

        this.font = font;

        this.size = size;

        this.color = color;

    }

 

    display(position) {

        console.log(`Character: ${this.char}, Position: ${position}, Font: ${this.font}, Size: ${this.size}, Color: ${this.color}`);

    }

}

 

// Creating separate objects for every character

let characters = [

    new Character("A", "Arial", 12, "red"),

    new Character("B", "Arial", 12, "blue"),

    new Character("A", "Arial", 12, "red"),

    new Character("C", "Arial", 12, "green"),

];

 

characters.forEach((char, index) => char.display(index));

This approach could be inefficient because many of the Character objects have the same properties (e.g., font, size, and color) but are stored as separate instances, wasting memory.

With Flyweight Pattern (Optimized Approach):

To optimize the memory usage, we can implement the Flyweight Pattern, where common data is shared, and only unique data is stored separately.

// Flyweight Class: Represents the shared state (intrinsic state)

class CharacterFlyweight {

    constructor(font, size, color) {

        this.font = font;

        this.size = size;

        this.color = color;

    }

 

    display(char, position) {

        console.log(`Character: ${char}, Position: ${position}, Font: ${this.font}, Size: ${this.size}, Color: ${this.color}`);

    }

}

 

// Flyweight Factory: Manages the shared flyweights

class CharacterFactory {

    constructor() {

        this.flyweights = {};

    }

 

    getFlyweight(font, size, color) {

        const key = `${font}-${size}-${color}`;

        if (!this.flyweights[key]) {

            this.flyweights[key] = new CharacterFlyweight(font, size, color);

        }

        return this.flyweights[key];

    }

}

 

// Extrinsic state: Position of each character

let characterFactory = new CharacterFactory();

 

let characters = [

    { char: "A", font: "Arial", size: 12, color: "red", position: 0 },

    { char: "B", font: "Arial", size: 12, color: "blue", position: 1 },

    { char: "A", font: "Arial", size: 12, color: "red", position: 2 },

    { char: "C", font: "Arial", size: 12, color: "green", position: 3 },

];

 

// Using the factory to get flyweights and share common states

characters.forEach(character => {

    let flyweight = characterFactory.getFlyweight(character.font, character.size, character.color);

    flyweight.display(character.char, character.position);

});

Explanation:

  1. CharacterFlyweight: The CharacterFlyweight class represents the intrinsic state of a character, which can be shared. This includes the font, size, and color. These properties are common and do not change between characters.

  2. CharacterFactory: The CharacterFactory class ensures that we only create one instance of a CharacterFlyweight for each unique combination of font, size, and color. It uses the combination of these three properties as a key to store and retrieve the flyweight objects.

  3. Extrinsic State: The position of the character is an extrinsic property that is unique for each instance and is passed to the display method when needed.

  4. Client Code: The client creates a list of characters, and instead of creating a new Character object for each one, it uses the CharacterFactory to get a flyweight. Only the position (extrinsic state) is passed to the flyweight when displaying the character.

Advantages of the Flyweight Pattern:

  1. Memory Efficiency: By sharing common data between objects, memory usage is reduced significantly. This is especially useful when you have large numbers of similar objects.

  2. Performance Improvement: Creating fewer objects means less overhead for object creation and management. The Flyweight pattern can improve performance when dealing with large numbers of objects.

  3. Centralized Management: The factory ensures that the flyweights are managed and reused properly, providing a clean interface for object creation.

Disadvantages of the Flyweight Pattern:

  1. Complexity: The Flyweight pattern introduces some complexity because it requires separating intrinsic and extrinsic states. This can make the code harder to understand and maintain.

  2. Not Always Applicable: The Flyweight pattern is only useful when there are many similar objects that can share intrinsic data. If the objects are unique in many aspects, the pattern may not provide significant benefits.

  3. Extrinsic State Management: The client code must explicitly manage the extrinsic state (e.g., the position of characters in the above example), which could increase the complexity.

When to Use the Flyweight Pattern:

  • When your system needs to handle large numbers of similar objects, and you want to reduce memory usage.

  • When you have objects with similar states that can be shared across many instances.

  • When you need to manage large sets of data (e.g., characters in a word processor, graphical elements in a game) where much of the data can be shared.

Conclusion:

The Flyweight Design Pattern is an effective solution for reducing memory usage when dealing with many similar objects. By separating shared and unique states, and reusing existing objects, the Flyweight pattern helps optimize both memory and performance. However, it is best suited for cases where many objects share common data, and managing the extrinsic state does not introduce significant complexity.

 

Facade Design Pattern | JavaScript Design Pattern

The Facade Design Pattern is a structural design pattern that provides a simplified interface to a complex subsystem or set of interfaces. It hides the complexities of the subsystem from the client and provides a higher-level interface that makes the subsystem easier to use. The main goal of the Facade pattern is to provide a single, unified interface to a set of interfaces in a subsystem, making the subsystem easier to interact with.

Key Concepts:

  • Facade: The main class that provides a simplified interface to the subsystem.

  • Subsystem: A group of classes or components that implement the actual functionality but are complex and often not directly accessible by the client.

Benefits of the Facade Pattern:

  1. Simplification: It hides the complexity of the subsystem, making it easier for the client to interact with.

  2. Decoupling: The client code is decoupled from the complex subsystem. It interacts only with the Facade and doesn't need to know about the subsystem's inner workings.

  3. Single Entry Point: A Facade provides a single entry point for operations that require access to multiple parts of a system.

  4. Maintainability: By reducing the complexity exposed to the client, you can make the system more maintainable and easier to change.

Example of the Facade Pattern in JavaScript

Imagine a complex system for managing a home theater, involving several subsystems: a DVD player, a projector, a sound system, and lights. Instead of requiring the client to interact with all of these systems directly, we can use a Facade to provide a simpler interface.

Without Facade Pattern (Complex Client Code):

class DVDPlayer {

    on() {

        console.log("DVD Player is on.");

    }

    play(movie) {

        console.log(`Playing movie: ${movie}`);

    }

    stop() {

        console.log("DVD Player stopped.");

    }

    off() {

        console.log("DVD Player is off.");

    }

}

 

class Projector {

    on() {

        console.log("Projector is on.");

    }

    setInput(source) {

        console.log(`Projector input set to ${source}`);

    }

    off() {

        console.log("Projector is off.");

    }

}

 

class SoundSystem {

    on() {

        console.log("Sound system is on.");

    }

    setVolume(level) {

        console.log(`Setting sound volume to ${level}`);

    }

    off() {

        console.log("Sound system is off.");

    }

}

 

class Lights {

    on() {

        console.log("Lights are on.");

    }

    dim() {

        console.log("Lights are dimmed.");

    }

    off() {

        console.log("Lights are off.");

    }

}

 

// Client code requires interacting with each subsystem:

const dvdPlayer = new DVDPlayer();

const projector = new Projector();

const soundSystem = new SoundSystem();

const lights = new Lights();

 

// Client needs to know how to manage multiple subsystems.

dvdPlayer.on();

projector.on();

projector.setInput("DVD");

soundSystem.on();

soundSystem.setVolume(5);

lights.dim();

dvdPlayer.play("Inception");

This approach works, but the client needs to interact with each subsystem directly, which can get cumbersome and error-prone.

With the Facade Pattern (Simplified Client Code):

Now, we create a Facade to simplify the interaction between the client and the subsystem.

// Facade Class: Simplifies interaction with the subsystems

class HomeTheaterFacade {

    constructor(dvdPlayer, projector, soundSystem, lights) {

        this.dvdPlayer = dvdPlayer;

        this.projector = projector;

        this.soundSystem = soundSystem;

        this.lights = lights;

    }

 

    watchMovie(movie) {

        console.log("Get ready to watch a movie...");

        this.lights.dim();

        this.projector.on();

        this.projector.setInput("DVD");

        this.soundSystem.on();

        this.soundSystem.setVolume(5);

        this.dvdPlayer.on();

        this.dvdPlayer.play(movie);

    }

 

    endMovie() {

        console.log("Shutting down the home theater...");

        this.dvdPlayer.stop();

        this.dvdPlayer.off();

        this.soundSystem.off();

        this.projector.off();

        this.lights.on();

    }

}

 

// Subsystem components

const dvdPlayer = new DVDPlayer();

const projector = new Projector();

const soundSystem = new SoundSystem();

const lights = new Lights();

 

// Facade to interact with the subsystems

const homeTheater = new HomeTheaterFacade(dvdPlayer, projector, soundSystem, lights);

 

// Client code now interacts with the Facade

homeTheater.watchMovie("Inception");

homeTheater.endMovie();

Explanation:

  1. Subsystem Classes (DVDPlayer, Projector, SoundSystem, Lights): These are the complex components that perform various functions in the home theater system. Each one has a variety of methods that can be called individually.

  2. Facade Class (HomeTheaterFacade): This class provides a simplified interface for the client. The client only needs to interact with the HomeTheaterFacade, which in turn coordinates with all the subsystems (e.g., turning on the DVD player, adjusting the projector, etc.).

  3. Client Code: The client code interacts only with the HomeTheaterFacade, calling methods like watchMovie() and endMovie() instead of interacting with each subsystem directly. This simplifies the client's experience and makes the code easier to maintain.

Advantages of the Facade Pattern:

  1. Simplicity: The Facade provides a simplified, unified interface to a set of complex subsystems. This makes it easier for the client to use.

  2. Decoupling: The client does not need to know the details of the subsystem. It just calls the methods of the Facade. This reduces the dependency between the client and the subsystem, making the system more flexible.

  3. Easy to Extend: You can add new subsystems or modify existing ones without changing the client code. The Facade can be modified to incorporate new behavior without affecting the clients.

  4. Centralized Control: The Facade acts as a central point of control for the subsystem, so if any part of the system needs to change, it only needs to be changed in the Facade.

When to Use the Facade Pattern:

  • When you have a complex subsystem that you want to simplify for client use.

  • When you want to decouple a client from the implementation details of a subsystem.

  • When you want to provide a simple interface to a set of related classes or modules.

  • When the client only needs to interact with a subset of the functionality offered by a subsystem.

Conclusion:

The Facade Pattern is a useful tool for simplifying complex systems and making them easier to use by providing a single, higher-level interface to clients. It can help in scenarios where a subsystem is large or complex, and you want to avoid exposing too much of that complexity to the client. By using a Facade, you make the system easier to interact with and more maintainable.

 

Decorator Design Pattern | JavaScript Design Patterns

The Decorator Design Pattern is a structural design pattern that allows you to dynamically add behavior or responsibilities to an object without altering its code. The key idea is to create a "wrapper" around an object that adds additional functionality while keeping the original object intact. This is typically done by implementing the same interface or base class and wrapping the original object.

Key Concepts:

  1. Component: The interface or base class that defines the basic functionality.

  2. ConcreteComponent: A class that implements the basic functionality (the object being decorated).

  3. Decorator: A base class or interface that wraps the Component and adds additional behavior.

  4. ConcreteDecorator: A class that extends the Decorator and implements the additional functionality.

Example of Decorator Pattern in JavaScript

Let's say we have a basic Coffee class and we want to add different kinds of "decorations" to it, such as milk, sugar, and chocolate. Each decorator adds something to the coffee's description or cost.

Without Decorators (Inflexible Approach):

class Coffee {

    cost() {

        return 5;

    }

 

    description() {

        return 'Plain coffee';

    }

}

 

class MilkCoffee {

    constructor(coffee) {

        this.coffee = coffee;

    }

 

    cost() {

        return this.coffee.cost() + 1;  // Adds cost of milk

    }

 

    description() {

        return this.coffee.description() + ', with milk';

    }

}

 

class SugarCoffee {

    constructor(coffee) {

        this.coffee = coffee;

    }

 

    cost() {

        return this.coffee.cost() + 0.5;  // Adds cost of sugar

    }

 

    description() {

        return this.coffee.description() + ', with sugar';

    }

}

 

let myCoffee = new Coffee();

console.log(myCoffee.description()); // Plain coffee

console.log(myCoffee.cost()); // 5

 

myCoffee = new MilkCoffee(myCoffee); // Decorate with Milk

console.log(myCoffee.description()); // Plain coffee, with milk

console.log(myCoffee.cost()); // 6

 

myCoffee = new SugarCoffee(myCoffee); // Decorate with Sugar

console.log(myCoffee.description()); // Plain coffee, with milk, with sugar

console.log(myCoffee.cost()); // 6.5

While this approach works, it creates a lot of subclassing (MilkCoffee, SugarCoffee, etc.) and becomes increasingly difficult to extend with additional features like adding chocolate or adjusting the size of the coffee.

With the Decorator Pattern (Flexible Approach):

// Base Component

class Coffee {

    cost() {

        return 5;

    }

 

    description() {

        return 'Plain coffee';

    }

}

 

// Decorator Base Class

class CoffeeDecorator {

    constructor(coffee) {

        this.coffee = coffee;

    }

 

    cost() {

        return this.coffee.cost();

    }

 

    description() {

        return this.coffee.description();

    }

}

 

// Concrete Decorators

class MilkDecorator extends CoffeeDecorator {

    cost() {

        return this.coffee.cost() + 1; // Adds milk cost

    }

 

    description() {

        return this.coffee.description() + ', with milk';

    }

}

 

class SugarDecorator extends CoffeeDecorator {

    cost() {

        return this.coffee.cost() + 0.5; // Adds sugar cost

    }

 

    description() {

        return this.coffee.description() + ', with sugar';

    }

}

 

class ChocolateDecorator extends CoffeeDecorator {

    cost() {

        return this.coffee.cost() + 1.5; // Adds chocolate cost

    }

 

    description() {

        return this.coffee.description() + ', with chocolate';

    }

}

 

// Usage

let myCoffee = new Coffee();

console.log(myCoffee.description()); // Plain coffee

console.log(myCoffee.cost()); // 5

 

myCoffee = new MilkDecorator(myCoffee); // Decorate with Milk

console.log(myCoffee.description()); // Plain coffee, with milk

console.log(myCoffee.cost()); // 6

 

myCoffee = new SugarDecorator(myCoffee); // Decorate with Sugar

console.log(myCoffee.description()); // Plain coffee, with milk, with sugar

console.log(myCoffee.cost()); // 6.5

 

myCoffee = new ChocolateDecorator(myCoffee); // Decorate with Chocolate

console.log(myCoffee.description()); // Plain coffee, with milk, with sugar, with chocolate

console.log(myCoffee.cost()); // 8

Explanation:

  1. Coffee: The base Component that defines the basic functionality of a coffee (cost and description).

  2. CoffeeDecorator: The abstract Decorator that holds a reference to a Coffee object and provides the same interface (cost() and description()). Concrete decorators will extend this class and add specific functionality.

  3. MilkDecorator, SugarDecorator, ChocolateDecorator: These are the ConcreteDecorators that add specific features to the coffee, such as milk, sugar, or chocolate. Each decorator modifies the behavior of the cost() and description() methods.

Advantages of the Decorator Pattern:

  1. Flexibility: You can dynamically add new behavior to an object at runtime without altering its structure. For example, you can mix and match decorators to create different variations of an object.

  2. Single Responsibility Principle: The functionality is added through decorators, which keep the codebase cleaner. Each decorator has a single responsibility: adding one piece of functionality to the object.

  3. Open-Closed Principle: You can extend the functionality of the object by creating new decorators rather than modifying the object itself, adhering to the open-closed principle of object-oriented design.

  4. Composition over inheritance: The decorator pattern avoids deep inheritance hierarchies by allowing new functionality to be added through composition rather than subclassing.

When to Use the Decorator Pattern:

  • When you need to add responsibilities or features to objects dynamically without affecting other objects of the same class.

  • When subclassing would result in an explosion of new classes (for example, multiple combinations of features).

  • When you want to keep the core logic of a class clean and add additional behavior step by step.

  • When you want to extend functionality in a flexible and reusable way without modifying existing code.

 

Composite Design Pattern | JavaScript Design Patterns

The Composite Design Pattern is a structural design pattern that allows you to treat individual objects and compositions of objects uniformly. It is particularly useful for representing part-whole hierarchies, where individual objects and collections of objects are treated in a similar way. This pattern is commonly used when you need to build a tree-like structure.

Key Concepts:

  1. Component: This is the common interface for both leaf objects and composite objects. It defines the operations that can be performed on both individual objects and groups of objects.

  2. Leaf: A leaf is an individual object in the structure. It implements the Component interface and doesn't have any children.

  3. Composite: A composite object is a collection of Component objects (which could be either leaf or other composite objects). It implements the Component interface and can have child components.

Example of Composite Pattern in JavaScript

Let's consider a scenario where we have a file system, with files (leaf) and folders (composite). Both files and folders can have the getSize method, but a folder can contain other folders or files.

Without the Composite Pattern (Non-Uniform Handling):

class File {

    constructor(name, size) {

        this.name = name;

        this.size = size;

    }

 

    getSize() {

        return this.size;

    }

}

 

class Folder {

    constructor(name) {

        this.name = name;

        this.children = [];

    }

 

    add(child) {

        this.children.push(child);

    }

 

    getSize() {

        let totalSize = 0;

        for (let child of this.children) {

            totalSize += child.getSize();

        }

        return totalSize;

    }

}

 

const file1 = new File("file1.txt", 10);

const file2 = new File("file2.txt", 20);

 

const folder1 = new Folder("folder1");

folder1.add(file1);

folder1.add(file2);

 

console.log(`Folder size: ${folder1.getSize()}`); // 30

In this example, the Folder class has to manage its children manually by iterating over them and summing up their sizes. If we wanted to add other kinds of elements, we'd need to modify this logic. This becomes cumbersome as the complexity grows.

With the Composite Pattern (Uniform Handling):

Now, let's refactor the code using the Composite Pattern, where both File and Folder share the same interface and are treated uniformly.

// Component: Common interface

class FileSystemComponent {

    constructor(name) {

        this.name = name;

    }

 

    getSize() {

        throw new Error("This method should be overridden!");

    }

}

 

// Leaf: File object

class File extends FileSystemComponent {

    constructor(name, size) {

        super(name);

        this.size = size;

    }

 

    getSize() {

        return this.size;

    }

}

 

// Composite: Folder object that can contain other components

class Folder extends FileSystemComponent {

    constructor(name) {

        super(name);

        this.children = [];

    }

 

    add(child) {

        this.children.push(child);

    }

 

    getSize() {

        let totalSize = 0;

        for (let child of this.children) {

            totalSize += child.getSize();

        }

        return totalSize;

    }

}

 

// Usage:

const file1 = new File("file1.txt", 10);

const file2 = new File("file2.txt", 20);

 

const folder1 = new Folder("folder1");

folder1.add(file1);

folder1.add(file2);

 

const file3 = new File("file3.txt", 15);

const folder2 = new Folder("folder2");

folder2.add(file3);

folder2.add(folder1); // folder2 contains folder1

 

console.log(`File3 size: ${file3.getSize()}`); // 15

console.log(`Folder1 size: ${folder1.getSize()}`); // 30

console.log(`Folder2 size: ${folder2.getSize()}`); // 45 (15 + 30)

Explanation:

  1. FileSystemComponent: This is the Component interface, providing the getSize method, which is overridden by both File and Folder.

  2. File: The Leaf class. It has a name and a size. The getSize method returns the size of the file.

  3. Folder: The Composite class. It has a collection of children (which could be File or other Folder objects). It overrides the getSize method to calculate the total size by recursively calling getSize on its children.

Advantages of the Composite Pattern:

  1. Uniformity: Both leaf and composite objects implement the same interface, making the handling of individual objects and compositions transparent and uniform.

  2. Flexibility: You can add new types of components (like different types of files or folders) without modifying existing code. New components can simply be added to the hierarchy.

  3. Ease of Use: The composite object (e.g., Folder) can be treated the same as the individual leaf objects (e.g., File) in client code, simplifying operations like calculating total size, printing contents, etc.

  4. Recursive Structure: The pattern naturally supports recursive structures, where composites can contain other composites.

When to Use the Composite Pattern:

  • When you need to represent part-whole hierarchies (like a filesystem, organization chart, or product catalog).

  • When you want to treat individual objects and groups of objects uniformly.

  • When you need to add new types of components to a structure without changing existing code.

 

Bridge Method | JavaScript Design Pattern

The Bridge Pattern is a structural design pattern that is used to separate an abstraction from its implementation, allowing both to vary independently. This pattern is especially useful when you need to avoid a "polluted" inheritance structure and want to decouple abstraction and implementation in a way that they can evolve separately. It is an excellent choice when a system needs to be flexible and scalable.

Key Concepts:

  1. Abstraction: The higher-level interface or class that defines the operations (abstract class).

  2. RefinedAbstraction: A subclass of the abstraction that can be refined or extended.

  3. Implementor: The interface or class that provides the implementation (lower-level interface).

  4. ConcreteImplementor: A concrete subclass of the implementor that provides the actual implementation.

The Bridge Pattern allows you to "bridge" the abstraction with its implementation by separating them into two different hierarchies: one for the abstraction and one for the implementation.

Example of Bridge Pattern in JavaScript

Imagine you have a graphic library that needs to support different shapes (e.g., circles, squares) and different rendering methods (e.g., canvas, SVG). Instead of creating multiple classes combining every possible shape and rendering method (e.g., CircleCanvas, SquareCanvas, CircleSVG, SquareSVG), you can use the Bridge Pattern to separate the shape (abstraction) from the rendering method (implementation).

Without Bridge Pattern (Complex Inheritance):

class CircleCanvas {

    draw() {

        console.log("Drawing circle on canvas");

    }

}

 

class SquareCanvas {

    draw() {

        console.log("Drawing square on canvas");

    }

}

 

class CircleSVG {

    draw() {

        console.log("Drawing circle in SVG");

    }

}

 

class SquareSVG {

    draw() {

        console.log("Drawing square in SVG");

    }

}

 

// Multiple combinations lead to a complex inheritance structure

let circleCanvas = new CircleCanvas();

circleCanvas.draw(); // Drawing circle on canvas

 

let squareSVG = new SquareSVG();

squareSVG.draw(); // Drawing square in SVG

This approach leads to a lot of classes that can be hard to maintain and extend.

With Bridge Pattern (Simplified Structure):

With the Bridge Pattern, you separate the Shape abstraction from the Rendering implementation. This allows you to mix and match different shapes with different rendering methods.

// Implementor: Defines the rendering interface

class Renderer {

    render(shape) {

        throw new Error("This method should be overridden!");

    }

}

 

// ConcreteImplementor: Canvas rendering

class CanvasRenderer extends Renderer {

    render(shape) {

        console.log(`Rendering ${shape.getType()} on canvas`);

    }

}

 

// ConcreteImplementor: SVG rendering

class SVGRenderer extends Renderer {

    render(shape) {

        console.log(`Rendering ${shape.getType()} in SVG`);

    }

}

 

// Abstraction: Defines the shape interface

class Shape {

    constructor(renderer) {

        this.renderer = renderer; // Bridge to the implementation

    }

 

    draw() {

        this.renderer.render(this);

    }

 

    getType() {

        throw new Error("This method should be overridden!");

    }

}

 

// RefinedAbstraction: Circle shape

class Circle extends Shape {

    constructor(renderer) {

        super(renderer);

    }

 

    getType() {

        return "circle";

    }

}

 

// RefinedAbstraction: Square shape

class Square extends Shape {

    constructor(renderer) {

        super(renderer);

    }

 

    getType() {

        return "square";

    }

}

 

// Usage

const canvasRenderer = new CanvasRenderer();

const svgRenderer = new SVGRenderer();

 

const circleOnCanvas = new Circle(canvasRenderer);

circleOnCanvas.draw(); // Rendering circle on canvas

 

const squareOnSVG = new Square(svgRenderer);

squareOnSVG.draw(); // Rendering square in SVG

 

const circleOnSVG = new Circle(svgRenderer);

circleOnSVG.draw(); // Rendering circle in SVG

 

const squareOnCanvas = new Square(canvasRenderer);

squareOnCanvas.draw(); // Rendering square on canvas

Explanation:

  1. Renderer: This is the Implementor interface that defines the render method, which will be used by different shape objects to perform the actual rendering.

  2. CanvasRenderer and SVGRenderer: These are concrete implementations of the Renderer interface. They define how to render a shape on either canvas or SVG.

  3. Shape: This is the Abstraction class. It holds a reference to the Renderer and delegates the rendering task to the Renderer object. This class does not know the details of how the rendering is done, only that it relies on the Renderer object.

  4. Circle and Square: These are RefinedAbstraction classes that define specific shapes. They override the getType method to provide the type of shape (circle or square).

With this pattern:

  • The abstraction (shape) can be modified independently of the implementation (rendering methods).

  • You can easily add new shapes and rendering methods without altering the existing code much, leading to a flexible and maintainable design.

Advantages of Bridge Pattern:

  1. Separation of Concerns: The abstraction and implementation are separated, which makes the code more modular and easier to maintain.

  2. Scalability: New abstractions (shapes) or implementations (rendering methods) can be added without altering existing code, just by extending the abstract classes and creating new concrete implementations.

  3. Avoids a Large Number of Subclasses: Without the Bridge Pattern, you would need a subclass for each combination of abstraction and implementation. With the Bridge Pattern, you only need subclasses for the abstraction and the implementation, making the design cleaner.

When to Use the Bridge Pattern:

  • When you need to decouple an abstraction from its implementation so both can evolve independently.

  • When you have multiple variations of objects and you want to avoid creating a large number of subclasses to represent all possible combinations.

  • When you want to avoid an explosion of classes in cases where different combinations of abstractions and implementations are needed.

 

Adapter Method | JavaScript Design Patterns

The Adapter Pattern is a structural design pattern used to enable two incompatible interfaces to work together. In JavaScript, the Adapter Pattern is typically used when you need to integrate a class or library with an existing system but the interfaces do not match. The Adapter pattern helps by creating a "wrapper" around the existing interface so that it can be used seamlessly with the new system or interface.

Key Concepts:

  1. Adaptee: The existing interface or class that needs to be adapted to work with a different interface.

  2. Adapter: The class that implements the Adapter pattern. It wraps the Adaptee and provides the expected interface.

  3. Target: The expected interface or class that the system is designed to work with.

Example of Adapter Pattern in JavaScript

Imagine you have a legacy system that works with a OldPaymentSystem interface, but you want to integrate a new NewPaymentSystem without changing the existing code.

Without Adapter (Incompatible Systems):

// Old system's payment interface

class OldPaymentSystem {

    processOldPayment(amount) {

        console.log(`Processing payment of ${amount} using old system`);

    }

}

 

// New system's payment interface

class NewPaymentSystem {

    processPayment(amount) {

        console.log(`Processing payment of ${amount} using new system`);

    }

}

 

// Legacy system expecting old system's interface

class LegacyPaymentProcessor {

    constructor(paymentSystem) {

        this.paymentSystem = paymentSystem;

    }

 

    process(amount) {

        this.paymentSystem.processOldPayment(amount);

    }

}

 

// Incompatible new payment system with legacy system

const oldPaymentSystem = new OldPaymentSystem();

const legacyProcessor = new LegacyPaymentProcessor(oldPaymentSystem);

legacyProcessor.process(100); // Works fine

 

const newPaymentSystem = new NewPaymentSystem();

// legacyProcessor.process(newPaymentSystem); // This would fail

In the above example, the new system (NewPaymentSystem) can't directly be used by LegacyPaymentProcessor, because it expects the processOldPayment method. This is where the Adapter pattern can help.

With Adapter (Using Adapter Pattern):

// Adaptee (Existing Interface)

class OldPaymentSystem {

    processOldPayment(amount) {

        console.log(`Processing payment of ${amount} using old system`);

    }

}

 

// Target (New Interface)

class NewPaymentSystem {

    processPayment(amount) {

        console.log(`Processing payment of ${amount} using new system`);

    }

}

 

// Adapter (Makes the Adaptee conform to the Target interface)

class PaymentAdapter {

    constructor(oldPaymentSystem) {

        this.oldPaymentSystem = oldPaymentSystem;

    }

 

    processPayment(amount) {

        this.oldPaymentSystem.processOldPayment(amount); // Adapting old interface to new one

    }

}

 

// Now the new system can be used seamlessly with the legacy code

class LegacyPaymentProcessor {

    constructor(paymentSystem) {

        this.paymentSystem = paymentSystem;

    }

 

    process(amount) {

        this.paymentSystem.processPayment(amount);

    }

}

 

// Usage:

const oldPaymentSystem = new OldPaymentSystem();

const adapter = new PaymentAdapter(oldPaymentSystem);

const legacyProcessor = new LegacyPaymentProcessor(adapter); // Using the adapter

legacyProcessor.process(100); // Works with old system through adapter

 

const newPaymentSystem = new NewPaymentSystem();

const newAdapter = new PaymentAdapter(newPaymentSystem);

const newLegacyProcessor = new LegacyPaymentProcessor(newAdapter); // Using the adapter

newLegacyProcessor.process(200); // Works with new system through adapter

Explanation:

  • OldPaymentSystem: The existing system with the processOldPayment method.

  • NewPaymentSystem: The new system with the processPayment method.

  • PaymentAdapter: The adapter that makes the old system compatible with the new interface.

  • LegacyPaymentProcessor: The class expecting the new interface (processPayment) but can work with both the old and new systems due to the adapter.

    In this case, the Adapter allows LegacyPaymentProcessor to work with both old and new systems without needing to modify its original design.

Advantages of Adapter Pattern:

  1. Code Reusability: The Adapter allows reusing existing code (Adaptee) without modifying it.

  2. Flexibility: It lets you integrate systems with different interfaces.

  3. Decoupling: It decouples the code from the specifics of the interfaces.

When to Use the Adapter Pattern:

  • When you want to integrate a new component into an existing system, but their interfaces are incompatible.

  • When working with legacy code that you cannot modify but need to adapt for new functionality.