Thursday, 12 December 2024

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.

 

Wednesday, 11 December 2024

Builder Method | JavaScript Design Pattern

 The Builder Method is a creational design pattern used to construct complex objects step by step. It allows you to separate the construction of an object from its representation so that the same construction process can create different representations. This pattern is particularly useful when you have an object that needs to be constructed in multiple ways or with many optional parameters.

Key Concepts of the Builder Pattern:

  1. Builder: A class that defines the steps needed to create an object, and provides methods to set the parts of the object.

  2. Concrete Builder: A subclass or implementation of the builder that actually assembles the product.

  3. Product: The object that is being created.

  4. Director: A class that controls the building process by using the builder.

Why Use the Builder Pattern?

  • When you need to construct a complex object with many possible configurations or optional components.

  • To avoid the need for constructors with many parameters (often known as the "telescoping constructor anti-pattern").

  • To ensure that the construction process is independent of the object's representation.

JavaScript Example of Builder Pattern

Let’s imagine we want to construct a Car object that has different configurations (e.g., with or without air conditioning, sunroof, etc.). The Builder Pattern would help in this case.

1. Product: The Car

The Car is the object that we are going to build.

class Car {

    constructor() {

        this.model = '';

        this.color = '';

        this.airConditioning = false;

        this.sunroof = false;

        this.gps = false;

    }

}

2. Builder: Abstract builder for constructing the car

The CarBuilder will define the steps to create the car object. It provides methods for setting the car's attributes.

class CarBuilder {

    constructor() {

        this.car = new Car();

    }

 

    setModel(model) {

        this.car.model = model;

        return this; // Return the builder itself for chaining

    }

 

    setColor(color) {

        this.car.color = color;

        return this; // Return the builder itself for chaining

    }

 

    addAirConditioning() {

        this.car.airConditioning = true;

        return this; // Return the builder itself for chaining

    }

 

    addSunroof() {

        this.car.sunroof = true;

        return this; // Return the builder itself for chaining

    }

 

    addGPS() {

        this.car.gps = true;

        return this; // Return the builder itself for chaining

    }

 

    build() {

        return this.car; // Return the constructed car

    }

}

3. Director (Optional): Controls the construction process

The CarDirector will use the builder to construct the car in a particular order.

class CarDirector {

    constructor(builder) {

        this.builder = builder;

    }

 

    constructBasicCar() {

        return this.builder.setModel('Basic Model')

                           .setColor('White')

                           .build();

    }

 

    constructLuxuryCar() {

        return this.builder.setModel('Luxury Model')

                           .setColor('Black')

                           .addAirConditioning()

                           .addSunroof()

                           .addGPS()

                           .build();

    }

}

4. Client Code: Putting it all together

Now, the client can use the CarBuilder and CarDirector to create different types of cars.

// Client Code

const builder = new CarBuilder();

const director = new CarDirector(builder);

 

// Construct a basic car

const basicCar = director.constructBasicCar();

console.log(basicCar);

 

// Construct a luxury car

const luxuryCar = director.constructLuxuryCar();

console.log(luxuryCar);

 

// Alternatively, construct a car manually using the builder

const customCar = builder.setModel('Custom Model')

                         .setColor('Red')

                         .addAirConditioning()

                         .build();

console.log(customCar);

Key Points:

  1. Builder Class: The CarBuilder class is responsible for constructing the Car object step by step.

  2. Director Class: The CarDirector class is optional and helps by using the builder to create a complex object with a predefined sequence of method calls. It ensures the construction process is independent of the parts' order or the object's configuration.

  3. Chaining Methods: The builder's methods return the builder itself (this), allowing method chaining to construct the object fluently.

Advantages of the Builder Pattern:

  • Separation of Construction and Representation: The construction of an object is separate from its representation. You can easily modify the construction process without affecting the rest of the code.

  • Complex Object Creation: It makes it easier to create complex objects that might have many parts or optional attributes.

  • Flexible Object Creation: The client can create an object in different ways without needing multiple constructors or factory methods.

Disadvantages:

  • Increased Code Complexity: For simple objects, the builder pattern might add unnecessary complexity.

  • Overhead for Simple Cases: If the object is simple, using the builder pattern might be overkill compared to using a straightforward constructor.

Summary:

