ShoppingCart クラスの作成

勝田 均

課題

注文入力システムで使うクラスShoppingCartを作ります。このクラスは次の3つのメソッドを持つことが求められています。

それでは実装を始めてみましょう。

テスト成功(OK)
テスト失敗(Failure)
エラー(Error)

テストクラス作成


まずテストクラスを作るところから始めます。
ShoppingCartTest.rb(新規作成)
require 'test/unit'

class ShoppingCartTest < Test::Unit::TestCase
end

ShoppingCart.rb(新規作成)
def ShoppingCart
end

新規作成


最初のテストはインスタンスを作るテストです。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  def test_brand_new_cart
    assert(false, "no implementation")
  end
end

初期状態を確かめます。
ShoppingCartTest.rb
require ShoppingCart'

class ShoppingCartTest < Test::Unit::TestCase
  def test_brand_new_cart
    assert(false, "no implementation")
    cart = ShoppingCart.new
    assert_equal(0, cart.item_count)
  end
end

仮実装で切り抜けます。
ShoppingCart.rb
class ShoppingCart
  def item_count
    return 0  #仮実装
  end
end

add_items


add_itemsのテストを書きます。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...

  def test_add_items
    assert(false, "no implementation")
  end
end

一番実装が簡単そうな、0個のItemを追加するテストです。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_add_items
    assert(false, "no implementation")
    cart = ShoppingCart.new
    cart.add_items("item", 0)
    assert_equal(0, cart.item_count)
  end
end

ここはadd_itemsを定義するだけで切り抜けられます。
ShoppingCart.rb
class ShoppingCart
  def item_count
    return 0  #仮実装
  end

  def add_items(item, quantity)
  end
end

test_brand_new_cartをtest_add_itemsにマージしてしまいます。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  def test_brand_new_cart
    cart = ShoppingCart.new
    assert_equal(0, cart.item_count)
  end

  def test_add_items
    cart = ShoppingCart.new
    assert_equal(0, cart.item_count)

    cart.add_items("item", 0)
    assert_equal(0, cart.item_count)
  end
end

いよいよ、1個追加するテストに挑戦です。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  def test_add_items
    cart = ShoppingCart.new
    assert_equal(0, cart.item_count)

    cart.add_items("item", 0)
    assert_equal(0, cart.item_count)

    cart.add_items("item", 1)
    assert_equal(1, cart.item_count)
  end
end

countをインスタンス変数で覚えて、インクリメントすればいいでしょう。
ShoppingCart.rb
class ShoppingCart
  def initialze
    @item_count = 0
  end

  def item_count
    return 0  #仮実装
    return @item_count
  end

  def add_items(item, quantity)
    @item_count += 1
  end
end

今度は2個追加してみます。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  def test_add_items
    cart = ShoppingCart.new
    assert_equal(0, cart.item_count)

    cart.add_items("item", 0)
    assert_equal(0, cart.item_count)

    cart.add_items("item", 1)
    assert_equal(1, cart.item_count)

    cart.add_items("item", 2)
    assert_equal(3, cart.item_count)
  end
end

quantityを足しこむようにします。
ShoppingCart.rb
class ShoppingCart
  ...
  def add_items(item, quantity)
    @item_count += 1
    @item_count += quantity
  end
end

quantityが負なら例外


ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_add_negative_count_items
    assert(false, "no implementation")
  end
end

例外が発生することを確かめるテストを書きます。例外の後で状態が変になっていないことも確認しておきます。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_add_negative_count_items
    assert(false, "no implementation")
    cart = ShoppingCart.new
    ex = assert_raises(ArgumentError) {
      cart.add_items("item", -1)
    }
    assert_equal("negative quantity", ex.message)
    assert_equal(0, cart.item_count)
  end
end

ShoppingCart.rb
class ShoppingCart
  ...
  def add_items(item, quantity)
    raise ArgumentError.new("negative quantity") if (quantity < 0)
    @item_count += quantity
  end
end

