DEV Community

Cover image for Software Design Patterns: The Secret Sauce Behind Every Great App
Suryansh Singh
Suryansh Singh Subscriber

Posted on

Software Design Patterns: The Secret Sauce Behind Every Great App

Imagine building a LEGO set without a manual. Sure, you might end up with something that vaguely resembles a spaceship, but more likely, you’ll end up with a pile of colorful chaos. In the world of coding, design patterns are like that LEGO manual—they give you a proven blueprint to solve common problems, making your code cleaner, more efficient, and easier to maintain.

But don’t worry, this isn’t a dry lecture on coding principles. Think of it as a fun little guide to some of the most common design patterns in JavaScript, complete with real-life analogies that make these concepts easy to digest.

1. The Singleton Pattern: The One and Only

What is it?

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. It’s like having a single remote control for your TV. You don’t need multiple remotes to control the volume, change the channel, or turn it off—you just need that one remote.

Real-Life Use Case:

In JavaScript, this pattern is commonly used for managing global application state. For instance, if you have a shopping cart on an e-commerce website, you want all the components that interact with the cart (like adding items, removing items, or checking out) to refer to the same instance of the cart. A Singleton ensures that all these actions affect the same cart, not different copies of it.

class ShoppingCart {
  constructor() {
    if (!ShoppingCart.instance) {
      this.items = [];
      ShoppingCart.instance = this;
    }
    return ShoppingCart.instance;
  }

  addItem(item) {
    this.items.push(item);
  }

  getItems() {
    return this.items;
  }
}

const cart1 = new ShoppingCart();
const cart2 = new ShoppingCart();

cart1.addItem('Laptop');
console.log(cart2.getItems()); // ['Laptop']
Enter fullscreen mode Exit fullscreen mode

2. The Observer Pattern: The Gossip Network

What is it?

The Observer pattern is all about keeping objects in sync. When one object changes, the others need to know about it. It’s like a group chat where everyone is constantly updated on what’s happening. If someone changes the plan for the weekend, everyone in the group gets the memo.

Real-Life Use Case:

In JavaScript, this pattern is often used in event handling systems. Let’s say you’re building a social media app. When someone likes a post, you want to update the like count, notify the post’s author, and maybe trigger an animation. The Observer pattern allows these different components to stay updated without being directly connected.

