feat: refactor GildedRose with Strategy pattern, ItemRecord, and days injection

This commit is contained in:
Raj Vora 2026-05-05 13:34:40 -07:00
parent 4db0d990a7
commit 81332e2d28
2 changed files with 111 additions and 31 deletions

View File

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol
@ -99,40 +101,57 @@ class ConjuredStrategy:
item.quality = max(0, item.quality - 2)
class GildedRose(object):
_STRATEGY_MAP: dict[str, UpdateStrategy] = {
"Aged Brie": AgedBrieStrategy(),
"Backstage passes to a TAFKAL80ETC concert": BackstagePassStrategy(),
"Sulfuras, Hand of Ragnaros": SulfurasStrategy(),
"Conjured Mana Cake": ConjuredStrategy(),
}
def __init__(self, items):
@dataclass
class ItemRecord:
"""
Internal pairing of an Item with its UpdateStrategy.
Built once in GildedRose.__init__ so the name-to-strategy lookup
happens at construction time, not on every update call. Exposed
publicly so tests can inject strategies directly without going
through the name-lookup dict.
"""
item: Item
strategy: UpdateStrategy
class GildedRose:
"""
Inventory manager for the Gilded Rose inn.
Accepts a list of Item objects and updates their quality and sell_in
values according to each item's type-specific strategy. The Item
class and the items list are left untouched per the goblin's terms.
Usage:
rose = GildedRose(items)
rose.update_quality() # advance one day
rose.update_quality(days=7) # advance a full week
"""
def __init__(self, items: list[Item]) -> None:
self.items = items
self._records: list[ItemRecord] = [
ItemRecord(
item=i,
strategy=_STRATEGY_MAP.get(i.name, NormalStrategy()),
)
for i in items
]
def update_quality(self):
for item in self.items:
if item.name != "Aged Brie" and item.name != "Backstage passes to a TAFKAL80ETC concert":
if item.quality > 0:
if item.name != "Sulfuras, Hand of Ragnaros":
item.quality = item.quality - 1
else:
if item.quality < 50:
item.quality = item.quality + 1
if item.name == "Backstage passes to a TAFKAL80ETC concert":
if item.sell_in < 11:
if item.quality < 50:
item.quality = item.quality + 1
if item.sell_in < 6:
if item.quality < 50:
item.quality = item.quality + 1
if item.name != "Sulfuras, Hand of Ragnaros":
item.sell_in = item.sell_in - 1
if item.sell_in < 0:
if item.name != "Aged Brie":
if item.name != "Backstage passes to a TAFKAL80ETC concert":
if item.quality > 0:
if item.name != "Sulfuras, Hand of Ragnaros":
item.quality = item.quality - 1
else:
item.quality = item.quality - item.quality
else:
if item.quality < 50:
item.quality = item.quality + 1
def update_quality(self, days: int = 1) -> None:
"""Advance quality and sell_in for all items by `days` days."""
for record in self._records:
record.strategy.update(record.item, days)
class Item:

View File

@ -10,6 +10,7 @@ if str(_EXERCISE_ROOT) not in sys.path:
from gilded_rose import (
Item, GildedRose, NormalStrategy, AgedBrieStrategy,
BackstagePassStrategy, SulfurasStrategy, ConjuredStrategy,
ItemRecord,
)
@ -207,5 +208,65 @@ class TestConjuredStrategy(unittest.TestCase):
self.assertEqual(0, item.quality)
class TestGildedRose(unittest.TestCase):
"""
Integration tests for GildedRose.
Verifies strategy dispatch by item name and that the days parameter
is forwarded correctly to each strategy.
"""
def test_dispatches_normal_strategy_for_unknown_item(self):
items = [Item("unknown widget", sell_in=5, quality=10)]
GildedRose(items).update_quality()
self.assertEqual(9, items[0].quality)
self.assertEqual(4, items[0].sell_in)
def test_dispatches_aged_brie_strategy(self):
items = [Item("Aged Brie", sell_in=5, quality=20)]
GildedRose(items).update_quality()
self.assertEqual(21, items[0].quality)
def test_dispatches_backstage_pass_strategy(self):
items = [Item("Backstage passes to a TAFKAL80ETC concert", sell_in=5, quality=20)]
GildedRose(items).update_quality()
self.assertEqual(23, items[0].quality)
def test_dispatches_sulfuras_strategy(self):
items = [Item("Sulfuras, Hand of Ragnaros", sell_in=0, quality=80)]
GildedRose(items).update_quality()
self.assertEqual(80, items[0].quality)
self.assertEqual(0, items[0].sell_in)
def test_dispatches_conjured_strategy(self):
items = [Item("Conjured Mana Cake", sell_in=5, quality=20)]
GildedRose(items).update_quality()
self.assertEqual(18, items[0].quality)
def test_multi_day_update_via_days_param(self):
# sell_in=3, quality=10, days=3
# Day1: q=9 si=2 | Day2: q=8 si=1 | Day3: q=7 si=0
items = [Item("unknown widget", sell_in=3, quality=10)]
GildedRose(items).update_quality(days=3)
self.assertEqual(7, items[0].quality)
self.assertEqual(0, items[0].sell_in)
def test_multiple_items_updated_independently(self):
items = [
Item("Aged Brie", sell_in=5, quality=20),
Item("unknown widget", sell_in=5, quality=20),
]
GildedRose(items).update_quality()
self.assertEqual(21, items[0].quality) # Aged Brie goes up
self.assertEqual(19, items[1].quality) # normal goes down
def test_item_record_allows_direct_strategy_injection(self):
# ItemRecord lets tests bypass name-lookup and inject a strategy directly
item = Item("anything", sell_in=5, quality=20)
record = ItemRecord(item=item, strategy=ConjuredStrategy())
record.strategy.update(record.item, days=1)
self.assertEqual(18, item.quality)
if __name__ == '__main__':
unittest.main()