DEV Community

koushikmaya
koushikmaya

Posted on

# Week 06 Task 01: Design Patterns in JavaScript

Introduction

This week, I learned how Design Patterns can help organize JavaScript applications and make code easier to maintain, reuse, and extend.

For Task 01, I implemented three commonly used design patterns:

  1. Observer Pattern
  2. Singleton Pattern
  3. Factory Pattern

Instead of only learning the theory, I implemented each pattern using a small practical example and tested the implementations using Node.js's built-in assert module.


What Are Design Patterns?

Design patterns are reusable solutions to common software design problems.

They are not ready-made code that we copy into every project. Instead, they provide a proven structure for solving certain types of problems.

For example:

  • Need one object to notify multiple objects? → Observer
  • Need only one shared instance of something? → Singleton
  • Need to create different types of objects without exposing the creation logic? → Factory

1. Observer Pattern

What is the Observer Pattern?

The Observer Pattern is useful when one object needs to notify multiple other objects whenever something happens.

A simple real-world example is YouTube subscriptions.

When a channel uploads a new video:

YouTube Channel
      |
      | notification
      ↓
Subscribers
   ↓    ↓    ↓
User1 User2 User3
Enter fullscreen mode Exit fullscreen mode

The channel doesn't need to manually manage what every subscriber does. It simply sends an event, and the subscribers respond.


My Implementation

I created a small event emitter:

class MiniEventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }

    this.events[event].push(listener);
  }

  off(event, listener) {
    if (!this.events[event]) return;

    this.events[event] = this.events[event].filter(
      (item) => item !== listener
    );
  }

  emit(event, data) {
    if (!this.events[event]) return;

    this.events[event].forEach((listener) => {
      listener(data);
    });
  }

  once(event, listener) {
    const wrapper = (data) => {
      listener(data);
      this.off(event, wrapper);
    };

    this.on(event, wrapper);
  }
}
Enter fullscreen mode Exit fullscreen mode

The class supports:

  • on() → subscribe to an event
  • off() → unsubscribe
  • emit() → trigger an event
  • once() → execute a listener only once

Example

const emitter = new MiniEventEmitter();

emitter.on("login", (user) => {
  console.log(`${user} logged in`);
});

emitter.emit("login", "Koushik");
Enter fullscreen mode Exit fullscreen mode

Output:

Koushik logged in
Enter fullscreen mode Exit fullscreen mode

Why is this useful?

This pattern is useful when different parts of an application need to react to the same event.

Examples include:

  • Notifications
  • UI events
  • Logging
  • Message systems
  • Real-time applications

2. Singleton Pattern

What is the Singleton Pattern?

The Singleton Pattern ensures that a class has only one instance throughout the application.

For example, an application configuration manager usually doesn't need multiple instances.

Instead of:

Config 1
Config 2
Config 3
Enter fullscreen mode Exit fullscreen mode

we want:

        Config
          ↑
     ┌────┼────┐
     ↓    ↓    ↓
   App  Service API
Enter fullscreen mode Exit fullscreen mode

Everyone uses the same configuration object.


My Implementation

I created a singleton configuration manager:

class ConfigManager {
  constructor() {
    if (ConfigManager.instance) {
      return ConfigManager.instance;
    }

    this.config = {};

    ConfigManager.instance = this;
  }

  set(key, value) {
    this.config[key] = value;
  }

  get(key) {
    return this.config[key];
  }
}

module.exports = new ConfigManager();
Enter fullscreen mode Exit fullscreen mode

Now whenever the module is imported, the same instance is reused.

Example

const config = require("./singleton-config");

config.set("environment", "development");

console.log(config.get("environment"));
Enter fullscreen mode Exit fullscreen mode

Output:

development
Enter fullscreen mode Exit fullscreen mode

Why is this useful?

Singletons can be useful for shared resources such as:

  • Configuration
  • Logging
  • Application state
  • Connection managers
  • Caches

However, Singleton should not be used everywhere because excessive global shared state can make applications harder to test and maintain.


3. Factory Pattern

What is the Factory Pattern?

The Factory Pattern is used when we want to create different types of objects without putting the object creation logic everywhere in the application.

For example, imagine a notification system.

We may have:

Notification Factory
       |
 ┌─────┼─────┐
 ↓     ↓     ↓
Email  SMS   Push
Enter fullscreen mode Exit fullscreen mode

The application only needs to tell the factory which notification it wants.


My Implementation

I created notification classes:

class EmailNotification {
  send(message) {
    return `Email: ${message}`;
  }
}

class SMSNotification {
  send(message) {
    return `SMS: ${message}`;
  }
}

class PushNotification {
  send(message) {
    return `Push: ${message}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then I created a factory:

class NotificationFactory {
  static create(type) {
    switch (type) {
      case "email":
        return new EmailNotification();

      case "sms":
        return new SMSNotification();

      case "push":
        return new PushNotification();

      default:
        throw new Error("Unsupported notification type");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the application can simply do:

const notification =
  NotificationFactory.create("email");

console.log(notification.send("Hello!"));
Enter fullscreen mode Exit fullscreen mode

Output:

Email: Hello!
Enter fullscreen mode Exit fullscreen mode

The calling code doesn't need to know how the EmailNotification object is created.


Testing

For testing, I used Node.js's built-in assert module.

Example:

const assert = require("assert");

assert.strictEqual(
  notification.send("Hello!"),
  "Email: Hello!"
);
Enter fullscreen mode Exit fullscreen mode

I tested the important behaviors of each pattern.

Observer tests

I verified:

  • Listeners can subscribe.
  • Events are emitted correctly.
  • Listeners can unsubscribe.
  • once() listeners execute only once.

Singleton tests

I verified that:

const config1 = require("./singleton-config");
const config2 = require("./singleton-config");
Enter fullscreen mode Exit fullscreen mode

refer to the same instance.

Factory tests

I verified that the factory creates the correct notification type and rejects unsupported types.


Project Structure

The final Task 01 structure was:

week-06/
└── Task-01/
    ├── README.md
    ├── mini-event-emitter.js
    ├── singleton-config.js
    ├── notification-factory.js
    └── test.js
Enter fullscreen mode Exit fullscreen mode

Each file had a specific responsibility:

File Purpose
mini-event-emitter.js Observer Pattern
singleton-config.js Singleton Pattern
notification-factory.js Factory Pattern
test.js Tests
README.md Documentation

What I Learned

This task helped me understand that design patterns are mainly about structuring code to solve recurring problems.

Observer

I learned how objects can communicate through events without being tightly coupled.

Singleton

I learned how to maintain one shared instance when multiple parts of an application need the same resource.

Factory

I learned how to separate object creation from the code that uses those objects.


Key Takeaways

The main concepts I learned from this task are:

Observer  →  Event-based communication

Singleton →  One shared instance

Factory   →  Centralized object creation
Enter fullscreen mode Exit fullscreen mode

These patterns are especially useful when applications become larger and different parts of the system need clear responsibilities.


Conclusion

Week 06 Task 01 gave me practical experience with three important JavaScript design patterns: Observer, Singleton, and Factory.

Instead of just reading about these patterns, I implemented each one from scratch and wrote tests to verify their behavior.

This exercise helped me understand how design patterns can improve code organization, reusability, maintainability, and separation of responsibilities.

I’ll be building on these concepts in the next tasks by exploring more design patterns and applying them to practical problems.

Top comments (0)