This blog entry is the second part of the GildedRose kata that I had taken up several months ago. It’s a great kata to help study and consider clean design principles; it can be found here. Whatever your level you will definitely get something out of it. I previously did a first attempt at this kata, and made a blog entry on it.

Funnily enough, this kata is all about dealing with legacy code. In a blog entry to follow this one, I’m going to give a quick introduction to a tool that can help us understand spaghetti code (Flog).

Looking at the initial iteration of code I had written for this kata, I understood that the tests I had written were not ideal. Mainly because the instantiation of special objects was actually made from within the test; this is great if we want to unit test the classes of these special objects, but not ideal if we actually want to think about what the GildedRose class is actually useful for.

How we left things previously.

All special items are attributed a class of their own, which inherits from the Item class, although it also has new functionality.

Here is a reminder of what we got to:

class GildedRose
include UpdateOperators

  def initialize(items)
    @items = items
  end

  attr_accessor :items

  def update_quality
    items.each do |item|
      item.respond_to?(:update_self) ? item.update_self : default_update(item)
    end
  end
end

# /*/ Parent class

class Item
  attr_accessor :name, :sell_in, :quality

  def initialize(name, sell_in, quality)
    @name = name
    @sell_in = sell_in
    @quality = quality
  end

  def to_s()
    "#{@name}, #{@sell_in}, #{@quality}"
  end
end

# /*/ Subclasses

class Sulfuras < Item

  def quality
    @quality = 80
  end

  def update_self
    self.quality
    self.sell_in
  end
end

class BackstagePass < Item
  include UpdateOperators

  def update_self

    if self.sell_in <= 0
      self.quality = 0
    elsif self.sell_in > 10
      increment_quality
      decrease_sell_in
    elsif self.sell_in <= 5 
      increment_quality(3)
      decrease_sell_in
    elsif (self.sell_in <= 10)
      increment_quality(2)
      decrease_sell_in
    end
  end
end

class Brie < Item
  include UpdateOperators

  def update_self
    if still_sellable(self)
      increment_quality
      decrease_sell_in
    else
      increment_quality(2) and decrease_sell_in unless max_quality
    end
  end
end

All special items hold an instance method for updating themselves. It means that when an item is of type Brie, it knows how to process its own update, as that functionality is an intrinsic part of its identity.

As a reminder, we are not allowed to touch the Item class, which looks like this:

class Item
  attr_accessor :name, :sell_in, :quality

  def initialize(name, sell_in, quality)
    @name = name
    @sell_in = sell_in
    @quality = quality
  end

  def to_s()
    "#{@name}, #{@sell_in}, #{@quality}"
  end
end

And here is an example of tests for a special item:


# Normal item test example:

it "does not change the name" do
  items = [Item.new("foo", 0, 0)]
  GildedRose.new(items).update_quality()
  expect(items[0].name).to eq "foo"
end

# Special item test example:
it "Brie increases in quality as sell-by-date approaches" do

   items = [Brie.new("Aged Brie", 8, 1)]

   expect(items[0].quality).to eq 1
   expect(items[0].sell_in).to eq 8
   
   GildedRose.new(items).update_quality()
   expect(items[0].quality).to eq 2
   expect(items[0].sell_in).to eq 7

   GildedRose.new(items).update_quality()
   expect(items[0].sell_in).to eq 6
   expect(items[0].quality).to eq 3
end

As unit tests, these work well, we know we can create an instance of Brie and that this Brie object will have the necessary behaviour which knows how to update itself. Though in order for the GildedRose class to actually be worth existing, it should have a special function.

Changing the tests

What the class should really do is process an item that is inputted, and determine which special item class should be called to apply the relevant update method. The objects inputted into the GildedRose class will all be of type Item, but from these items, the GildedRose should be able to tell us what particular type of item we’re dealing with (this make sense in regards the fact that Item is the parent class, and all special items inheriting children of Item). GildedRose is a factory of objects, it receives items, but can then differentiate between different special items. We should therefore have a dictionary/catalogue of sorts, wherein we can store our special items. We’ll see why this is a good idea momentarily.

Changing the tests, we get all of the failures we expect:

it "Brie increases in quality as sell-by-date approaches" do

   items = [Item.new("Aged Brie", 8, 1)]

   expect(items[0].quality).to eq 1
   expect(items[0].sell_in).to eq 8
   
   GildedRose.new(items).update_quality()
   expect(items[0].quality).to eq 2
   expect(items[0].sell_in).to eq 7

   GildedRose.new(items).update_quality()
   expect(items[0].sell_in).to eq 6
   expect(items[0].quality).to eq 3
