Redux で気軽にできる TDD


[TDDBC大阪4.0](http://www.kokuchpro.com/event/tddbcosaka4/) のテーマは **気軽にできるTDD** です。

> 気軽とは、態度がもったいぶらず、打ち解けやすいさま。きさく。また、こだわりなくすぐ物事をするさま。

[TDD](https://ja.wikipedia.org/wiki/%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA) は *Red Green Refactor* のサイクルを繰り返します。その初回のサイクルはこちらです。

1. まず、テストから書いて **Red** (Assert First)
2. すぐ、テストを成功させて **Green** (Fake It)
3. シュッとさせる **Refactor** (Refactoring)

初回のサイクルこそ気軽にやりたい、そして、リファクタリングでも楽したいです。
[Redux](http://redux.js.org/) のテクニックを使うと気軽にできそうに思えたのでサンプルコードを書きました。
当日は [Ruby](https://www.ruby-lang.org/) のTAで参加したのでテストツールには [RSpec](http://rspec.info/) を使っています。

# Exercise

今回の課題は飲み物の自動販売機です。

## ステップ０

### お金の投入と払い戻し

* 10円玉、50円玉、100円玉、500円玉、1000円札を１つずつ投入できる。
* 投入は複数回できる。
* 投入金額の総計を取得できる。
* 払い戻し操作を行うと、投入金額の総計を釣り銭として出力する。

## ステップ１

### 扱えないお金

* 想定外のものが投入された場合は、投入金額に加算せず、それをそのまま釣り銭としてユーザに出力する。
    * 想定は硬貨：１円玉、５円玉。お札：千円札以外のお札

## ステップ２

### ジュースの管理

* 値段と名前の属性からなるジュースを１種類格納できる。
    * 初期状態で、コーラ（値段:120円、名前”コーラ”）を5本格納している。
* 格納されているジュースの情報（値段と名前と在庫）を取得できる。

## ステップ３

### 購入

* 投入金額、在庫の点で、コーラが購入できるかどうかを取得できる。
* ジュース値段以上の投入金額が投入されている条件下で購入操作を行うと、ジュースの在庫を減らし、売り上げ金額を増やす。
* 投入金額が足りない場合もしくは在庫がない場合、購入操作を行っても何もしない。
* 現在の売上金額を取得できる。
* 払い戻し操作では現在の投入金額からジュース購入金額を引いた釣り銭を出力する。

## ステップ４

### 機能拡張

* ジュースを3種類管理できるようにする。
    * 在庫にレッドブル（値段:200円、名前”レッドブル”）5本を追加する。
    * 在庫に水（値段:100円、名前”水”）5本を追加する。
* 投入金額、在庫の点で購入可能なドリンクのリストを取得できる。

## ステップ５

### 釣り銭と売り上げ管理

* ジュース値段以上の投入金額が投入されている条件下で購入操作を行うと、釣り銭（投入金額とジュース値段の差分）を出力する。
    * ジュースと投入金額が同じ場合、つまり、釣り銭0円の場合も、釣り銭0円と出力する。
    * 釣り銭の硬貨の種類は考慮しなくてよい。

# Example

Rubyはオブジェクト指向プログラミングをサポートするための機能でクラスを使うことができます。
最初にどう作っていくか話すときにクラス図があるとよいかもしれません。
今回は自動販売機を "お金で飲み物を購入することができる装置" と決めました。

![class](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/images/class.png)

[PlantUML](http://plantuml.com/)で図を書くとテストを書くときに文字が使えて便利です。

```
Money ..o MoneyStore
Drink ..o DrinkStore

MoneyStore ..* VendingMachine
DrinkStore ..* VendingMachine

class Money {
  name
  value
  +valid?()
}

class MoneyStore {
  moneys
  amount
  +insert()
  +eject()
}

class Drink {
  name
  price
}

class DrinkStore {
  drinks
  +insert()
  +eject()
}

class VendingMachine {
  deposit_amount
  change_moneys
  stock_amount
  sale_drinks
  stock_drinks
  choose_drinks
  +insert_money()
  +eject_money()
  +insert_drink()
  +purchase_drink()
}
```

## Money

お金には名前があって、その名前で自動販売機が扱う価値をみなすと決めました。

![money](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/images/money.png)

まず `10円玉` は `10` という Assert ですぐ `10` の仮実装をするだけなので、ここは気軽にTDDができますね。
RSpec は `shared_examples` を使うとテストを **DRY** に書くことができます。

```ruby
shared_examples "valid money" do |name, value|
  subject { Money.new(name) }

  example "value of #{name} is #{value}" do
    expect(subject.value).to eq value
  end

  example "#{name} is valid" do
    expect(subject.valid?).to be_truthy
  end
end

shared_examples "invalid money" do |name|
  subject { Money.new(name) }

  example "value of #{name} is 0" do
    expect(subject.value).to eq 0
  end

  example "#{name} is invalid" do
    expect(subject.valid?).to be_falsey
  end
end

describe Money do
  context "valid money" do
    describe "10 yen coin" do
      include_examples "valid money", "10円玉", 10
    end

    describe "50 yen coin" do
      include_examples "valid money", "50円玉", 50
    end

    describe "100 yen coin" do
      include_examples "valid money", "100円玉", 100
    end

    describe "500 yen coin" do
      include_examples "valid money", "500円玉", 500
    end

    describe "1000 yen bill" do
      include_examples "valid money", "1000円札", 1000
    end
  end

  context "invalid money" do
    describe "1 yen coin" do
      include_examples "invalid money", "1円玉"
    end

    describe "5 yen coin" do
      include_examples "invalid money", "5円玉"
    end

    describe "2000 yen bill" do
      include_examples "invalid money", "2000円札"
    end

    describe "5000 yen bill" do
      include_examples "invalid money", "5000円札"
    end

    describe "10000 yen bill" do
      include_examples "invalid money", "10000円札"
    end
  end
end
```

`to_value` の `case` が気になるなら `{"10円玉" => 10, "50円玉" => 50, ...}` にリファクタリングされていくのかもしれません。

```ruby
class Money
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def value
    @value ||= to_value
  end

  def valid?
    value > 0
  end

  private
  def to_value
    case name
    when "10円玉"
      10
    when "50円玉"
      50
    when "100円玉"
      100
    when "500円玉"
      500
    when "1000円札"
      1000
    else
      0
    end
  end
end
```

## MoneyStore

お金を入れたり出したりできる装置は `入れたお金` と `そのお金の総額` の状態を記憶すると決めました。

![money_store](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/images/money_store.png)

これは行動で状態が変化する装置です。これを気軽にTDDをしたいです。そこで **Redux** の *Reducer* を参考にします。
*Reducer* は `変更前の状態` と `行動` をパラメータにセットすると `変更後の状態` をリターンするメカニズムです。
独立した機構なので TDD の *Assert First* と *Fake It* で実装していくのに相性がよさそうです。

*Reducer* は `state` と `action` の `let` を準備して `reducer` を `subject` に設定すると `is_expected` で Assert を書くことができます。
*Action* には `type` と `payload` が設定されます。`reducer` をコールする `dispatch` のパラメータに *Action* をセットする方式にしているので RSpec では `receive(:dispatch)` で *Action* の Assert を書くことができます。

```ruby
describe MoneyStore do
  let(:money_10_yen_coin){ Money.new("10円玉") }
  let(:money_50_yen_coin){ Money.new("50円玉") }
  let(:money_100_yen_coin){ Money.new("100円玉") }
  let(:money_store){ MoneyStore.new }

  describe "reducer" do
    describe "insert" do
      context "first" do
        let(:state){ {moneys: [], amount: 0} }
        let(:action){ {type: :insert, payload: money_10_yen_coin} }
        subject { money_store.send(:reducer, state, action) }
        it { is_expected.to match a_hash_including(moneys: [money_10_yen_coin]) }
        it { is_expected.to match a_hash_including(amount: 10) }
      end

      context "append" do
        let(:state){ {moneys: [money_10_yen_coin], amount: 10} }
        let(:action){ {type: :insert, payload: money_50_yen_coin} }
        subject { money_store.send(:reducer, state, action) }
        it { is_expected.to match a_hash_including(moneys: [money_50_yen_coin, money_10_yen_coin]) }
        it { is_expected.to match a_hash_including(amount: 60) }
      end
    end

    describe "eject" do
      let(:state){ {moneys: [money_100_yen_coin, money_10_yen_coin], amount: 110} }
      let(:action){ {type: :eject, payload: state[:amount]} }
      subject { money_store.send(:reducer, state, action) }
      it { is_expected.to match a_hash_including(moneys: [], amount: 0) }
    end
  end

  describe "dispatch" do
    describe "insert" do
      example do
        expect(money_store).to receive(:dispatch).with({:type=>:insert, :payload=>money_10_yen_coin})
        money_store.insert(money_10_yen_coin)
      end
    end

    describe "eject" do
      example do
        expect(money_store).to receive(:dispatch).with({:type=>:eject, :payload=>"10円玉"})
        money_store.eject(money_10_yen_coin.name)
      end
    end
  end
end
```

このサンプルでは `reducer` の内部で `state` の値を書き換えていますが、Redux では `state` の値を書き換えません。JavaScript の [Object.assign()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) を使われることが多いです。
Ruby で同様の機能は [reduce()](https://docs.ruby-lang.org/ja/latest/class/Enumerable.html#I_INJECT) で出来ますね。 `[].reduce({}, :merge)` と書きます。

```ruby
class MoneyStore
  attr_reader :state

  def initialize()
    @state = {
      moneys: [],
      amount: 0
    }
  end

  def insert(money)
    dispatch({type: :insert, payload: money})
  end

  def eject(name)
    dispatch({type: :eject, payload: name})
  end

  private
  def reducer(state, action)
    case action[:type]
    when :insert
      state[:moneys] << action[:payload]
    when :eject
      amount = action[:payload]
      state[:moneys].reject! {|money| amount >= money.value ? amount = amount - money.value : false }
    end
    state[:moneys].sort_by! {|money| - money.value }
    state[:amount] = state[:moneys].map(&:value).reduce(0, :+)
    state
  end

  def dispatch(action)
    @state = reducer(state, action)
  end
end
```

## Drink

飲み物は名前と値段が属性にあるだけと決めました。

![drink](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/images/drink.png)

```ruby
describe Drink do
  let(:drink){ Drink.new("コーラ", 120) }

  describe "#to_h" do
    subject { drink.to_h }
    it { is_expected.to a_hash_including(:name=>"コーラ", :price=>120) }
  end
end
```

飲み物は賞味期限や温度など関心事が増える可能性があるのかなと思いますが、今はその関心事は必要ないです。

```ruby
class Drink
  attr_reader :name, :price

  def initialize(name, price)
    @name, @price = name, price
  end

  def to_h
    { name: @name, price: @price }
  end
end
```

## DrinkStore

飲み物を入れたり出したりできる装置は `入れた飲み物` の状態を記憶すると決めました。

![drink_store](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/images/drink_store.png)

MoneyStore とほとんど同じなので割愛します。

## VendingMachine

ステップ０ と ステップ１ のサンプルです。
有効なお金を入れると預り金に入り、無効なお金を入れるとお釣りの装置に入ると決めました。
また、お金の返却すると預り金の全てをお釣りの装置に入れると決めました。

![vending_machine](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/images/vending_machine.png)

```ruby
describe VendingMachine do
  let(:vending_machine){ VendingMachine.new }

  describe "insert money" do
    context "valid money" do
      before { vending_machine.insert_money("10円玉") }
      example { expect(vending_machine.state[:deposit_amount]).to eq 10 }
    end

    context "invalid money" do
      before { vending_machine.insert_money("1円玉") }
      example { expect(vending_machine.state[:change_moneys]).to eq ["1円玉"] }
    end
  end

  describe "eject money" do
    before {
      vending_machine.insert_money("10円玉")
      vending_machine.eject_money
    }

    example { expect(vending_machine.state[:deposit_amount]).to eq 0 }
    example { expect(vending_machine.state[:change_moneys]).to eq ["10円玉"] }
  end
end
```

複雑な機能でも *Reducer* を利用することで、最初から簡単に仮実装で済ませることができます。

```ruby
class VendingMachine
  attr_reader :state

  def initialize()
    @deposit_money_store = MoneyStore.new
    @change_money_store = MoneyStore.new
    @state = {
      deposit_amount: 0,
      change_moneys: []
    }
  end

  def insert_money(name)
    money = Money.new(name)
    money_store = money.valid? ? @deposit_money_store : @change_money_store
    money_store.insert(money)

    dispatch({type: :update_money, payload: {
      deposit_amount: @deposit_money_store.state[:amount],
      change_moneys: @change_money_store.state[:moneys]
    }})
  end

  def eject_money
    @deposit_money_store.state[:moneys].each do |money|
      @change_money_store.insert(money)
    end
    @deposit_money_store.eject(state[:deposit_amount])

    dispatch({type: :update_money, payload: {
      deposit_amount: @deposit_money_store.state[:amount],
      change_moneys: @change_money_store.state[:moneys]
    }})
  end

  private
  def reducer(state, action)
    case action[:type]
    when :update_money
      state[:deposit_amount] = action[:payload][:deposit_amount]
      state[:change_moneys] = action[:payload][:change_moneys].map(&:name)
    end
    state
  end

  def dispatch(action)
    @state = reducer(state, action)
  end
end
```

テクニックで気軽に TDD ができることを説明しました。もっと **気軽にできるTDD** をみなさんも見つけてください。
ステップ５までのサンプルコードは[こちら](https://github.com/ogom/ruby-vending-machine/tree/0.0.1)です。

*Enjoy TDD*

# RDRA

要件定義が気になったら [モデルベース要件定義テクニック](http://www.shuwasystem.co.jp/products/7980html/3944.html) の書籍が参考になるかもしれません。
こちらの図はRDRA(ラドラ)の方式を参考に書いたサンプルです。独自に書いた図なので正しくは[こちら](http://k-method.jp/index.html)を参照してください。

## コンテキストモデル

![](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/rdra/%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%83%A2%E3%83%87%E3%83%AB.png)

## 要求モデル

![](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/rdra/%E8%A6%81%E6%B1%82%E3%83%A2%E3%83%87%E3%83%AB.png)

## システム外部環境

![](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/rdra/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E5%A4%96%E9%83%A8%E7%92%B0%E5%A2%83.png)

## システム境界

![](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/rdra/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E5%A2%83%E7%95%8C.png)

## システム

![](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/rdra/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0.png)

## システム地図

![](https://raw.githubusercontent.com/ogom/ruby-vending-machine/master/doc/rdra/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E5%9C%B0%E5%9B%B3.png)

外部からシステムを見たり、内部からシステムを育てたりの繰り返しをして、気軽が気楽になるといいですね。

> 気楽とは、気兼ねや心配がなく、物事にこだわらず、のんきなさま