close
close

first Drop

Com TW NOw News 2024

Cook Your Code: JavaScript Design Patterns
news

Cook Your Code: JavaScript Design Patterns

Picture this: You’re standing in the kitchen, ready to cook a tasty meal. You have all the ingredients ready, but you’re missing a recipe to follow. You start experimenting, but soon you feel overwhelmed. You add too much salt to one dish, and burn another. Without a clear plan, cooking becomes a mess of guesswork.

Creating software can feel a lot like that. You have all the tools and know-how, but adding new features can become a frustrating puzzle without a well-organized approach. Do you understand what your code needs to do, but are you also working on the best way to make it all work together? That’s where it gets complicated. One small mistake and you end up in a hole full of bugs and confusing code.

Meet design patterns: the tried-and-true recipes that programmers have passed down through the ages. These reusable solutions help you tackle the hard parts of creating software without breaking a sweat. We’ll cover what design patterns are, how they can make your life as a programmer easier, and why they’re the key to building robust, maintainable apps. To make things even more interesting, we’ll use cooking terminology throughout our explanation. Because let’s face it, who doesn’t love a good cooking show?

So, what is a design pattern? How are they going to help us build better apps?

A design pattern is a reusable solution template that you can apply to recurring problems and themes in software design. It will be a good cookbook of proven solutions from experienced developers working on common problems in software design. The guidelines say that with design patterns we can achieve maintainable and reusable code in our apps.

Design patterns are essentially classified into three broad categories depending on the problem they solve: creative design patterns, structural design patterns, and behavioral design patterns.

Design patterns are divided into three categories based on the problem they solve. They are creational design patterns, structural design patterns, and behavioral design patterns.

Creative Design Patterns: The Ingredients and Preparation

Creational design patterns provide mechanisms for creating objects. In the context of a cooking program, these patterns are like gathering and preparing ingredients for cooking. Some patterns that fall under this category are Constructor, Factory, Abstract, Prototype, Singleton, and Builder. Take a look at the three examples below to get a better understanding.

1. Loner

Imagine there is a family secret sauce that can only be made in a special pot, passed down from generation to generation. Of course, the sauce can’t taste the same if the pot is different. This is roughly what Singleton does: a design pattern where a class is limited to a single instance.

class SecretSauce {
  constructor() {
    if (SecretSauce.instance) {
      return SecretSauce.instance;
    }
    SecretSauce.instance = this;
    this.flavor="Umami";
  }

  getFlavor() {
    return this.flavor;
  }
}

const sauce1 = new SecretSauce();
const sauce2 = new SecretSauce();

console.log(sauce1.getFlavor() === sauce2.getFlavor()); // true

Go to full screen mode

Exit full screen

2. Factory method

The Factory Method provides a generic interface for creating objects, allowing us to specify the type of object we want. In our cooking show, the recipe book is the factory. Depending on the type of dish you want to make, it will give you the recipe—object—you need.

// Product Classes
class Pizza {
    constructor(size, toppings) {
      this.size = size;
      this.toppings = toppings;
    }

    prepare() {
      console.log(`Preparing a ${this.size} pizza with ${this.toppings.join(', ')} toppings.`);
    }
  }

  class Pasta {
    constructor(sauce, noodles) {
      this.sauce = sauce;
      this.noodles = noodles;
    }

    prepare() {
      console.log(`Preparing pasta with ${this.noodles} noodles and ${this.sauce} sauce.`);
    }
  }

  // Creator Class
  class RecipeBook {
    createDish(type, options) {
      let dish;

      if (type === 'Pizza') {
        dish = new Pizza(options.size, options.toppings);
      } else if (type === 'Pasta') {
        dish = new Pasta(options.sauce, options.noodles);
      }

      return dish;
    }
  }

  // Usage
  const recipeBook = new RecipeBook();

  const pizzaOptions = {
    size: 'large',
    toppings: ('cheese', 'pepperoni', 'olives')
  };

  const pastaOptions = {
    sauce: 'alfredo',
    noodles: 'fettuccine'
  };

  const pizza = recipeBook.createDish('Pizza', pizzaOptions);
  const pasta = recipeBook.createDish('Pasta', pastaOptions);

  pizza.prepare(); // Preparing a large pizza with cheese, pepperoni, olives toppings.
  pasta.prepare(); // Preparing pasta with fettuccine noodles and alfredo sauce.