class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log(`Observer received: ${data}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('New post liked!'); 
// Output: Observer received: New post liked!
// Output: Observer received: New post liked!
Enter fullscreen mode Exit fullscreen mode

3. The Factory Pattern: The Cookie Cutter

What is it?

The Factory pattern is all about creating objects without specifying the exact class of object that will be created. Imagine a cookie cutter. You don’t know exactly what shape the dough will take until you press the cutter down, but you know you’ll get a cookie.

Real-Life Use Case:

This pattern is particularly useful when you have a collection of objects that share a common interface or structure but have different underlying implementations. For example, in a game, you might have different types of enemies (zombies, vampires, werewolves), but they all share common properties like health, speed, and attack power. A Factory can help you create these enemies without worrying about the specific type.

class Enemy {
  constructor(type) {
    this.type = type;
    this.health = 100;
  }

  attack() {
    console.log(`${this.type} attacks with ${this.attackPower}`);
  }
}

class EnemyFactory {
  createEnemy(type) {
    let enemy;

    if (type === 'zombie') {
      enemy = new Enemy('Zombie');
      enemy.attackPower = 10;
    } else if (type === 'vampire') {
      enemy = new Enemy('Vampire');
      enemy.attackPower = 20;
    } else if (type === 'werewolf') {
      enemy = new Enemy('Werewolf');
      enemy.attackPower = 30;
    }

    return enemy;
  }
}

const factory = new EnemyFactory();

const zombie = factory.createEnemy('zombie');
zombie.attack(); // Zombie attacks with 10

const vampire = factory.createEnemy('vampire');
vampire.attack(); // Vampire attacks with 20
Enter fullscreen mode Exit fullscreen mode

4. The Module Pattern: The Organized Workspace

What is it?

The Module pattern is like having a well-organized workspace where everything has its place. It helps you keep different parts of your code organized and encapsulated, preventing the global namespace from becoming a cluttered mess.

Real-Life Use Case:

This pattern is especially useful in JavaScript for creating public and private variables and functions. For example, when you’re building a library or plugin, you might want to expose certain methods to the outside world while keeping others hidden. The Module pattern allows you to do just that.

const Calculator = (function() {
  let result = 0;

  function add(x) {
    result += x;
    return result;
  }

  function subtract(x) {
    result -= x;
    return result;
  }

  function multiply(x) {
    result *= x;
    return result;
  }

  function divide(x) {
    if (x !== 0) {
      result /= x;
      return result;
    } else {
      console.error('Cannot divide by zero');
    }
  }

  return {
    add,
    subtract,
    multiply,
    divide,
    getResult: () => result,
  };
})();

console.log(Calculator.add(10)); // 10
console.log(Calculator.subtract(2)); // 8
console.log(Calculator.multiply(3)); // 24
console.log(Calculator.divide(4)); // 6
Enter fullscreen mode Exit fullscreen mode

5. The Adapter Pattern: The Plug Converter

What is it?

The Adapter pattern allows incompatible interfaces to work together. Think of it like a plug adapter you use when traveling abroad. Your laptop charger’s plug might not fit into the wall socket in another country, but with the right adapter, it works perfectly.

Real-Life Use Case:

In JavaScript, you might use the Adapter pattern when you’re integrating with third-party libraries or APIs that don’t exactly match the structure of your application. The Adapter can transform the interface of a class into another interface that clients expect, making the integration seamless.

class OldApi {
  constructor() {
    this.data = 'Old API data';
  }

  getData() {
    return this.data;
  }
}

class NewApi {
  fetchData() {
    return 'New API data';
  }
}

class ApiAdapter {
  constructor(oldApi) {
    this.oldApi = oldApi;
  }

  fetchData() {
    return this.oldApi.getData();
  }
}

const oldApi = new OldApi();
const adapter = new ApiAdapter(oldApi);

console.log(adapter.fetchData()); // 'Old API data'

Enter fullscreen mode Exit fullscreen mode

6. The Composite Pattern: The Russian Nesting Dolls

What is it?

The Composite pattern allows you to treat individual objects and compositions of objects uniformly. It’s like Russian nesting dolls where each doll is part of a larger structure, but you can interact with them both as individual dolls and as a nested set.

Real-Life Use Case:

This pattern is often used in scenarios where you need to manage a hierarchy of objects. For example, consider a file system where files and folders are represented as objects. A folder can contain multiple files or even other folders, and you want to treat both files and folders similarly when it comes to operations like moving, copying, or deleting.

class File {
  constructor(name) {
    this.name = name;
  }

  display() {
    console.log(this.name);
  }
}

class Folder {
  constructor(name) {
    this.name = name;
    this.children = [];
  }

  add(child) {
    this.children.push(child);
  }

  display() {
    console.log(this.name);
    this.children.forEach(child => child.display());
  }
}

const file1 = new File('file1.txt');
const file2 = new File('file2.txt');
const folder = new Folder('MyFolder');
folder.add(file1);
folder.add(file2);

folder.display();
// Output:
// MyFolder
// file1.txt
// file2.txt
Enter fullscreen mode Exit fullscreen mode

Conclusion

These are just a few of the many design patterns that can make your JavaScript code more robust, maintainable, and, let’s face it, fun to write. Whether you’re building the next big app or just trying to organize your code a bit better, these patterns can be your secret weapon. So next time you’re stuck, remember: there’s probably a pattern for that!

And hey, even if your code doesn’t end up looking like a LEGO spaceship, at least it won’t be a pile of colorful chaos.

Happy coding! 🚀

Top comments (0)