DEV Community

loading...
Cover image for Master Design Patterns By Building Real Projects - Chain of Responsibility Pattern - Javascript

Master Design Patterns By Building Real Projects - Chain of Responsibility Pattern - Javascript

hieptl profile image Hiep Le Updated on ・5 min read

Behavioral Pattern - Chain of Responsibility

This is the sixth part in my series (23 GoF Design Patterns). My series will help you understand about design patterns by building real projects. For this reason, you can see the places in which each pattern could be applied. I want to focus on learning by doing instead of talking too much about theories.

I'm Hiep. I work as a full-time software engineer. Most of my open-source projects are focused on one thing - to help people learn 📚.

If the repository is useful, please help me share the post and give me a Github ⭐. It will make me feel motivation to work even harder. I will try to make many open sources and share to the community.

I also created some series that help you improve your practical skills.

1. Learn React By Buiding Netflix

https://dev.to/hieptl/learn-react-by-building-netflix-1127

Table of Contents

1. Definition.

Chain of responsibility delegates commands to a chain of processing objects.

2. Scenarios.

We can imagine that we are buliding a promotion engine for Amazon. When the users add products into their cart. The promotion engine will take responsibility to calculate total discount based on the cart's products.

We will build the following rules:

  1. If the number of products > 3. We will discount 50%.
  2. If the total price > 100. We will discount 20%.

In order to implement the feature, we just need to write some if...else statements.

However, promotion programs are unpredictable because they depend on the business. For this reason, we need to find the solution which has ability to scale when Amazon wants to have new promotion programs so that we do not need to write too many if...else statements.

We will implement the feature by using Chain of Responsibility pattern.

3. Building a Promotion Engine for Amazon.

I want to use Javascript class to implement the idea because in the case you are using object-oriented programming languages or you have to use those languages in the future, you can still implement design patterns by using different languages. In my opinion, design patterns are mindset and approaches and not depend on programming languages.

Step 1: Create Cart class. It will be used to store the information of the shopping cart.

class Cart {
  constructor() {
    this.products = []
  }

  addProduct(product) { 
    this.products.push(product);
  }
}
Enter fullscreen mode Exit fullscreen mode

1st NOTE: addProduct will be used to add a new product to the cart. It accept product as parameter. product parameter specifies the product which is belonging to the cart.

Step 2: The first promotion rule need to be implemented. We create NumberDiscount to check about if the number of products > 3. We will discount 50%.

class NumberDiscount { 
  constructor() {
    this.nextPromotion = null;
  }

  setNext(nextPromotion) {
    this.nextPromotion = nextPromotion;
  }

  exec(products) {
    let currentTotalDiscount = 0;
    if (products.length > 2) {
      currentTotalDiscount= 0.5;
    }
    return currentTotalDiscount+ (this.nextPromotion ? this.nextPromotion.exec(products) : 0);
  }
}
Enter fullscreen mode Exit fullscreen mode

2nd NOTE:

  • We are defining Chain of Responsibility. It means that we are defining chain of promotion rules. That's why we have this.next. this.next determines the next promotion rule which would be executed after the current promotion rule.

  • setNext is used to set the next promotion rules by assigning to this.next.

  • exec is used to apply the current promotion rule to list of cart's products. In this case, we are checking the number of products. If it is > 3, we will discount 50%.

  • In fact, we are executing chain of promotion rules. Therefore, we need to save the result at each step. currentTotalDiscount variable help us to achieve that. We will store the discount value of the current promotion and add it to the result of the next promotion. The process will be continue until we do not have any promotion rules to apply.

Step 3: The second promotion rule need to be implemented. We create TotalPriceDiscount to check if the total price > 100. We will discount 20%.

class TotalPriceDiscount {
  constructor() {
    this.nextPromotion = null;
  }

  setNext(nextPromotion) {
    this.nextPromotion = nextPromotion;
  }

  calculateTotalPrice(products) { 
    let totalPrice = 0;
    products.forEach(product => {
      totalPrice = totalPrice + product.price;
    });
    return totalPrice;
  }