インスタンスを作るコードが、2つのメソッドに共通なので、setupに括り出します。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  def setup
    @cart = ShoppingCart.new
  end

  def test_add_items
    cart = ShoppingCart.new
    assert_equal(0, cart.item_count)
    assert_equal(0, @cart.item_count)

    cart.add_items("item", 0)
    @cart.add_items("item", 0)
    assert_equal(0, cart.item_count)
    assert_equal(0, @cart.item_count)

    cart.add_items("item", 1)
    @cart.add_items("item", 1)
    assert_equal(1, cart.item_count)
    assert_equal(1, @cart.item_count)

    cart.add_items("item", 2)
    @cart.add_items("item", 2)
    assert_equal(3, cart.item_count)
    assert_equal(3, @cart.item_count)
  end

  def test_add_negative_count_items
    cart = ShoppingCart.new
    ex = assert_raises(ArgumentError) {
      cart.add_items("item", -1)
      @cart.add_items("item", -1)
    }
    assert_equal("negative quantity", ex.message)
    assert_equal(0, cart.item_count)
    assert_equal(0, @cart.item_count)
  end
end

delete_items


ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...

  def test_delete_items
    assert(false, "no implementation")
  end
end

何が一番簡単そうでしょうか。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...

  def test_delete_items
    assert(false, "no implementation")
    @cart.add_items("item", 1)

    @cart.delete_items("item", 0)
    assert_equal(1, @cart.item_count)
  end
end

ShoppingCart.rb
class ShoppingCart
  ...

  def delete_items(item, quantity)
  end
end

ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...

  def test_delete_items
    @cart.add_items("item", 1)

    @cart.delete_items("item", 0)
    assert_equal(1, @cart.item_count)

    @cart.delete_items("item", 1)
    assert_equal(0, @cart.item_count)
  end
end

ShoppingCart.rb
class ShoppingCart
  ...

  def delete_items(item, quantity)
    @item_count -= quantity
  end
end

quantityが負なら例外


ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_negative_count_items
    assert(false, "no implementation")
  end
end

ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_negative_count_items
    assert(false, "no implementation")
    @cart.add_items("item", 1)

    ex = assert_raises(ArgumentError) {
      @cart.delete_items("item", -1)
    }
    assert_equal("negative quantity", ex.message)
    assert_equal(0, cart.item_count)
  end
end

ShoppingCart.rb
class ShoppingCart
  ...

  def delete_items(item, quantity)
    raise ArgumentError.new("negative quantity") if (quantity < 0)
    @item_count -= quantity
  end
end

add_itemsとdelete_itemsに共通な引数チェックを1つにまとめます。
ShoppingCart.rb
class ShoppingCart
  ...

  def add_items(item, quantity)
    raise ArgumentError.new("negative quantity") if (quantity < 0)
    assert_quantity_is_not_negative(quantity)
    @item_count += quantity
  end

  def delete_items(item, quantity)
    raise ArgumentError.new("negative quantity") if (quantity < 0)
    assert_quantity_is_not_negative(quantity)
    @item_count -= quantity
  end

  def assert_quantity_is_not_negative(quantity)
    raise ArgumentError.new("negative quantity") if (quantity < 0)
  end
end

ないItemを削除しようとしたら例外


ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_not_exist_items
    assert(false, "no implementation")
  end
end
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_not_exist_items
    assert(false, "no implementation")
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 1)
    }
    assert_equal(0, cart.item_count)
  end
end

add_itemsしたItemを覚えておく必要があります。
ShoppingCart.rb
class ShoppingCart
  def initialize
    @item_count = 0
    @items = []
  end
  ...

  def add_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    @items.push(item) unless @items.include?(item)
    @item_count += quantity
  end

  def delete_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    raise "no such item" unless @items.include?(item)
    @item_count -= quantity
  end

  ...
end

見え見えの欠陥を突きます。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_not_exist_items
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 1)
    }
    assert_equal(0, cart.item_count)

    @cart.add_items("item", 1)
    @cart.delete_items("item", 1)
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 1)
    }
    assert_equal(0, cart.item_count)
  end