Go to full screen mode

Exit full screen

The factory method is useful in scenarios where complex objects are created, for example when generating different instances depending on the environment or when managing many similar objects.

*3. Abstract factory *

It summarizes the implementation details of the general use of the Objects. The best way to explain this is if you consider a meal kit delivery service: whether you cook Italian, Chinese or Mexican, this service delivers everything with ingredients and recipes, tailored only to the cuisine at hand, so that everything fits perfectly.

// Abstract Factory Interfaces
class ItalianKitchen {
    createPizza(options) {
      return new Pizza(options.size, options.toppings);
    }

    createPasta(options) {
      return new Pasta(options.sauce, options.noodles);
    }
  }

  class MexicanKitchen {
    createTaco(options) {
      return new Taco(options.shellType, options.fillings);
    }

    createBurrito(options) {
      return new Burrito(options.size, options.fillings);
    }
  }

  // Concrete Product Classes
  class Pizza {
    constructor(size, toppings) {
      this.size = size;
      this.toppings = toppings;
    }

    prepare() {
      console.log(`Preparing a ${this.size} pizza with ${this.toppings.join(', ')} toppings.`);
    }
  }

  class Pasta {
    constructor(sauce, noodles) {
      this.sauce = sauce;
      this.noodles = noodles;
    }

    prepare() {
      console.log(`Preparing pasta with ${this.noodles} noodles and ${this.sauce} sauce.`);
    }
  }

  class Taco {
    constructor(shellType, fillings) {
      this.shellType = shellType;
      this.fillings = fillings;
    }

    prepare() {
      console.log(`Preparing a taco with a ${this.shellType} shell and ${this.fillings.join(', ')} fillings.`);
    }
  }

  class Burrito {
    constructor(size, fillings) {
      this.size = size;
      this.fillings = fillings;
    }

    prepare() {
      console.log(`Preparing a ${this.size} burrito with ${this.fillings.join(', ')} fillings.`);
    }
  }

  // Client Code
  const italianKitchen = new ItalianKitchen();
  const mexicanKitchen = new MexicanKitchen();

  const italianPizza = italianKitchen.createPizza({
    size: 'medium',
    toppings: ('mozzarella', 'tomato', 'basil')
  });

  const mexicanTaco = mexicanKitchen.createTaco({
    shellType: 'hard',
    fillings: ('beef', 'lettuce', 'cheese')
  });

  italianPizza.prepare(); // Preparing a medium pizza with mozzarella, tomato, basil toppings.
  mexicanTaco.prepare(); // Preparing a taco with a hard shell and beef, lettuce, cheese fillings.


Go to full screen mode

Exit full screen

Structural Design Patterns: Cooking Techniques and Tools

Structural design patterns focus on the composition of objects and identify simple ways to create relationships between different objects. They help ensure that when one part of a system changes, the overall structure remains stable. In cooking, these patterns represent the techniques and tools we use to combine ingredients into a harmonious and delicious dish.

Patterns that fall under this category include Decorator, Facade, Flyweight, Adapter, and Proxy.

1. The facade pattern

The Facade pattern provides a convenient, high-level interface to a more complex code body, effectively hiding the underlying complexity. Imagine a sous chef who simplifies complex tasks for the head chef. The sous chef gathers ingredients, preps them, and organizes everything so the head chef can focus on the finishing touches.

// Complex Subsystem
class IngredientPrep {
    chop(ingredient) {
      console.log(`Chopping ${ingredient}.`);
    }