end

I personally like this approach of considering that everything that passes through is an Item first, and then potentially a special item if it’s found within the dictionary, as it really demonstrates well the hierarchy of inheritance: Item first (parent class), then possibly a special item (child class). The behaviour that we are expecting here of the Item object is not behaviour that we have programmed into the Item class (remember, we’re not allowed to change any code inside that class). What we can do, therefore is check the name of the inputted item, and see whether it matches any names belonging to special items. To fast forward I’m skipping test, and putting the special items straight into a hash.

The problem with the previous iteration of this problem is principally with the tests, in that we are creating an instance of the special objects from within the tests, which is counter to what we should actually be doing.

It also make us consider whether the GildedRose class actually has any particular usage, since all of the logic for updating quality of special items is given to the special classes themselves.

The answer to this is to consider the GildedRose as a factory of objects. When an item, or multiple items, pass through the GildedRose, this “processing” class should decide, or designate which TYPE of item they are: an item is either a special item, or a default item. In this way, we can figure that the GildedRose class is actually a factory for items, and it should be able to recognise an object that is passed in and call it along with its behaviours….

At this point, we already know that we have multiple special items, so why not put them inside a dictionary or catalogue of sorts. The immediate benefit of this is that if down the line we were to have a new special item to consider, we can simply add the item into this dictionary without modifying any of the business logic immediately inside of the code.

Therein, the responsibility of the GildedRose is only to consider incoming objects and check whether they are “special items”, against the catalogue. This is the open/close principle, that says it’s best to “arrange code so that adding new behaviour does not require that you edit existing code.”

Before doing this, we make use of the tests and replace all instantiation of special items with Item, ensuring that all items, aswell as having the characteristics of special items are also items by default. The special item classes will allow us to make the special update functionality.

But the items are only ever inputted as default items.

Switching the functionality of the GildedRose class

After changing the tests and watching them fail, we can refactor the code of the GildedRose from this:

class GildedRose

include UpdateOperators

  def initialize(items)
    @items = items
  end

  attr_accessor :items

  def update_quality
    items.each do |item|
      item.respond_to?(:update_self) ? item.update_self : default_update(item)
    end
  end

end

to this:

class GildedRose
include UpdateOperators

  ITEM_CLASSES = {
    "Aged Brie" => Brie,
    "Sulfuras, Hand of Ragnaros" => Sulfuras,
    "Backstage passes to a TAFKAL80ETC concert" => BackstagePass,
    "Conjured Item" => ConjuredItem,
  }

  def initialize(items)
    @items = items
  end

  attr_accessor :items

  def update_quality
    @items.each do |item|
      special_item = ITEM_CLASSES[item.name]&.new(item.name, item.sell_in, item.quality)
      if special_item
        special_item.update_self
        item.quality = special_item.quality
        item.sell_in = special_item.sell_in
      else
        default_update(item)
      end
    end
  end

end

It looks longer, but now the class does what it should actually be doing, checking the incoming items against a catalogue that can easily be updated.

You may notice that within the conditional of the method we assign the “item.quality” and “item.sell_in” to that we get from the special item, this is because we are actually considering every inputted object to first be an item, and potentially a special item also. We could even make an extra abstraction by taking the asignement of the special item values in the update_quality method aimed at overwriting the Item values, like this:

  def update_quality
    @items.each do |item|
      special_item = ITEM_CLASSES[item.name]&.new(item.name, item.sell_in, item.quality)
      if special_item
        special_item.update_self
        
        item.quality = special_item.quality
        item.sell_in = special_item.sell_in
      else
        default_update(item)
      end
    end
  end

to this:

def synthesize_values_of(item, special_item) 
  item.quality = special_item.quality
  item.sell_in = special_item.sell_in
end

def update_quality
  @items.each do |item|
      special_item = ITEM_CLASSES[item.name]&.new(item.name, item.sell_in, item.quality)
    if special_item
      special_item.update_self
      synthesize_values_of(item, special_item)
    else
      default_update(item)
    end
  end
end

We can then hide away the synthesize_values_of method into theUpdateOperators module.

Limitations

It might be an idea to unpack some of the things inside UpdateOperators module, as currently it seems to be getting pretty cumbersome. But for now I think this is a pretty satisfactory solution, though I will be curious to check in again in several months, to perhaps see if I a new iteration could be even better.

The final result of the code is over here.