class Item { name: string; sellIn: number; quality: number; constructor(name: string, sellIn: number, quality: number) { this.name = name; this.sellIn = sellIn; this.quality = quality; } } class ItemWithStrategy extends Item { strategy: UpdateStrategy; /** * Specific strategies for updating the quality and sellIn values of specific items. * If an item does not have a specific strategy, the default strategy is used. * @type {Record} * @see http://documentation.com/guilded-rose#specific-strategies */ //Open/Closed Principle: Adding a new type is as simple as creating a new strategy (like ConjuredStrategy) and mapping it in the strategies object. The GildedRose class remains untouched. private specificStrategies: Record = { 'Aged Brie': new AgedBrieUpdateStrategy(), 'Backstage passes to a TAFKAL80ETC concert': new TAFKAL80ETCConcertUpdateStrategy(), 'Conjured Mana Cake': new ConjuredManaCakeStrategy(), 'Sulfuras, Hand of Ragnaros': new SulfurasUpdateStrategy(), }; constructor(name: string, sellIn: number, quality: number) { super(name, sellIn, quality); // Clear Delegation: If an item’s name isn’t recognized, the system falls back to the default behavior—ensuring that no item is left unhandled. this.strategy = this.specificStrategies[name] || new DefaultUpdateStrategy(); } updateMetadata() { this.strategy.applyForItem(this); } } interface UpdateStrategy { applyForItem: (item: Item) => void; } class AgedBrieUpdateStrategy implements UpdateStrategy { applyForItem(item: Item) { item.sellIn = item.sellIn - 1; if (item.quality < 50) { item.quality = item.quality + 1; if (item.sellIn < 0) { // A good brie gets better with age item.quality = item.quality + 1; } } } } class DefaultUpdateStrategy implements UpdateStrategy { applyForItem(item: Item) { if (item.quality > 0) { item.quality = item.quality - 1; } item.sellIn = item.sellIn - 1; if (item.sellIn < 0) { item.quality = item.quality - item.quality; } } } class TAFKAL80ETCConcertUpdateStrategy implements UpdateStrategy { applyForItem(item: Item) { // Erreur du code initial, < 50 if (item.quality <= 50) { item.quality = Math.min(item.quality + 1, 50); if (item.sellIn < 11) { item.quality = Math.min(item.quality + 1, 50); } item.sellIn = item.sellIn - 1; if (item.sellIn < 0) { item.quality = item.quality - item.quality; } } } } class SulfurasUpdateStrategy implements UpdateStrategy { // Sulfuras is always at 80 quality and is not for sale // NOTE: legacy code had a bug here, sellin could be -1 applyForItem(item: Item) { item.quality = 80; item.sellIn = 0; } } class ConjuredManaCakeStrategy implements UpdateStrategy { applyForItem(item: Item) { const currentQuality = item.quality; const defaultStrategy = new DefaultUpdateStrategy(); defaultStrategy.applyForItem(item); const qualityDelta = currentQuality - item.quality; item.quality = Math.max(0, item.quality - qualityDelta); } } /** * GildedRoseItemUpdater * A class that updates the inn's product metadata, specifically the quality and sellIn values depending * on the item's specificities. */ // Separation of Concerns export class GildedRoseItemUpdater { items: Array; constructor(items = [] as Array) { this.items = items.map( (item) => new ItemWithStrategy(item.name, item.sellIn, item.quality) ); } updateProductsMetadata = () => { this.items.forEach((item) => item.updateMetadata()); }; } const testItems: Item[] = [ new Item('+5 Dexterity Vest', 10, 20), new Item('Aged Brie', 2, 0), new Item('Elixir of the Mongoose', 5, 7), new Item('Sulfuras, Hand of Ragnaros', 0, 80), new Item('Sulfuras, Hand of Ragnaros', -1, 80), new Item('Backstage passes to a TAFKAL80ETC concert', 15, 20), new Item('Backstage passes to a TAFKAL80ETC concert', 10, 49), new Item('Backstage passes to a TAFKAL80ETC concert', 5, 49), // this conjured item does not work properly yet new Item('Conjured Mana Cake', 3, 6), ]; const getAsserter = (item: Item) => (actual: number, expected: number) => { if (actual === expected) { return; } console.error(item); throw new Error(`Expected ${expected}, but got ${actual}`, { cause: item, }); }; const guildedRose = new GildedRoseItemUpdater(testItems); for (let i = 0; i < 10; i++) { guildedRose.updateProductsMetadata(); } guildedRose.items.forEach((item, i) => { const assertEqual = getAsserter(item); switch (item.name) { case '+5 Dexterity Vest': assertEqual(item.quality, 10); assertEqual(item.sellIn, 0); break; case 'Aged Brie': assertEqual(item.quality, 18); assertEqual(item.sellIn, -8); break; case 'Elixir of the Mongoose': assertEqual(item.quality, 0); assertEqual(item.sellIn, -5); break; case 'Sulfuras, Hand of Ragnaros': if (i === 3) { assertEqual(item.quality, 80); assertEqual(item.sellIn, 0); } else { assertEqual(item.quality, 80); assertEqual(item.sellIn, 0); } case 'Backstage passes to a TAFKAL80ETC concert': if (i === 5) { assertEqual(item.quality, 35); assertEqual(item.sellIn, 5); } if (i === 6) { assertEqual(item.quality, 50); assertEqual(item.sellIn, 0); } if (i === 7) { assertEqual(item.quality, 0); assertEqual(item.sellIn, -5); } break; case 'Conjured Mana Cake': assertEqual(item.quality, 0); assertEqual(item.sellIn, -7); break; } });