Gilded Rose Kata and Strategy Pattern

JavaScript Algorithm

I recently came across the Gilded Rose kata problem from this talk Thank God it’s not LeetCode: Interviewing in the age of AI by Luigi Taira D, who discussed how his company is changing the interview style in this AI era. Instead of using Leetcode style problems, they ask candidates to refactor a block of code. As an example, he used the Gilded Rose Problem.

The Gilded Rose is a piece of code which needs to be refactored to make it more readable and scalable. The requirement can be found in the github repo-

gilded rose requirements
Here is the original code that needs to be refactored- gilded rose requirements

As you can see, the code has a lot of nested ifs, making it unreadable. Also, adding a new item is difficult and would make code more complex. The task here is to add new item and refactor this nested ifs.

The first step of refactoring is to understand the requirement clearly.

sell In >= 0 sell In < 0
Normal Item -1 -2
Aged Brie +1 +2
Sulfuras 80 80
Backstage passes +3 => sell In <= 5
+2 => sell In <= 10
+1 => sell In > 10
0
Conjured -2 -4

Also quality can never be more than 50(except for Sulfuras which is always constant) or less than 0.

Since there are different condition for some item, we can encapsulate those Item in it's own class and control it's behavior. We can do this by following a design called Strategy Pattern. In simple words, Strategy pattern is like having different "plans" that can be swapped in and out depending on the situation.

There are three main core components of a strategy pattern-

  • Strategy Interface - Defines the common method(s) all strategies must implement
  • Concrete Strategies - Different implementations of the strategy interface
  • Context - The class that uses a strategy
// Strategy Interface
class ItemStrategy {
  updateQuality(item) {
    throw new Error('Must implement updateQuality')
  }
}

// Concrete Strategy
class NormalItemStrategy extends ItemStrategy {
  updateQuality(item) {
    // algorithm for normal item
  }
}

// Concrete Strategy
class AgedBrieStrategy extends ItemStrategy {
  updateQuality(item) {
    // algorithm for AgedBrew
  }
}

// Concrete Strategy
class BackstagePassStrategy extends ItemStrategy {
  updateQuality(item) {
    // algorithm for Backstage
  }
}

// Concrete Strategy
class ConjuredStrategy extends ItemStrategy {
  updateQuality(item) {
    // algoritm for Conjured item
  }
}

// Concrete Strategy
class SulfurasStrategy extends ItemStrategy {
  updateQuality(item) {
    // algorithm for Sulfus
  }
}

The Shop class is the context because it can assign specific strategy-

// context
class Shop {
  constructor(items = []) {
    this.items = items
  }
  updateQuality() {
    for (const item of this.items) {
      const strategy = ItemStrategyFactory.getStrategy(item.name) // call specific strategy
      strategy.updateQuality(item)
    }
    return this.items
  }
}

Now let's write the algorithm for each item-

class NormalItemStrategy extends ItemStrategy {
  updateQuality(item) {
    item.sellIn -= 1
    const qualityDrop = item.sellIn < 0 ? 2 : 1
    item.quality = Math.max(0, item.quality - qualityDrop)
  }
}
class AgedBrieStrategy extends ItemStrategy {
  updateQuality(item) {
    item.sellIn -= 1
    const qualityIncreaseBy = item.sellIn < 0 ? 2 : 1
    item.quality = Math.min(50, item.quality + qualityIncreaseBy)
  }
}
class BackstagePassStrategy extends ItemStrategy {
  updateQuality(item) {
    item.sellIn -= 1
    const sellIn = item.sellIn
    let amount = 1
    if (sellIn < 0) amount = quality
    else if (sellIn <= 5) amount = 3
    else if (sellIn <= 10) amount = 2
    item.quality = Math.min(50, item.quality + amount)
  }
}
class ConjuredStrategy extends ItemStrategy {
  updateQuality(item) {
    item.sellIn -= 1
    const qualityDropBy = item.sellIn < 0 ? 2 : 4
    item.quality = Math.max(0, item.quality - qualityDropBy)
  }
}
class SulfurasStrategy extends ItemStrategy {
  updateQuality(item) {
    item.quality = 80 // does not change
  }
}

As you can see, we are writing Math.max or Math.min everytime and this kind of causing a repetition. We can write some helper methods to simplify this-

class ItemStrategy {
  updateQuality(item) {
    throw new Error('Must implement updateQuality')
  }
  // helper methods
  adjustQuality(quality, amount, sign) {
    if (sign === '+') {
      return Math.min(50, quality + amount)
    }
    return Math.max(0, quality - amount)
  }
  // common degradation rate
  getDegradationRate(sellIn, baseRate = 1) {
    return sellIn < 0 ? baseRate * 2 : baseRate
  }
  updateSellIn(item) {
    if (item.name !== 'Sulfuras, Hand of Ragnaros') {
      item.sellIn -= 1
    }
  }
}
// now the strategies become more cleaner
class NormalItemStrategy extends ItemStrategy {
  updateQuality(item) {
    this.updateSellIn(item)
    const qualityDropBy = this.getDegradationRate(item.sellIn)
    item.quality = this.adjustQuality(item.quality, qualityDropBy, '-')
  }
}
class BackstagePassStrategy extends ItemStrategy {
  updateQuality(item) {
    this.updateSellIn(item)
    const sellIn = item.sellIn
    let amount = 1
    if (sellIn < 0) amount = quality
    else if (sellIn <= 5) amount = 3
    else if (sellIn <= 10) amount = 2
    item.quality = this.adjustQuality(item.quality, amount, '+')
  }
}
// rest strategies

Then we can write ItemStrategyFactory class that would return specific strategy and use it inside Shop-

class ItemStrategyFactory {
  static strategies = {
    'Sulfuras, Hand of Ragnaros': SulfurasStrategy,
    'Aged Brie': AgedBrieStrategy,
    'Backstage passes to a TAFKAL80ETC concert': BackstagePassStrategy,
  }
  
  static getStrategy(itemName) {
    if (this.strategies[itemName]) {
      return new this.strategies[itemName]()
    }
    // check for Conjured item
    if (itemName.startsWith('Conjured')) {
      return new ConjuredStrategy()
    }
    // default
    return new NormalItemStrategy()
  }
}
class Shop {
  constructor(items = []) {
    this.items = items
  }
  updateQuality() {
    for (const item of this.items) {
      const strategy = ItemStrategyFactory.getStrategy(item.name) // call specific strategy
      strategy.updateQuality(item)
    }
    return this.items
  }
}

Following Strategy Pattern we may have written more code, but there are some benefits-

  • Now it's easier to add new item and new algorithm
  • Each strategy can be tested in isolation
  • Shop has become scalable

But it can also have some disadvantages like-

  • Overkill for simple cases
  • More Classes to manage

Overall, using the Strategy pattern helped to refactor the Gilded Rose into a more readable and scalable code.