Tuesday, 8 October 2024

JavaScript Design Patterns & There Usages With Example

 


Design patterns are proven solutions to recurring problems in software development. They provide a template or framework that helps developers design scalable, maintainable, and efficient code. Below are some common design patterns useful in front-end development and large-scale applications.

  • Creational Patterns:
    Creational patterns focus on object creation mechanisms, trying to create objects in a way that suits the situation best. The goal is to manage and control how objects are instantiated.

  1. Factory Pattern:
    The Factory pattern provides a way to create objects without specifying the exact class of the object that will be created. This is useful when the exact type of object isn't known until runtime.

  1. Example (JavaScript Factory):

    class ButtonFactory {
      createButton(type) {
        if (type === 'primary') {
          return new PrimaryButton();
        } else if (type === 'secondary') {
          return new SecondaryButton();
        }
      }
    }
  2. Singleton Pattern:

        The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This is often used for managing application-wide state or services.

        Example (JavaScript Singleton):
            class Singleton {

          constructor() {
            if (!Singleton.instance) {
              this.state = 'initial';
              Singleton.instance = this;
            }
            return Singleton.instance;
          }
 
          getState() {
            return this.state;
          }
    }
    const instance1 = new Singleton();
    const instance2 = new Singleton();

    console.log(instance1 === instance2); // true

  • Structural Patterns:
    Structural patterns focus on object composition and relationships. They help ensure that if one part of a system changes, the entire structure does not need to change.

  • 1. Observer Pattern:

    The Observer pattern defines a one-to-many dependency, where multiple objects listen for changes in a single object (the subject). It’s useful in scenarios like event handling in user interfaces.

    Example (JavaScript Observer):

    class Subject {
      constructor() {
        this.observers = [];
      }
      addObserver(observer) {
        this.observers.push(observer);
      }
      notify(data) {
        this.observers.forEach(observer => observer.update(data));
      }
    }
    class Observer {
      update(data) {
        console.log('Observer received data: ', data);
      }
    }
    const subject = new Subject();
    const observer1 = new Observer();
    subject.addObserver(observer1);

    subject.notify('New Data');


    2. 
    Decorator Pattern:

    The Decorator pattern allows behavior to be added to individual objects, dynamically, without affecting the behavior of other objects from the same class. This pattern is useful for adding functionality to objects without modifying their structure.

    Example (JavaScript Decorator):

    class Car {
      constructor() {
        this.cost = 20000;
      }
     
      getCost() {
        return this.cost;
      }
    }
     
    class SunroofDecorator {
      constructor(car) {
        this.car = car;
      }
     
      getCost() {
        return this.car.getCost() + 1500;
      }
    }
     
    let myCar = new Car();
    myCar = new SunroofDecorator(myCar);

    console.log(myCar.getCost()); // 21500
    
    
  • Behavioral Patterns:

    Behavioral patterns focus on communication between objects, helping to manage interactions and data flow across different parts of a system.

    1. Strategy Pattern:

    The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

    Example (JavaScript Strategy):

    class Payment {
      setStrategy(strategy) {
        this.strategy = strategy;
      }
      pay(amount) {
        return this.strategy.pay(amount);
      }
    }
     
    class CreditCardPayment {
      pay(amount) {
        console.log(`Paid ${amount} using credit card`);
      }
    }
    class PayPalPayment {
      pay(amount) {
        console.log(`Paid ${amount} using PayPal`);
      }
    }
     
    const payment = new Payment();
    payment.setStrategy(new CreditCardPayment());
    payment.pay(100); // Paid 100 using credit card
    payment.setStrategy(new PayPalPayment());

    payment.pay(100); // Paid 100 using PayPal
    
    
    2. Command Pattern:
    
    

    The Command pattern turns a request into an object, allowing parameterization and queuing of requests. It's useful when dealing with operations that require undo/redo capabilities.

    Example (JavaScript Command Pattern):

    class Light {
      turnOn() {
        console.log('Light is ON');
      }
      turnOff() {
        console.log('Light is OFF');
      }
    }
    class LightOnCommand {
      constructor(light) {
        this.light = light;
      }
      execute() {
        this.light.turnOn();
      }
    }
     
    class LightOffCommand {
      constructor(light) {
        this.light = light;
      }
      execute() {
        this.light.turnOff();
      }
    }
     
    class Switch {
      constructor() {
        this.commands = [];
      }
      addCommand(command) {
        this.commands.push(command);
      }
      executeCommands() {
        this.commands.forEach(command => command.execute());
      }
    }
    const light = new Light();
    const switchController = new Switch();
    switchController.addCommand(new LightOnCommand(light));
    switchController.addCommand(new LightOffCommand(light));
    switchController.executeCommands(); 
  • Frontend-Specific Patterns:
    1. 
    Container-Presenter Pattern (React):

    The Container-Presenter pattern divides React components into two types:

    • Container components handle state and business logic.
    • Presenter components are stateless and render the UI.

    Example (React Container-Presenter)

    // Container Component
    class UserContainer extends React.Component {
      state = { user: null };
      componentDidMount() {
        fetch('/api/user')
          .then(res => res.json())
          .then(user => this.setState({ user }));
      }
      render() {
        return <UserPresenter user={this.state.user} />;
      }
    }
    // Presenter Component
    const UserPresenter = ({ user }) => (
      <div>{user ? `Hello, ${user.name}` : 'Loading...'}</div>
    );
    
    

    2. Higher-Order Component (HOC) Pattern (React):

    In React, Higher-Order Components (HOCs) are a pattern for reusing component logic. An HOC is a function that takes a component and returns a new component with added functionality.

    Example (React HOC)

    const withUser = (Component) => {
      return class extends React.Component {
        state = { user: null };
        componentDidMount() {
          fetch('/api/user')
            .then(res => res.json())
            .then(user => this.setState({ user }));
        }
        render() {
          return <Component {...this.props} user={this.state.user} />;
        }
      };
    };
    const UserProfile = ({ user }) => (
      <div>{user ? `Hello, ${user.name}` : 'Loading...'}</div>
    );
    export default withUser(UserProfile);
    
    

    Conclusion:

    Design patterns provide time-tested solutions to common problems in software design. By applying appropriate patterns like Singleton, Observer, Factory, or Strategy, you can improve code readability, maintainability, and scalability in both frontend and backend development. Choosing the right pattern depends on your specific use case and the structure of your application.

No comments:

Post a Comment