    measure(amount, ingredient) {
      console.log(`Measuring ${amount} of ${ingredient}.`);
    }
  }

  class CookingProcess {
    boil(waterAmount) {
      console.log(`Boiling ${waterAmount} of water.`);
    }

    bake(temp, duration) {
      console.log(`Baking at ${temp} degrees for ${duration} minutes.`);
    }
  }

  class Plating {
    arrangeDish(dish) {
      console.log(`Arranging the ${dish} on the plate.`);
    }

    garnish(garnish) {
      console.log(`Adding ${garnish} as garnish.`);
    }
  }

  // Facade Class
  class SousChef {
    constructor() {
      this.ingredientPrep = new IngredientPrep();
      this.cookingProcess = new CookingProcess();
      this.plating = new Plating();
    }

    prepareDish(dishName) {
      console.log(`Starting to prepare ${dishName}...`);
      this.ingredientPrep.chop('vegetables');
      this.ingredientPrep.measure('2 cups', 'flour');
      this.cookingProcess.boil('1 liter');
      this.cookingProcess.bake(180, 30);
      this.plating.arrangeDish(dishName);
      this.plating.garnish('parsley');
      console.log(`${dishName} is ready!`);
    }
  }

  // Client Code
  const sousChef = new SousChef();
  sousChef.prepareDish('Lasagna');
  // Output:
  // Starting to prepare Lasagna...
  // Chopping vegetables.
  // Measuring 2 cups of flour.
  // Boiling 1 liter of water.
  // Baking at 180 degrees for 30 minutes.
  // Arranging the Lasagna on the plate.
  // Adding parsley as garnish.
  // Lasagna is ready!
Go to full screen mode

Exit full screen

2. Decorator

The Decorator pattern is used to modify existing systems by adding functionality to objects without significantly changing the underlying code. If our applications require many different types of objects, this pattern is ideal. For example, when making coffee, we start with a basic cup and then dynamically add ingredients such as milk, sugar, or whipped cream. With the Decorator pattern, we can add the basic coffee without changing the basic recipe.

// Base Component
class Coffee {
    constructor() {
      this.description = 'Basic Coffee';
    }

    getDescription() {
      return this.description;
    }

    cost() {
      return 2; // Base cost for a simple coffee
    }
  }

  // Decorator Class
  class CoffeeDecorator {
    constructor(coffee) {
      this.coffee = coffee;
    }

    getDescription() {
      return this.coffee.getDescription();
    }

    cost() {
      return this.coffee.cost();
    }
  }

  // Concrete Decorators
  class Milk extends CoffeeDecorator {
    constructor(coffee) {
      super(coffee);
    }

    getDescription() {
      return `${this.coffee.getDescription()}, Milk`;
    }

    cost() {
      return this.coffee.cost() + 0.5;
    }
  }

  class Sugar extends CoffeeDecorator {
    constructor(coffee) {
      super(coffee);
    }

    getDescription() {
      return `${this.coffee.getDescription()}, Sugar`;
    }

    cost() {
      return this.coffee.cost() + 0.2;
    }
  }

  class WhippedCream extends CoffeeDecorator {
    constructor(coffee) {
      super(coffee);
    }

    getDescription() {
      return `${this.coffee.getDescription()}, Whipped Cream`;
    }

    cost() {
      return this.coffee.cost() + 0.7;
    }
  }

  // Client Code
  let myCoffee = new Coffee();
  console.log(`${myCoffee.getDescription()} costs $${myCoffee.cost()}`); // Basic Coffee costs $2

  myCoffee = new Milk(myCoffee);
  console.log(`${myCoffee.getDescription()} costs $${myCoffee.cost()}`); // Basic Coffee, Milk costs $2.5

  myCoffee = new Sugar(myCoffee);
  console.log(`${myCoffee.getDescription()} costs $${myCoffee.cost()}`); // Basic Coffee, Milk, Sugar costs $2.7

  myCoffee = new WhippedCream(myCoffee);
  console.log(`${myCoffee.getDescription()} costs $${myCoffee.cost()}`); // Basic Coffee, Milk, Sugar, Whipped Cream costs $3.4

Go to full screen mode

Exit full screen

3. Flyweight

