Skip to content

Design Pattern

ramyaselvaraj817 edited this page Sep 3, 2023 · 4 revisions

For a JavaScript developer, understanding and applying design patterns can significantly improve the quality and maintainability of your code.

SOLID Principles:

SOLID is an acronym that represents a set of five design principles aimed at creating more maintainable and robust software systems.

  1. S - Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it should have only one responsibility or concern.

  2. O - Open-Closed Principle (OCP): Software entities should be open for extension but closed for modification, allowing new features to be added without altering existing code.

  3. L - Liskov Substitution Principle (LSP): Subtypes (derived classes) must be substitutable for their base types (parent classes) without affecting the program's correctness.

  4. I - Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don't use, and interfaces should be small and specific to their clients' needs.

  5. D - Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules, and both should depend on abstractions, promoting flexibility and easy replacement of dependencies.

Here's a list of must-know design patterns for JavaScript developers:

1. Singleton Pattern:

  • Ensures that a class has only one instance and provides a global point of access to that instance. It's commonly used for managing shared resources.
const Singleton = (function () {
  let instance;

  function createInstance() {
    return { message: "I am the Singleton instance" };
  }

  return {
    getInstance: function () {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();

console.log(singleton1 === singleton2); // true (same instance)

Usecases

  • Caching - Implementing a caching mechanism to store frequently used data can benefit from the Singleton pattern. It ensures that the cache instance is shared across different parts of the application.

  • User Authentication and Session Management - In web applications, a Singleton can be used to manage user authentication, user sessions, and user-related data, ensuring consistent access and state management.

2. Factory Pattern:

  • Defines an interface for creating objects but allows subclasses to alter the type of objects that will be created. It provides a way to create objects without specifying the exact class.

The Factory Pattern is a creational design pattern that provides an interface for creating objects but allows subclasses or concrete implementations to alter the type of objects that will be created. This pattern is useful for creating objects without specifying the exact class of object that needs to be instantiated. Below is a simple JavaScript code example with comments explaining each line:

// Step 1: Create a constructor function for the objects you want to create.
function Product(name, price) {
  this.name = name;
  this.price = price;
}

// Step 2: Define a factory function that will create instances of the objects.
function createProduct(name, price) {
  // Step 3: Inside the factory function, create a new instance of the object.
  const product = new Product(name, price);

  // Step 4: Optionally, you can perform additional operations or set default values here.
  product.category = 'General'; // Adding a default category

  // Step 5: Finally, return the created object instance.
  return product;
}

// Step 6: Use the factory function to create objects.
const product1 = createProduct('Widget', 20);
const product2 = createProduct('Gadget', 30);

// Step 7: You now have two product objects created using the factory function.
console.log(product1); // { name: 'Widget', price: 20, category: 'General' }
console.log(product2); // { name: 'Gadget', price: 30, category: 'General' }

Usecase

UI Component Libraries - In web development, UI component libraries may use factories to create reusable UI elements like buttons, forms, and tables with predefined styles and behavior.

3. Module Pattern:

The Module Pattern is a design pattern in JavaScript that allows you to create encapsulated modules with private variables and functions. It provides a way to organize and structure your code, preventing pollution of the global namespace and enabling data encapsulation. Here's a simple JavaScript code example of the Module Pattern with comments explaining each line, followed by use cases:

// Module definition using an immediately invoked function expression (IIFE).
const MyModule = (function () {
  // Private variables and functions (not accessible from outside).
  let privateVar = 'I am private';

  function privateFunction() {
    console.log('This is private');
  }

  // Public interface (accessible from outside).
  return {
    publicVar: 'I am public',
    publicFunction: function () {
      console.log('This is public');
    },
  };
})();

// Usage of the module's public interface.
console.log(MyModule.publicVar); // Accessing the public variable.
MyModule.publicFunction(); // Calling the public function.

// Attempting to access private members will result in an error.
console.log(MyModule.privateVar); // undefined
MyModule.privateFunction(); // Error: MyModule.privateFunction is not a function

Explanation of each part:

  1. Module Definition (IIFE):

    • The Module Pattern is typically implemented using an immediately invoked function expression (IIFE). This function serves as a closure that encapsulates private variables and functions.
  2. Private Variables and Functions:

    • Inside the closure, you can declare private variables (e.g., privateVar) and functions (e.g., privateFunction). These are not accessible from outside the module and are encapsulated within the closure.
  3. Public Interface:

    • The module exposes a public interface by returning an object containing public variables and functions. These are accessible from outside the module and can be used by client code.
  4. Usage of the Module:

    • Client code can access the module's public members (e.g., publicVar and publicFunction) by using the module's name (MyModule in this example). These members can be used just like any other object properties and methods.
  5. Accessing Private Members:

    • Attempting to access private members (e.g., privateVar and privateFunction) from outside the module will result in undefined or an error. This demonstrates encapsulation and information hiding.

Use Cases for the Module Pattern:

1. Namespace Management:

  • The Module Pattern is often used to create namespaces for organizing and encapsulating code in large applications, preventing naming conflicts between different parts of the application.

2. Data Encapsulation:

  • Private variables and functions can be used to encapsulate data and logic, ensuring that they are not directly accessible or modifiable from outside the module.

3. Event Handling:

  • Modules can be used to encapsulate event handling logic, allowing you to define private event handlers and expose a clean public API for event subscription and triggering.

4. Configuration Management:

  • Configuration settings and constants can be stored as private variables within a module, ensuring that they are not accidentally modified.

The Module Pattern is a powerful tool for organizing and structuring JavaScript code, promoting encapsulation, and improving code maintainability in larger applications.

4. Revealing Module Pattern:

  • An extension of the module pattern that explicitly reveals public methods and properties while keeping others private. Enhances code readability and control over access.

5. Observer Pattern:

  • Defines a one-to-many dependency between objects so that when one object changes state, all its dependents (observers) are notified and updated automatically. Useful for implementing event handling.

The Observer Pattern is a behavioral design pattern in JavaScript that defines a one-to-many relationship between objects. It allows one object (the subject) to notify its observers (subscribers) about state changes, ensuring that the observers are automatically updated when the subject's state changes. Here's a simple JavaScript code example of the Observer Pattern with comments explaining each line, followed by use cases:

// Observer - Represents an observer that listens for updates from the subject.
class Observer {
  constructor(name) {
    this.name = name;
  }

  // Update method called by the subject when a change occurs.
  update(message) {
    console.log(`${this.name} received a message: ${message}`);
  }
}

// Subject - Represents the object being observed.
class Subject {
  constructor() {
    this.observers = [];
  }

  // Add an observer to the list of subscribers.
  addObserver(observer) {
    this.observers.push(observer);
  }

  // Remove an observer from the list of subscribers.
  removeObserver(observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }

  // Notify all observers about a state change.
  notify(message) {
    this.observers.forEach((observer) => observer.update(message));
  }
}

// Create subject and observers.
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

// Subscribe observers to the subject.
subject.addObserver(observer1);
subject.addObserver(observer2);

// Notify observers about a state change.
subject.notify('State has been updated.');

// Output:
// Observer 1 received a message: State has been updated.
// Observer 2 received a message: State has been updated.

Use Cases for the Observer Pattern:

  1. UI Updates in Web Applications:

    • In web applications, the Observer Pattern is used to automatically update user interfaces (UI) when underlying data changes. For example, updating a chart when data is modified.
  2. Event Handling:

    • JavaScript's built-in event system, such as the DOM's event handling, follows the Observer Pattern. Event listeners (observers) are notified when events (state changes) occur.
  3. Publish-Subscribe Systems:

    • Pub-Sub systems utilize the Observer Pattern, where publishers (subjects) notify subscribers (observers) about events or messages.
  4. Model-View-Controller (MVC):

    • In the MVC architectural pattern, the Observer Pattern is used to keep the view synchronized with changes in the model. Views (observers) update when the model (subject) changes.

The Observer Pattern helps achieve loose coupling between objects, promoting maintainability and flexibility in software design. It's particularly useful in scenarios where objects need to react to changes in the state of other objects without explicitly knowing each other.

6. Publish-Subscribe Pattern (PubSub):

  • Decouples components by allowing objects to subscribe and listen for specific events or topics, promoting a more flexible and scalable architecture. Often used in JavaScript frameworks.

The Publish-Subscribe Pattern, also known as the Pub-Sub Pattern, is a behavioral design pattern in JavaScript that facilitates communication and coordination among different parts of an application without requiring them to be tightly coupled. It allows one-to-many or many-to-many relationships between objects, where one object (the publisher or subject) sends messages (events) to multiple objects (subscribers or observers) that are interested in receiving those messages. Here's a simple JavaScript code example of the Publish-Subscribe Pattern with comments explaining each line, followed by use cases

Use Cases for the Publish-Subscribe Pattern:

  1. Notification Systems:
    • Building notification or messaging systems where various users or entities can subscribe to receive updates or alerts.

The Publish-Subscribe Pattern decouples publishers and subscribers, allowing for flexible and loosely coupled communication between different parts of an application. It's a powerful tool for achieving a more modular and maintainable codebase.

7. Prototype Pattern:

  • Creates new objects by copying an existing object (prototype) and then modifying the copy as needed. Useful for efficient object cloning and extending built-in JavaScript objects.

The Prototype Pattern is a creational design pattern in JavaScript that allows you to create objects by cloning an existing object, known as the prototype. This pattern is especially useful when you want to create multiple objects with the same structure without duplicating code. Here's a simple JavaScript code example of the Prototype Pattern with comments explaining each line, followed by use cases:

// Prototype object (constructor function).
function Animal(name, species) {
  this.name = name;
  this.species = species;
}

// Adding a method to the prototype.
Animal.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.name}, a ${this.species}.`);
};

// Create instances (clones) of the prototype.
const cat = new Animal('Whiskers', 'Cat');
const dog = new Animal('Buddy', 'Dog');

// Test the instances.
cat.sayHello(); // Output: Hello, I'm Whiskers, a Cat.
dog.sayHello(); // Output: Hello, I'm Buddy, a Dog.

Use Cases for the Prototype Pattern:

  1. Object Cloning:

    • The primary use case is to create multiple objects with the same structure efficiently by cloning a prototype object. This is especially helpful when objects have complex configurations or require resource-intensive setup.
  2. Factory Methods:

    • Factories that create objects with specific configurations can use prototypes to create instances efficiently.
  3. Prototypal Inheritance:

    • The Prototype Pattern is at the core of JavaScript's prototypal inheritance, where objects inherit properties and methods from other objects.

The Prototype Pattern is foundational in JavaScript and is used throughout the language, including for object creation, inheritance, and method sharing. It provides a flexible way to create and manage objects efficiently.

8. Command Pattern:

  • Encapsulates a request as an object, allowing you to parameterize clients with queues, requests, and operations. Useful for implementing undo/redo functionality.

9. Strategy Pattern:

  • Defines a family of algorithms, encapsulates each one, and makes them interchangeable. It enables you to select an algorithm at runtime, promoting flexibility and extensibility.

10. Decorator Pattern:

- Dynamically adds responsibilities to objects without altering their code. Useful for extending the behavior of objects at runtime.

The Decorator Pattern is a structural design pattern in JavaScript that allows you to add behavior or responsibilities to objects dynamically at runtime, without altering their existing code. It is often used to extend the functionality of objects in a flexible and reusable way. Here's a simple JavaScript code example of the Decorator Pattern with comments explaining each line, followed by use cases:

// Interface representing a basic component.
class Coffee {
  cost() {
    return 5; // Base cost of a plain coffee.
  }

  description() {
    return 'Plain coffee';
  }
}

// Concrete component - PlainCoffee.
class PlainCoffee extends Coffee {}

// Decorator - AdditiveDecorator.
class AdditiveDecorator extends Coffee {
  constructor(baseCoffee) {
    super();
    this.baseCoffee = baseCoffee;
  }

  cost() {
    return this.baseCoffee.cost(); // Delegate cost calculation to the base component.
  }

  description() {
    return this.baseCoffee.description(); // Delegate description to the base component.
  }
}

// Concrete decorator - MilkDecorator.
class MilkDecorator extends AdditiveDecorator {
  cost() {
    return super.cost() + 2; // Add cost of milk.
  }

  description() {
    return `${super.description()} with Milk`; // Append "with Milk" to the description.
  }
}

// Concrete decorator - SugarDecorator.
class SugarDecorator extends AdditiveDecorator {
  cost() {
    return super.cost() + 1; // Add cost of sugar.
  }

  description() {
    return `${super.description()} with Sugar`; // Append "with Sugar" to the description.
  }
}

// Usage example:
const myCoffee = new SugarDecorator(new MilkDecorator(new PlainCoffee()));
console.log(`Cost: $${myCoffee.cost()}`); // Output: Cost: $8
console.log(`Description: ${myCoffee.description()}`); // Output: Description: Plain coffee with Milk with Sugar
  1. Usage Example:
    • In the usage example, a plain coffee is first wrapped with a MilkDecorator and then with a SugarDecorator. The resulting myCoffee object has both milk and sugar added to it.

Use Cases for the Decorator Pattern:

  1. Input Validation:

    • Decorators can be used to validate input data by wrapping classes that accept user input.
  2. Authentication and Authorization:

    • In security-related scenarios, decorators can be used to enforce authentication and authorization checks.

The Decorator Pattern is a powerful way to extend and enhance objects in a flexible and maintainable manner, making it a valuable tool in software design and architecture.

11. Facade Pattern:

Provides a simplified, higher-level interface to a set of interfaces, making it easier to use a complex subsystem or library. Useful for simplifying API interactions.

The Facade Pattern is a structural design pattern in JavaScript that provides a simplified interface to a complex system of objects, making it easier to interact with that system. It acts as a facade or a single entry point to a set of functionalities, hiding the complexity of the underlying components. Here's a simple JavaScript code example of the Facade Pattern with comments explaining each line, followed by use cases:

// Complex subsystem with multiple classes.
class SubsystemA {
  operationA() {
    return 'Subsystem A operation';
  }
}

class SubsystemB {
  operationB() {
    return 'Subsystem B operation';
  }
}

class SubsystemC {
  operationC() {
    return 'Subsystem C operation';
  }
}

// Facade providing a simplified interface to the subsystem.
class Facade {
  constructor() {
    this.subsystemA = new SubsystemA();
    this.subsystemB = new SubsystemB();
    this.subsystemC = new SubsystemC();
  }

  // Simplified methods that orchestrate the subsystem operations.
  operation1() {
    return `${this.subsystemA.operationA()} and ${this.subsystemB.operationB()}`;
  }

  operation2() {
    return `${this.subsystemB.operationB()} and ${this.subsystemC.operationC()}`;
  }
}

// Client code using the Facade.
const facade = new Facade();
const result1 = facade.operation1();
const result2 = facade.operation2();

console.log(result1); // Output: Subsystem A operation and Subsystem B operation
console.log(result2); // Output: Subsystem B operation and Subsystem C operation

Explanation of each part:

  1. Complex Subsystem:

    • The complex subsystem consists of multiple classes (SubsystemA, SubsystemB, and SubsystemC) representing various operations.
  2. Facade Class:

    • The Facade class is created to provide a simplified interface to the subsystem. It aggregates instances of the subsystem classes.
  3. Simplified Methods:

    • The Facade class defines simplified methods (operation1 and operation2) that orchestrate the interactions with the subsystem classes.
  4. Client Code:

    • The client code creates an instance of the Facade and uses it to perform operations without needing to interact directly with the complex subsystem.

Use Cases for the Facade Pattern:

  1. Simplifying Complex APIs:

    • The Facade Pattern is used to simplify complex APIs or systems by providing a more straightforward and user-friendly interface.
  2. Hiding Complexity:

    • It hides the implementation details and complexity of a system from the client code, making it easier to use.
  3. Legacy System Integration:

    • When integrating with legacy systems or third-party librries, a facade can wrap and adapt the complex APIs to provide a more modern and intuitive interface.
  4. Multi-Layered Architectures:

    • In multi-layered software architectures (e.g., front-end and back-end), a facade can serve as the entry point for communication between layers, abstracting away the communication details.
  5. User Interface (UI):

    • In UI development, a facade can simplify interactions with complex user interface components, such as a video player or charting library.

The Facade Pattern is especially useful when dealing with large and complex systems where interactions with the subsystem are convoluted. It promotes encapsulation, separation of concerns, and ease of use, making it a valuable design pattern in software development.

12. Adapter Pattern:

- Allows the interface of an existing class to be used as another interface. It helps in making incompatible interfaces compatible.

The Adapter Pattern is a structural design pattern in JavaScript that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, allowing them to collaborate. This pattern is especially useful when integrating new functionality into existing code or when using third-party libraries with different interfaces.

# Initial Code

class Calculator {
  operation(num1, num2, operation) {
    switch (operation) {
      case 'multiplication':
        return num1 * num2;
      case 'division':
        return num1 / num2;
      default:
        return NaN;
    }
  }
}

export default Calculator;


**Refactor**

lass Calculator {
  add(num1, num2) {
    return num1 + num2;
  }
  div(num1, num2) {
    return num1 / num2;
  }
  mult(num1, num2) {
    return num1 * num2;
  }
}

export default Calculator;


**Adaptor**

import Calculator from "./Calculator";

class CalculatorAdapter {
  constructor() {
    this.calculator = new Calculator();
  }
  operation(num1, num2, operation) {
    switch (operation) {
      case "add":
        return this.calculator.add(num1, num2);
      case "multiplication":
        return this.calculator.mult(num1, num2);
      case "division":
        return this.calculator.div(num1, num2);
      default:
        return NaN;
    }
  }
}

export default CalculatorAdapter;

**Usage**

import Calculator from "./Calculator";
import CalculatorAdapter from "./CalculatorAdapter";

//Adapter
const calcAdapter = new CalculatorAdapter();
const sumAdapter = calcAdapter.operation(2, 2, "multiplication");
console.log(sumAdapter); //output 4

//Calculator
const calculator = new Calculator();
const sum = calculator.mult(2, 2);
console.log(sum); //output 4

13. Composite Pattern:

- Composes objects into tree structures to represent part-whole hierarchies. Useful for treating individual objects and compositions of objects uniformly.

14. State Pattern:

- Allows an object to alter its behavior when its internal state changes. The object will appear to change its class. Useful for managing stateful components.

15. Chain of Responsibility Pattern:

- Passes a request along a chain of handlers, with each handler deciding either to process the request or to pass it to the next handler in the chain. Useful for implementing request handling pipelines.

16. MVC (Model-View-Controller):

Separates an application into three interconnected components: Model (data and business logic), View (presentation layer), and Controller (handles user input and manages the flow). Commonly used in web applications.

he Model-View-Controller (MVC) pattern is a design pattern widely used in web development to separate the concerns of an application into three interconnected components: Model, View, and Controller. It helps maintain clean and organized code, making it easier to manage, test, and scale an application. Here's a simple JavaScript example of the MVC pattern with comments explaining each line, followed by use cases:

MVC Pattern Overview:

Model: Represents the application's data and business logic. View: Represents the user interface and presentation logic. Controller: Acts as an intermediary between the Model and View, handling user input and updating the Model and View accordingly.

Example - TODO

Model (TodoModel):

Manages the application's data (todo items) and business logic. Provides methods to add todos, toggle their completion status, and retrieve all todos.

View (TodoView):

Handles user interface rendering and user input events. Listens for events such as clicking the "Add" button or clicking on todo items. Calls controller methods to update the Model and re-renders the View accordingly.

Controller (TodoController):

Acts as an intermediary between the Model and View. Initializes the Model and View, and connects them. Provides methods that the View can call to interact with the Model.

Initialization:

The application is initialized by creating an instance of the TodoController, which sets up the Model and View.

17. MVVM (Model-View-ViewModel):

- Combines the Model-View-Controller concepts with data binding to separate the UI logic from the presentation logic. Commonly used in frontend frameworks like Angular and Knockout.

18. Dependency Injection (DI):

Allows you to inject dependencies into a component rather than having the component create or manage its dependencies. It promotes modularity and testability.

The Dependency Injection Pattern is a design pattern used in software development to achieve Inversion of Control (IoC). In this pattern, dependencies are "injected" or provided to a class rather than the class creating them itself. This allows for better separation of concerns, testability, and flexibility in the code. Here's a simple JavaScript example of the Dependency Injection Pattern with comments explaining each line, followed by use cases:

Dependency Injection Pattern Overview:

The Dependency Injection Pattern involves passing dependencies (e.g., objects, services, or configurations) into a class rather than having the class create them internally. This helps decouple components and makes it easier to manage and test dependencies.

JavaScript Example with Comments:

Here's a simple JavaScript example of the Dependency Injection Pattern:

// Service class - Represents a database service.
class DatabaseService {
  constructor(connectionString) {
    this.connectionString = connectionString;
  }

  connect() {
    // Simulate connecting to a database.
    console.log(`Connected to database: ${this.connectionString}`);
  }

  disconnect() {
    // Simulate disconnecting from a database.
    console.log('Disconnected from database');
  }
}

// Class that depends on the DatabaseService.
class UserRepository {
  constructor(databaseService) {
    this.databaseService = databaseService;
  }

  getAllUsers() {
    // Use the injected databaseService to fetch users.
    this.databaseService.connect();
    console.log('Fetching users from the database...');
    // Simulate fetching users.
    const users = ['User1', 'User2', 'User3'];
    this.databaseService.disconnect();
    return users;
  }
}

// Create a database service instance.
const dbService = new DatabaseService('mongodb://localhost/mydb');

// Create a user repository instance and inject the database service.
const userRepository = new UserRepository(dbService);

// Fetch and display users.
const users = userRepository.getAllUsers();
console.log('Users:', users);

Explanation of Each Part:

  1. DatabaseService:

    • Represents a database service with methods to connect and disconnect from a database.
    • It accepts a connection string during construction.
  2. UserRepository:

    • Represents a class that depends on the DatabaseService.
    • It is constructed with an instance of DatabaseService, which is injected as a dependency.
    • The getAllUsers method uses the injected DatabaseService to connect, fetch users (simulated), and disconnect from the database.
  3. Dependency Injection:

    • In the client code, a DatabaseService instance (dbService) is created.
    • This instance is injected into the UserRepository when constructing it, ensuring that the UserRepository uses the same DatabaseService.
  4. Usage:

    • The UserRepository can fetch users using the injected DatabaseService.
    • This separation of concerns and injection of dependencies makes it easier to test and switch out dependencies (e.g., for testing or using different databases).

Use Cases for the Dependency Injection Pattern:

  1. Testing:

    • Dependency injection makes it easier to substitute real dependencies with mock or stub objects during testing.
  2. Reusability:

    • Components with injected dependencies can be reused in various contexts.

The Dependency Injection Pattern helps improve code quality, maintainability, and testability by promoting modularity and decoupling. It is widely used in modern software development, especially in frameworks like Angular and libraries like React.

19. Flux Pattern:

- A design pattern used for managing data flow in React applications, involving unidirectional data flow and a unidirectional architecture.

20. Redux Pattern:

A specific implementation of the Flux pattern for state management in JavaScript applications, commonly used with React.

The Redux Pattern is a predictable state container pattern used in JavaScript applications, particularly in the context of front-end libraries and frameworks like React. Redux is designed to manage the state of an application in a predictable and centralized manner. It's especially useful for large-scale applications with complex state management requirements. Here's a simple JavaScript example of the Redux Pattern with comments explaining each line, followed by use cases:

Redux Pattern Overview:

Redux is based on three core principles:

  1. Single Source of Truth: The entire application's state is stored in a single JavaScript object called the "store."

  2. State is Read-Only: The state cannot be modified directly. Instead, you dispatch actions, which are plain JavaScript objects describing what should change.

  3. Changes are Made by Pure Functions: Changes to the state are made by pure functions called "reducers," which take the current state and an action and return a new state.

JavaScript Example with Comments:

Here's a simple JavaScript example of the Redux Pattern:

// Redux Store - Manages the application's state.
function createStore(reducer) {
  let state;
  let listeners = [];

  function getState() {
    return state;
  }

  function dispatch(action) {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  }

  function subscribe(listener) {
    listeners.push(listener);
    return function unsubscribe() {
      listeners = listeners.filter((l) => l !== listener);
    };
  }

  // Initialize the store with the default state.
  dispatch({ type: '@@INIT' });

  return {
    getState,
    dispatch,
    subscribe,
  };
}

// Reducer - Handles state modifications based on actions.
function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

// Create the store with the counterReducer.
const store = createStore(counterReducer);

// Subscribe to changes in the store.
const unsubscribe = store.subscribe(() => {
  console.log('Current State:', store.getState());
});

// Dispatch actions to modify the state.
store.dispatch({ type: 'INCREMENT' }); // Increment the counter.
store.dispatch({ type: 'INCREMENT' }); // Increment the counter again.
store.dispatch({ type: 'DECREMENT' }); // Decrement the counter.

// Unsubscribe from store updates.
unsubscribe();

Use Cases for the Redux Pattern:

  1. Server-Side Rendering (SSR):

    • Redux is commonly used in SSR environments to manage state on both the client and server.
  2. Integration with UI Libraries:

    • Redux integrates well with UI libraries like React, Angular, and Vue.js.

Redux is a powerful state management pattern that helps maintain the sanity and scalability of an application's state. While the example here is simplified, Redux is often used in conjunction with libraries like react-redux to manage the state of React applications effectively.

These design patterns are fundamental to writing well-structured, maintainable, and scalable JavaScript code. Depending on your project and requirements, you may find that certain patterns are more applicable than others, so it's important to choose wisely and apply them where appropriate.

Clone this wiki locally