ステップ3 購入
ここが今回の山場になります。コインメッククラスと商品ラッククラスに機能を追加しつつ、両者を連携させるコントローラークラスも作っていくことになります。
ステップ3 購入
- 投入金額、在庫の点で、コーラが購入できるかどうかを取得できる。
- ジュース値段以上の投入金額が投入されている条件下で購入操作を行うと、ジュースの在庫を減らし、売り上げ金額を増やす。
- 投入金額が足りない場合もしくは在庫がない場合、購入操作を行っても何もしない。
- 現在の売上金額を取得できる。
- 払い戻し操作では現在の投入金額からジュース購入金額を引いた釣り銭を出力する。
ステップ3 1: コーラが購入できるか判定する
在庫のチェック
まず、在庫のチェックができなければなりません。これは、在庫を持っている商品ラッククラスの役目です。
すぐにジュースの種類が増えることは分かっているので、それを先取りしたテストケースにしておきます。もしも真面目にやるのであれば、Is在庫有り()メソッドは引数無しで始めて、ジュースの種類が増えたときに引数を追加してください。
コーラの在庫があるか
[TestCase]
public void Is在庫有りTest_コーラ() {
商品ラック rack = new 商品ラック();
Assert.IsTrue(rack.Is在庫有り(ジュース.コーラ));
}
商品ラッククラス
public bool Is在庫有り(ジュース kind) {
return ビン.在庫数 > 0;
//TODO: kindに応じた在庫チェック
}
投入金額が足りているか
次に、投入金額が足りているかチェックできなければなりません。投入金額はコインメッククラスが知っています。しかし、コインメッククラスがジュースの値段まで知っているようでは、責務過剰です。コインメッククラスはお金のやり取りだけに専念してもらい、投入金額が足りているかどうかの判定は、新しくコントローラークラスを作ってそれにやらせることにしましょう。
コントローラークラスは投入金額を知る必要がありますから、コインメックと連携しなければなりません。コントローラーをインスタンス化するときにコインメックを引き渡すことにしましょう。ちょっとややこしいので、仮実装→三角測量と進めます。
[TestCase]
public void Is購入可能Test_投入額不足で買えない() {
コントローラー ctl = new コントローラー(new コインメック());
ctl._coinMech.お金を投入する(100); //コーラは120円なので、買えない
Assert.IsFalse(ctl.Is購入可能(ジュース.コーラ));
}
コントローラークラス(仮実装)
internal class コントローラー {
internal コインメック _coinMech;
internal コントローラー(コインメック coinMech){
this._coinMech = coinMech;
}
public bool Is購入可能(ジュース kind) {
return false;
}
}
続いて、三角測量です。
[TestCase]
public void Is購入可能Test_買える() {
コントローラー ctl = new コントローラー(new コインメック());
ctl._coinMech.お金を投入する(100);
ctl._coinMech.お金を投入する(10);
ctl._coinMech.お金を投入する(10);
Assert.IsTrue(ctl.Is購入可能(ジュース.コーラ));
}
コントローラークラス
public bool Is購入可能(ジュース kind) {
if (_coinMech.預り金 < kind.Price)
return false;
//TODO: 在庫チェック
return true;
}
次に、Is購入可能()メソッドに在庫切れの条件を追加したいのですが、在庫を減らす機能をまだ実装していないので、テストできません。これは後に回します。
テストのリファクタリング
ところで、テストコードにお金を投入する()が何行も連続で出てきます。これは面倒なので、ヘルパーメソッドを書いて、テストコードをリファクタリングしておきましょう。
internal static class コインメックExtension {
public static int お金を投入する(this コインメック coinMech,
int coin1, params int[] coins) {
int 返却金 = 0;
返却金 += coinMech.お金を投入する(coin1);
foreach(int m in coins)
返却金 += coinMech.お金を投入する(m);
return 返却金;
}
}
[TestCase]
public void Is購入可能Test_買える() {
コントローラー ctl = new コントローラー(new コインメック());
//ctl._coinMech.お金を投入する(100);
//ctl._coinMech.お金を投入する(10);
//ctl._coinMech.お金を投入する(10);
ctl._coinMech.お金を投入する(100, 10, 10);
Assert.AreEqual(true, ctl.Is購入可能(ジュース.コーラ));
}
ステップ3 2: コーラを購入する
購入操作は、お客にジュースを提供し(在庫は減る)、預り金から代金を売上金額に移します。いっぺんに「購入」を作るのは複雑すぎるので、ジュース提供することと、代金を受け取ることに分けて作っていきます。
ジュースを提供する
これは商品ラッククラスの仕事です。ジュースを提供する()メソッドを呼び出すとジュースが返され、在庫は1減ることになります。なお、ここでもジュースの種類が増えることを見越して、ジュースを提供する()メソッドに引数を与えています。
[TestCase]
public void ジュースを提供するTest_コーラ() {
商品ラック rack = new 商品ラック(); //初期状態でコーラ5本
Assert.AreEqual(ジュース.コーラ.Name, rack.ジュースを提供する(ジュース.コーラ).Name);
Assert.AreEqual(4, rack.ビン.在庫数);
}
商品ラッククラス
public ジュース ジュースを提供する(ジュース kind) {
ビン.在庫数--;
return kind;
}
ジュースクラスのEquals()メソッドのオーバーライド
ところで、上のテストケースでNameプロパティの比較にしているのは、ジュースオブジェクト同士の直接の比較ができないからです。ここでちょっと寄り道をして、ジュースクラスのEquals()メソッドなどを実装しておきましょう。
Equals()メソッドなどをオーバーライドする方法については、次のMSDNのドキュメントを参考にしてください。
これもテストケースを1つずつRED→GREENにして行きますが、最終的な状態だけを示します。テストケースの並び順は、作っていった順番です。
Equals()メソッドのオーバーライド
[TestCase]
public void GetHashCodeTest() {
int hash1 = ジュース.コーラ.GetHashCode();
int hash2 = ジュース.コーラ.GetHashCode();
Assert.AreEqual(hash1, hash2);
}
[TestCase]
public void EqualsTest_等しい() {
ジュース cola1 = ジュース.コーラ;
ジュース cola2 = ジュース.コーラ;
Assert.AreEqual(cola1, cola2);
Assert.IsTrue(cola1 == cola2);
}
[TestCase]
public void EqualsTest_nullとは等しくない() {
ジュース cola = ジュース.コーラ;
Assert.AreNotEqual(cola, null);
Assert.IsTrue(cola != null);
Assert.IsTrue(null != cola);
}
[TestCase]
public void EqualsTest_nullとnullは等しい() {
ジュース cola = null;
Assert.AreEqual(cola, null);
Assert.IsTrue(cola == null);
Assert.IsTrue(null == cola);
}
ジュースクラス
public override int GetHashCode() {
return (Name.GetHashCode() ^ Price.GetHashCode());
}
public override bool Equals(object obj) {
var j = obj as ジュース;
return (this == j);
}
public static bool operator ==(ジュース x, ジュース y) {
if ((object)x == null && (object)y == null)
return true;
if ((object)x == null || (object)y == null)
return false;
return (x.Name == y.Name && x.Price == y.Price);
}
public static bool operator !=(ジュース x, ジュース y) {
return !(x == y);
}
ジュースを提供する(再)
ジュースオブジェクト同士の比較ができるようになったので、さきほどのテストケースを書き直しておきます。
[TestCase]
public void ジュースを提供するTest_コーラ() {
商品ラック rack = new 商品ラック(); //初期状態でコーラ5本
//Assert.AreEqual(ジュース.コーラ.Name, rack.ジュースを提供する(ジュース.コーラ).Name);
Assert.AreEqual(ジュース.コーラ, rack.ジュースを提供する(ジュース.コーラ));
Assert.AreEqual(4, rack.ビン.在庫数);
}
