Monday 2018-08-27

After the in-character refactoring of the Gilded Rose kata ( GildedWoes ), a "normie" refactoring would likely involve making a class for each product. However, a rules-based refactoring seems cleaner.

Rules-based has two strong advantages over other implementations: 1) since the requirements are also rules-based, verifying them is straightforward, and 2) the previous implementation's variance with requirements is made obvious.

In the typical products-are-objects implementation, Sulfuras is the "normal" object, since the class does nothing. Adding in a rules-based refactoring makes the normal item the normal object -- i.e. something similar to the following:

class Rules(object):
    default_increment = -1
    def ruleSellInDecrement(item):
        item.sell_in -= 1
    def ruleSetIncrement(item):
        item.incr = item.default_increment
    def ruleSellInDoubles(item):
        if item.sell_in < 0:
             item.incr = 2 * item.incr
    def ruleHandleLowOutOfRangeAsPerPreviousImplementation(item):
        if item.quality < 0:
            item.incr = 0
    def ruleHandleHighOutOfRangeAsPerPreviousImplementation(item):
        if item.quality > 50 and item.incr > 0:
            item.incr = 0
    def ruleNeverGrowNegative(item):
        if item.incr < 0 and item.quality + item.incr < 0:
            item.quality = 0
            item.incr = 0
    def ruleNeverGrowOver50(item):
        if item.incr > 0 and item.quality + item.incr > 50:
            item.quality = 50
            item.incr = 0
    def ruleAddIncrement(item):
        item.quality += item.incr
    def ruleBackstageZeroing(item):
        pass
    rules = [ x for x in locals() if x.startswith('rule') ]

class ItemNormal(Item, Rules):
    pass

class ItemConjured(Item, Rules):
    default_increment = 2 * ItemNormal.default_increment

class ItemBrie(Item, Rules):
    default_increment = 1
    def ruleHandleLowOutOfRangeAsPerPreviousImplementation(item):
        pass

class ItemBackstage(Item, Rules):
    def ruleSetIncrement(item):
        item.incr = sum([ item.sell_in >= 0, 0 <= item.sell_in < 5, 0 <= item.sell_in < 10 ])
    def ruleHandleLowOutOfRangeAsPerPreviousImplementation(item):
        pass
    def ruleBackstageZeroing(item):
        if item.sell_in < 0:
            item.quality = 0

class ItemSulfuras(Item, Rules):
    default_increment = 0
    def ruleSellInDecrement(item):
        pass

class GildedRose(object):
    def __init__(self, items):
        objmap = { "Sulfuras, Hand of Ragnaros": ItemSulfuras,
            "Conjured code": ItemConjured,
            "Backstage passes to a TAFKAL80ETC concert": ItemBackstage,
            "Aged Brie": ItemBrie }
        for item in items:
            item.__class__ = objmap[item.name] if item.name in objmap else ItemNormal
        self.items = items
    def update(self):
        for item in self.items:
            for rule in item.rules:
                getattr(item, rule)()
        return self.items
#codekata