The Flyweight pattern is a classic structural solution for optimizing repetitive, slow, inefficient code that shares data. It aims to minimize memory usage in an application by sharing as much data as possible with related objects. Consider common ingredients like salt, pepper, and olive oil that are used in many dishes. Instead of having separate instances of these ingredients for each dish, they are shared across dishes to save resources. For example, you put salt on fried chicken and beef stew from the same pot.

// Flyweight Class
class Ingredient {
    constructor(name) {
      this.name = name;
    }

    use() {
      console.log(`Using ${this.name}.`);
    }
  }

  // Flyweight Factory
  class IngredientFactory {
    constructor() {
      this.ingredients = {};
    }

    getIngredient(name) {
      if (!this.ingredients(name)) {
        this.ingredients(name) = new Ingredient(name);
      }
      return this.ingredients(name);
    }

    getTotalIngredientsMade() {
      return Object.keys(this.ingredients).length;
    }
  }

  // Client Code
  const ingredientFactory = new IngredientFactory();

  const salt1 = ingredientFactory.getIngredient('Salt');
  const salt2 = ingredientFactory.getIngredient('Salt');
  const pepper = ingredientFactory.getIngredient('Pepper');

  salt1.use(); // Using Salt.
  salt2.use(); // Using Salt.
  pepper.use(); // Using Pepper.

  console.log(ingredientFactory.getTotalIngredientsMade()); // 2, Salt and Pepper were created only once
  console.log(salt1 === salt2); // true, Salt is reused


Go to full screen mode

Exit full screen

Behavioral Design Patterns: The Cooking Process and Interaction

Behavioral patterns focus on improving or streamlining communication between different objects in a system. They identify common communication patterns between objects and provide solutions that distribute the responsibility for communication across different objects, increasing the flexibility of communication. In a cooking show, behavioral design patterns are the way we cook the dish, the cooking process, and how different parts of the kitchen interact with each other to create the final dish. Some of the behavior patterns are Iterator, Mediator, Observer, and Visitor.

1.Observer

The Observer pattern is used to notify components of state changes. When a subject needs to notify observers of a change, a notification is sent. If an observer no longer wants to receive updates, he or she can be removed from the observer list. For example, once the chef finishes preparing a dish, all assistant chefs need to be notified to begin their tasks, such as plating or garnishing. The Observer pattern allows multiple chefs (observers) to be notified when the chef (subject) completes a dish.

// Subject Class
class HeadChef {
  constructor() {
    this.chefs = ();
    this.dishReady = false;
  }

  addObserver(chef) {
    this.chefs.push(chef);
  }

  removeObserver(chef) {
    this.chefs = this.chefs.filter(c => c !== chef);
  }

  notifyObservers() {
    if (this.dishReady) {
      this.chefs.forEach(chef => chef.update(this.dishName));
    }
  }

  prepareDish(dishName) {
    this.dishName = dishName;
    console.log(`HeadChef: Preparing ${dishName}...`);
    this.dishReady = true;
    this.notifyObservers();
  }
}

// Observer Class
class Chef {
  constructor(name) {
    this.name = name;
  }

  update(dishName) {
    console.log(`${this.name}: Received notification - ${dishName} is ready!`);
  }
}

// Client Code
const headChef = new HeadChef();

const chef1 = new Chef('Chef A');
const chef2 = new Chef('Chef B');

headChef.addObserver(chef1);
headChef.addObserver(chef2);

headChef.prepareDish('Beef Wellington');
// Output:
// HeadChef: Preparing Beef Wellington...
// Chef A: Received notification - Beef Wellington is ready!
// Chef B: Received notification - Beef Wellington is ready!

Go to full screen mode

Exit full screen

2. Mediator

The Mediator pattern allows a single object to control communication between multiple other objects when an event occurs. Although similar to the Observer pattern, the key difference is that the Mediator controls communication between objects rather than just passing along changes. For example, consider our kitchen with its grill, bakery, and garnish station sections. A kitchen coordinator (mediator) controls communication so that all preparations are done on time.