The Builder Pattern in JavaScript is a great tool for constructing complex objects step-by-step, especially when those objects have many parameters or optional fields. By separating the object construction logic into a builder, it allows for more flexible and maintainable code, particularly when dealing with objects that require multiple configurations.

 

Tuesday, 10 December 2024

Overview of JavaScript Abstract Factory Method Design Pattern

 The Abstract Factory Method in JavaScript follows the same core principles as in other object-oriented languages, enabling the creation of families of related or dependent objects without specifying their concrete classes. This pattern allows you to work with product families in a way that decouples the client from their concrete implementations.

Example of the Abstract Factory Pattern in JavaScript:

We'll create a simple furniture factory that can produce different types of chairs and sofas for different styles, such as Modern and Victorian.

1. Abstract Factory (Interface for creating products)

We define the abstract factory class. In JavaScript, this is often done with a base class or a constructor function.

class FurnitureFactory {

    createChair() {

        throw new Error("Method 'createChair()' must be implemented.");

    }

    createSofa() {

        throw new Error("Method 'createSofa()' must be implemented.");

    }

}

2. Concrete Factories (Implementations of the abstract factory)

Now, we create two concrete factories that implement the abstract methods to produce different types of furniture (Modern or Victorian).

class ModernFurnitureFactory extends FurnitureFactory {

    createChair() {

        return new ModernChair();

    }

    createSofa() {

        return new ModernSofa();

    }

}

 

class VictorianFurnitureFactory extends FurnitureFactory {

    createChair() {

        return new VictorianChair();

    }

 

    createSofa() {

        return new VictorianSofa();

    }

}

3. Abstract Products (Interfaces for product types)

We also define the abstract products (chair and sofa). These will act as blueprints for the specific products in each family.

class Chair {

    sitOn() {

        throw new Error("Method 'sitOn()' must be implemented.");

    }

}

class Sofa {

    lieOn() {

        throw new Error("Method 'lieOn()' must be implemented.");

    }

}

4. Concrete Products (Specific implementations of the products)

Next, we create the concrete product classes (Modern and Victorian).

class ModernChair extends Chair {

    sitOn() {

        console.log("Sitting on a modern chair");

    }

}

 

class ModernSofa extends Sofa {

    lieOn() {

        console.log("Lying on a modern sofa");

    }

}

 

class VictorianChair extends Chair {

    sitOn() {

        console.log("Sitting on a Victorian chair");

    }

}

 

class VictorianSofa extends Sofa {

    lieOn() {

        console.log("Lying on a Victorian sofa");

    }

}

5. Client Code (The client interacts with the abstract factory and products)

The client can now interact with the abstract factory and does not need to know the concrete implementations of the products. The client can use any factory (Modern or Victorian) and still get consistent results.

function clientCode(factory) {

    const chair = factory.createChair();

    const sofa = factory.createSofa();

 

    chair.sitOn();

    sofa.lieOn();

}

 

// Using Modern Furniture Factory

const modernFactory = new ModernFurnitureFactory();

clientCode(modernFactory);

 

// Using Victorian Furniture Factory

const victorianFactory = new VictorianFurnitureFactory();

clientCode(victorianFactory);

 

Explanation:

  1. FurnitureFactory is the abstract factory class that defines the methods createChair() and createSofa().

  2. ModernFurnitureFactory and VictorianFurnitureFactory are concrete factories that implement the abstract factory methods to produce specific products (Modern or Victorian).

  3. Chair and Sofa are abstract product classes defining the behavior of the products (methods sitOn() and lieOn()).

  4. ModernChair, ModernSofa, VictorianChair, and VictorianSofa are concrete product classes implementing the abstract product behavior.

Benefits of the Abstract Factory Pattern in JavaScript:

  • Decoupling: The client code does not depend on specific concrete classes, just on abstract classes or interfaces, making it more flexible.

  • Consistency: It ensures that products created by a factory (like a chair and sofa) are compatible with each other, maintaining a consistent family of objects.

  • Scalability: You can easily add new families of products (e.g., Scandinavian furniture) without changing the client code.

Drawbacks:

  • Increased Complexity: More classes and code are involved, which might make the system more complex and harder to understand.

  • Difficulty in Extending: If you want to add new types of products (e.g., tables), you would need to modify the abstract factory and potentially all concrete factories.