  exec(products) {
    let currentTotalDiscount = 0;
    const totalPrice = this.calculateTotalPrice(products);
    if (totalPrice > 100) {
      currentTotalDiscount= 0.2;
    }
    return currentTotalDiscount + (this.nextPromotion ? this.nextPromotion.exec(products) : 0);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: After defining different promotion rules, we need to build our promotion engine and set up chain of promotion rules.

class PromotionEngine { 
  constructor() {
    this.numberDiscount = new NumberDiscount();
    this.priceDiscount = new TotalPriceDiscount();
    this.numberDiscount.setNext(this.priceDiscount);
  }

  execPromotionEngine(products) {
    return this.numberDiscount.exec(products);
  }
}
Enter fullscreen mode Exit fullscreen mode

3rd NOTE:

  • The code above describe the magic of setNext function. We initialize the first promotion rule (numberDiscount) and then we continute to initialize the second promotion rule (priceDiscount).

  • We create a chain of promotion rules by writing this.numberDiscount.setNext(this.priceDiscount) It means that we want to excecute the numberDiscount promotion first and then execute the priceDiscount promotion.

  • The last but not least, execPromotionEngine accepts cart's products as parameter. The products parameter will be passed to the chain of responsibility in order to apply promotion programs.

Out full source code will look like this:

class Cart {
  constructor() {
    this.products = []
  }

  addProduct(product) { 
    this.products.push(product);
  }
}

class NumberDiscount { 
  constructor() {
    this.nextPromotion = null;
  }

  setNext(nextPromotion) {
    this.nextPromotion = nextPromotion;
  }

  exec(products) {
    let currentTotalDiscount = 0;
    if (products.length > 2) {
      currentTotalDiscount = 0.5;
    }
    return currentTotalDiscount + (this.nextPromotion ? this.nextPromotion.exec(products) : 0);
  }
}

class TotalPriceDiscount {
  constructor() {
    this.nextPromotion = null;
  }

  setNext(nextPromotion) {
    this.nextPromotion = nextPromotion;
  }

  calculateTotalPrice(products) { 
    let totalPrice = 0;
    products.forEach(product => {
      totalPrice = totalPrice + product.price;
    });
    return totalPrice;
  }

  exec(products) {
    let currentTotalDiscount = 0;
    const totalPrice = this.calculateTotalPrice(products);
    if (totalPrice > 100) {
      currentTotalDiscount= 0.2;
    }
    return currentTotalDiscount + (this.nextPromotion ? this.nextPromotion.exec(products) : 0);
  }
}

class PromotionEngine { 
  constructor() {
    this.numberDiscount = new NumberDiscount();
    this.priceDiscount = new TotalPriceDiscount();
    this.numberDiscount.setNext(this.priceDiscount);
  }

  execPromotionEngine(products) {
    return this.numberDiscount.exec(products);
  }
}

const cart = new Cart();
cart.addProduct({id: 1, name: 'Product A', price: 100});
cart.addProduct({id: 2, name: 'Product B', price: 50});
cart.addProduct({id: 3, name: 'Product C', price: 30});
cart.addProduct({id: 4, name: 'Product D', price: 20});
cart.addProduct({id: 5, name: 'Product E', price: 200});

const promotionEngine = new PromotionEngine();
const totalDiscount = promotionEngine.execPromotionEngine(cart.products);

console.log(`total discount: ${totalDiscount}`);
Enter fullscreen mode Exit fullscreen mode

4. Result.

total discount: 0.7
Enter fullscreen mode Exit fullscreen mode

The above result describes that the total discount value was calculated by applying Chain of Responsibility pattern.

By using design patterns, we can understand the core concepts and make our code become more readable and cleaner. I hope that the post can help you understand about Chain of Responsibility pattern.

Thanks and Best Regards,
Hiep.

Discussion (1)

Collapse
michelledai2020 profile image
Michelle Dai

Thank you for taking the time to write these articles. They are really good and well thought-out. Keep them coming!

Forem Open with the Forem app