Thursday, 12 December 2024

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.

 

No comments:

Post a Comment