There's a special sort of satisfication I get from turning absolutely awful code into a well-tested, well-factored program. Not only have I increased the amount of good code in a codebase, I've gotten rid of bad stuff.
You can view the README for the full story, but the basic premise is you are taking over the maintenance of an inventory system for the Gilded Rose Inn, and you are faced with the simple task of adding a new item type to the inventory. Items have three attributes - name, sell-in (the amount of time before the item expires), and quality.
The inventory system has one function, which updates the quality of the inventory. This will adjust the items' sell-in and quality attributes according to certain item types.
The rules of the program are non-trivial, but also straightforward to grasp. The code you've inherited, however, is not. It is terrible. Here's an example of the Ruby code:
1 for i in 0..(@items.size-1) 2 if (@items[i].name != "Aged Brie" && @items[i].name != "Backstage passes to a TAFKAL80ETC concert") 3 if (@items[i].quality > 0) 4 if (@items[i].name != "Sulfuras, Hand of Ragnaros") 5 @items[i].quality = @items[i].quality - 1 6 end 7 end 8 else 9 if (@items[i].quality < 50) 10 @items[i].quality = @items[i].quality + 1 11 if (@items[i].name == "Backstage passes to a TAFKAL80ETC concert") 12 if (@items[i].sell_in < 11) 13 if (@items[i].quality < 50) 14 @items[i].quality = @items[i].quality + 1 15 end 16 end 17 ...
For loops, nested ifs, magic strings - it's got it all, and it is a bear to untangle.
The problem in front of you is that in order to add the new item, you need to make sure the rest of the update process still works. My typical approach is to start writing high level acceptance tests that exercise the business logic laid out in the README, then move on to refactoring in order to make it easier to add the new item. It can take a while to get through, from one to three hours depending on your familiarity with the problem.
The Kata in Clojure
I've been working with Clojure lately, and although writing little new projects is fun, I wanted to work on a problem like the Gilded Rose kata. Since things don't exist unless you can find them in 15 seconds worth of Googling, I decided to write my own version in Clojure, which you can find here:
For those of you familiar with the Ruby version, there are some slight differences in the bad implementation that are not present. For example - the Ruby version has an Item class with attr_readers for name, sell_in, and quality, and a limitation is that you cannot modify the Item class. However, in Clojure I am just using a map to represent this data. Adding any functionality around the item data is really up to the user as long as the data structure doesn't change.
Another wonderful feature of the Ruby version is the hideous mutation of item objects through the changing of sell_in and quality values inside of a single method. In Clojure this is difficult to replicate due to immutability of data structures, but I think the end result is obfuscated enough to provide a challenge.
I rewrote this kata in Clojure as way to learn the language and learn the testing strategies that will be helpful in other projects. What I also learned again is the need for refactoring is constant. Knowing that I would ugly up the code after I was done, I held off on refactoring when I would have normally stopped to clean up between tests. When I brought fellow craftsman Patrick Gombert in, I realized that I had let the code rot over the course of a few hours to the point that it was starting to get unreadable to a person new to the project.
That is one of the major points of the Gilded Rose kata - without refactoring, a simple set of rules can become so obtuse that it is near impossible to add minor features without breaking existing functionality. The skill of refactoring is in high demand, and this kata works those muscles.
I also learned that once you have a good test suite, it is really fun to defactor code. For probably the only time ever, I was able to increase duplication and inline functions and feel good about it. If you're feeling devious, give it a try with some well tested code and see how quickly it gets hairy (just git reset after you're done).
I encourage anyone looking to practice their refactoring skills and/or Clojure to give it a try, and let me know what your solution looks like!
Spoiler: if you are interested in the solution Patrick and I came up with, check out this gist.