// Mediator Class
class KitchenCoordinator {
  notify(sender, event) {
    if (event === 'dishPrepared') {
      console.log(`Coordinator: Notifying all stations that ${sender.dishName} is ready.`);
    } else if (event === 'orderReceived') {
      console.log(`Coordinator: Received order for ${sender.dishName}, notifying preparation stations.`);
    }
  }
}

// Colleague Classes
class GrillStation {
  constructor(coordinator) {
    this.coordinator = coordinator;
  }

  prepareDish(dishName) {
    this.dishName = dishName;
    console.log(`GrillStation: Grilling ${dishName}.`);
    this.coordinator.notify(this, 'dishPrepared');
  }
}

class BakeryStation {
  constructor(coordinator) {
    this.coordinator = coordinator;
  }

  bakeDish(dishName) {
    this.dishName = dishName;
    console.log(`BakeryStation: Baking ${dishName}.`);
    this.coordinator.notify(this, 'dishPrepared');
  }
}

// Client Code
const coordinator = new KitchenCoordinator();
const grillStation = new GrillStation(coordinator);
const bakeryStation = new BakeryStation(coordinator);

grillStation.prepareDish('Steak');
// Output:
// GrillStation: Grilling Steak.
// Coordinator: Notifying all stations that Steak is ready.

bakeryStation.bakeDish('Bread');
// Output:
// BakeryStation: Baking Bread.
// Coordinator: Notifying all stations that Bread is ready.


Go to full screen mode

Exit full screen

3. Command

The Command design pattern is an Object Behavioral Pattern that encapsulates the invocation of methods, requests, or operations in a single object and allows both parameterization and pass method calls that can be performed at will. For example, consider how Chef issues the command below.

// Command Interface
class Command {
  execute() {}
}

// Concrete Commands
class GrillCommand extends Command {
  constructor(grillStation, dishName) {
    super();
    this.grillStation = grillStation;
    this.dishName = dishName;
  }

  execute() {
    this.grillStation.grill(this.dishName);
  }
}

class BakeCommand extends Command {
  constructor(bakeryStation, dishName) {
    super();
    this.bakeryStation = bakeryStation;
    this.dishName = dishName;
  }

  execute() {
    this.bakeryStation.bake(this.dishName);
  }
}

// Receiver Classes
class GrillStation {
  grill(dishName) {
    console.log(`GrillStation: Grilling ${dishName}.`);
  }
}

class BakeryStation {
  bake(dishName) {
    console.log(`BakeryStation: Baking ${dishName}.`);
  }
}

// Invoker Class
class HeadChef {
  setCommand(command) {
    this.command = command;
  }

  executeCommand() {
    this.command.execute();
  }
}

// Client Code
const grillStation = new GrillStation();
const bakeryStation = new BakeryStation();

const grillCommand = new GrillCommand(grillStation, 'Steak');
const bakeCommand = new BakeCommand(bakeryStation, 'Bread');

const headChef = new HeadChef();

headChef.setCommand(grillCommand);
headChef.executeCommand(); // GrillStation: Grilling Steak.

headChef.setCommand(bakeCommand);
headChef.executeCommand(); // BakeryStation: Baking Bread.

Go to full screen mode

Exit full screen

Behavioral patterns can look similar, so let’s highlight the differences:

  • Observer: When a chef prepares a dish, several other chefs are notified.

  • Mediator: A coordinator works in the kitchen and facilitates communication between the different departments in the kitchen.

  • Assignment: The chef gives commands to grill or bake dishes and summarizes these actions in objects.

Design patterns provide a straightforward way to solve common problems in software development, just as a tidy kitchen and smart cooking techniques lead to a good meal. By getting and using these patterns, you’ll simplify your coding and help your apps perform better and grow more. It doesn’t matter whether you’re new to coding or have been doing it for a long time – think of design patterns as trusted recipes that have been passed down by many programmers over the years. Try them out, play with them, and you’ll soon find that building strong apps becomes as natural as following a recipe you love. Happy coding!