refactor: add IItemUpdater strategies and ItemUpdaterFactory

Introduce the Strategy + Factory design: IItemUpdater (Name + Update),
an ItemUpdaterBase with the single shared [0,50] clamp helper, stateless
strategies for normal/Aged Brie/Sulfuras/Backstage, and a factory with
two-tier resolution (exact name via case-insensitive dictionary, then
Conjured prefix rule, then the default fallback).

Not wired into GildedRose yet; no behaviour change. The Conjured
strategy itself arrives in the feature commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Artur Goloyad 2026-07-25 20:50:18 +02:00
parent a0066f58cd
commit 8965637f18
8 changed files with 142 additions and 0 deletions

View File

@ -0,0 +1,16 @@
namespace GildedRoseKata;
public class AgedBrieUpdater : ItemUpdaterBase
{
public override string Name => "Aged Brie";
public override void Update(Item item)
{
item.Quality = ClampQuality(item.Quality + 1);
item.SellIn -= 1;
if (item.SellIn < 0)
{
item.Quality = ClampQuality(item.Quality + 1);
}
}
}

View File

@ -0,0 +1,17 @@
namespace GildedRoseKata;
public class BackstagePassUpdater : ItemUpdaterBase
{
public override string Name => "Backstage passes to a TAFKAL80ETC concert";
public override void Update(Item item)
{
var increase = item.SellIn <= 5 ? 3 : item.SellIn <= 10 ? 2 : 1;
item.Quality = ClampQuality(item.Quality + increase);
item.SellIn -= 1;
if (item.SellIn < 0)
{
item.Quality = 0;
}
}
}

View File

@ -0,0 +1,10 @@
namespace GildedRoseKata;
public interface IItemUpdater
{
/// <summary>The exact item name this strategy handles ("default" for the fallback).</summary>
string Name { get; }
/// <summary>Applies one full day: quality change + SellIn change + clamping.</summary>
void Update(Item item);
}

View File

@ -0,0 +1,6 @@
namespace GildedRoseKata;
public interface IItemUpdaterFactory
{
IItemUpdater Get(Item item);
}

View File

@ -0,0 +1,15 @@
using System;
namespace GildedRoseKata;
public abstract class ItemUpdaterBase : IItemUpdater
{
public const int MinQuality = 0;
public const int MaxQuality = 50;
public abstract string Name { get; }
public abstract void Update(Item item);
protected static int ClampQuality(int quality) => Math.Clamp(quality, MinQuality, MaxQuality);
}

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
namespace GildedRoseKata;
public class ItemUpdaterFactory : IItemUpdaterFactory
{
public const string ConjuredPrefix = "Conjured";
private const string DefaultName = "default";
private readonly Dictionary<string, IItemUpdater> _updatersByName;
private readonly IItemUpdater _fallback;
public ItemUpdaterFactory(IEnumerable<IItemUpdater> updaters)
{
_updatersByName = new Dictionary<string, IItemUpdater>(StringComparer.OrdinalIgnoreCase);
foreach (var updater in updaters)
{
_updatersByName[updater.Name] = updater;
}
if (!_updatersByName.TryGetValue(DefaultName, out _fallback))
{
throw new ArgumentException($"A \"{DefaultName}\" fallback updater is required.", nameof(updaters));
}
}
public static ItemUpdaterFactory Default() => new(new IItemUpdater[]
{
new NormalItemUpdater(),
new AgedBrieUpdater(),
new SulfurasUpdater(),
new BackstagePassUpdater(),
});
public IItemUpdater Get(Item item)
{
if (_updatersByName.TryGetValue(item.Name, out var updater))
{
return updater;
}
if (item.Name.StartsWith(ConjuredPrefix, StringComparison.OrdinalIgnoreCase) &&
_updatersByName.TryGetValue(ConjuredPrefix, out var conjured))
{
return conjured;
}
return _fallback;
}
}

View File

@ -0,0 +1,16 @@
namespace GildedRoseKata;
public class NormalItemUpdater : ItemUpdaterBase
{
public override string Name => "default";
public override void Update(Item item)
{
item.Quality = ClampQuality(item.Quality - 1);
item.SellIn -= 1;
if (item.SellIn < 0)
{
item.Quality = ClampQuality(item.Quality - 1);
}
}
}

View File

@ -0,0 +1,11 @@
namespace GildedRoseKata;
public class SulfurasUpdater : ItemUpdaterBase
{
public override string Name => "Sulfuras, Hand of Ragnaros";
public override void Update(Item item)
{
// Legendary item: Quality stays at 80 and SellIn never changes.
}
}