end

とりあえず元に戻して出直すことにします。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_not_exist_items
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 1)
    }
    assert_equal(0, cart.item_count)

    @cart.add_items("item", 1)
    @cart.delete_items("item", 1)
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 1)
    }
    assert_equal(0, cart.item_count)
  end
end

Item毎に数を保持できるよう、辞書を使うことにします。
ShoppingCart.rb
class ShoppingCart
  def initialize
    @item_count = 0
    @items = []
    @itemDict = {}
  end
  ...

  def add_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    @items.push(item) unless @items.include?(item)
    unless @itemDict.has_key?(item)
      @itemDict[item] = quantity
    end
    @item_count += quantity
  end
end

配列での実装を完全に辞書で置き換えます。
ShoppingCart.rb
class ShoppingCart
  def initialize
    @item_count = 0
    @items = []
    @itemDict = {}
  end
  ...

  def add_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    @items.push(item) unless @items.include?(item)
    unless @itemDict.has_key?(item)
      @itemDict[item] = quantity
    end
    @item_count += quantity
  end

  def delete_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    raise "no such item" unless @items.include?(item)
    raise "no such item" unless @itemDict.has_key?(item)
    @item_count -= quantity
  end
end

前に断念したテストに再挑戦です。
ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_not_exist_items
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 1)
    }
    assert_equal(0, cart.item_count)

    @cart.add_items("item", 1)
    @cart.delete_items("item", 1)
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 1)
    }
    assert_equal(0, cart.item_count)
  end
end

Item毎の数の管理をまじめに実装します。
ShoppingCart.rb
class ShoppingCart
  ...

  def add_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    unless @itemDict.has_key?(item)
    if @itemDict.has_key?(item)
      @itemDict[item] += quantity
    else
      @itemDict[item] = quantity
    end
    @item_count += quantity
  end

  def delete_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    raise "no such item" unless @itemDict.has_key?(item)
    if (@itemDict[item] == quantity)
      @itemDict.delete(item)
    else
      @itemDict[item] -= quantity
    end
    @item_count -= quantity
  end
end

辞書から全体の数も計算できます。
ShoppingCart.rb
class ShoppingCart
  ...

  def item_count
    return @item_count
    return @itemDict.values.inject(0) {|sum, each|
      sum + each
    }
  end

  ...
end

不要になった@item_countを削除します。
ShoppingCart.rb
class ShoppingCart
  def initialize
    @item_count = 0
    @itemDict = {}
  end
  ...

  def add_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    unless @itemDict.has_key?(item)
    if @itemDict.has_key?(item)
      @itemDict[item] += quantity
    else
      @itemDict[item] = quantity
    end
    @item_count += quantity
  end

  def delete_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    raise "no such item" unless @itemDict.has_key?(item)
    if (@itemDict[item] == quantity)
      @itemDict.delete(item)
    else
      @itemDict[item] -= quantity
    end
    @item_count -= quantity
  end
end

カートにあるItem数より多く削除しようとしたら例外


ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_too_large_count_items
    assert(false, "no implementation")
  end
end

ShoppingCartTest.rb
class ShoppingCartTest < Test::Unit::TestCase
  ...
  def test_delete_too_large_count_items
    assert(false, "no implementation")
    @cart.add_items("item", 1)
    ex = assert_raises(RuntimeError) {
      @cart.delete_items("item", 2)
    }
    assert_equal(1, cart.item_count)
  end
end

ShoppingCart.rb
class ShoppingCart
  ...

  def delete_items(item, quantity)
    assert_quantity_is_not_negative(quantity)
    raise "no such item" unless @itemDict.has_key?(item)
    raise "delete quantity is larger than current quantity" if (quantity > @itemDict[item])
    if @itemDict[item] == quantity
      @itemDict.delete(item)
    else
      @itemDict[item] -= quantity
    end
  end
end
これでおしまいです。