Back to basic: TDD (3)

Nei primi 2 step abbiamo introdotto una piccola funzionalità che permette di aggiungere un articolo al carrello. Continuiamo sullo stesso tema introducendo la seguente funzionalità:

Quando aggiungo 2 item uguali, deve aumentare la quantità dell’item.

Nello step 2 abbiamo gestito l’aggiunta dello stesso articolo per 2 volte oggi gestiamo la quantità e questo ci porta alla creazione di una nuova classe che rappresenta l’articolo aggiunto. Proviamo a scrivere un test per decidere l’interfaccia che vogliamo usare e come vogliamo operare:

[Fact]
public void AddSameItemTwice_TheItemQuantityShouldBe2()
{
  var cart = new ShoppingCart();
  cart.AddItem(new CartItem(99));
  cart.AddItem(new CartItem(99));
  Assert.Equal(2, cart.Items[0].Quantity);
}

Questa implementazione non mi piace molto perché ci costringe a sapere dell’esistenza della classe CartItem che al momento non serve se non per gestire la quantità. Inoltre è meglio non esporre pubblicamente la lista dei CartItem altrimenti si potrebbero aggiungere item senza passare dal metodo AddItem o si potrebbero fare operazioni non consentite.

Quindi modifico il test per nascondere l’esistenza di CartItem e per esporlo sotto forma di IEnumerable cosi che la collezione non sia modificabile ma sia in sola lettura.

[Fact]
public void AddSameItemTwice_TheItemQuantityShouldBe2()
{
  var cart = new ShoppingCart();
  cart.AddItem(99);
  cart.AddItem(99);
  Assert.Equal(2, cart.Items.ElementAt(0).Quantity);
}

In questo modo la creazione degli oggetti CartItem è demandato all’interno della classe ShoppingCart (che diventerà molto probabilmente l’aggregate root…ne parleremo più avanti). Una possibile implementazione è la seguente:

public class ShoppingCart
{
  private readonly List _items;

  public ShoppingCart()
  {
    _items = new List();
  }

  public IEnumerable Items
  {
    get { return new ReadOnlyCollection(_items); }
  }

  public int ItemCount { get { return _items.Count; } }

  public void AddItem(int itemId)
  {
    CartItem item = _items.SingleOrDefault(i => i.Id == itemId);
    if (item != null) 
    {
      item.Quantity++;
    }
    else
    {
      item = new CartItem(itemId);
      item.Quantity = 1;
      _items.Add(item);
    }
  }
}

public class CartItem
{
  public int Id { get; private set; }
  public int Quantity { get; set; }

  public CartItem(int id)
  {
    Id = id;
  }
}

Con questa implementazione i test diventano tutti verdi. Dedichiamoci al refactoring che questa volta serve. Non mi piace inoltre il fatto di dover impostare nel metodo AddItem il valore di Quantity a 1, quindi sposto la definizione della quatità sull’oggetto CartItem. Non mi piace neppure che Quantity sia esposto in scrittura, quindi aggiungo un metodo AddOne:

public void AddItem(int itemId)
{
  CartItem item = _items.SingleOrDefault(i => i.Id == itemId);
  if (item != null)
  {
    item.AddOne();
  }
  else
  {
    _items.Add(new CartItem(itemId));
  }
}

mentre la class CartItem:

public class CartItem
{
  public int Id { get; private set; }
  public int Quantity { get; private set; }

  public CartItem(int id)
  {
    Id = id;
    Quantity = 1;
  }

  public void AddOne()
  {
    Quantity++;
  }
}

Così le cose sono un po’ meglio anche se, si potrebbe provare ad eliminare l’if nel metodo AddItem, ma per ora ci tappiamo il naso e continuiamo, vediamo cosa succede.