ステップ3 1: コーラが購入できるか判定する(続)
商品ラック
にジュースを提供する()
メソッドを実装したので、在庫を減らすことができるようになりました。ステップ3-1で後回しにしておいた、Is購入可能()
メソッドに在庫切れの条件を追加することができます。
コントローラー
が在庫を知ることも必要になったので、商品ラック
クラスとも連携しなければなりません。コントローラー
のコンストラクターの引数を増やします。そのため、既存のテストコードにも修正が必要になります。
[TestCase] public void Is購入可能Test_在庫不足で買えない() { コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック()); for (int i = 0; i < 5; i++) ctl._rack.ジュースを提供する(ジュース.コーラ); //Assert.IsFalse(ctl._rack.Is在庫有り(ジュース.コーラ)); // 確認 ctl._coinMech.お金を投入する(100, 10, 10); Assert.AreEqual(false, ctl.Is購入可能(ジュース.コーラ)); }
internal class コントローラー { internal コインメック _coinMech; internal 商品ラック _rack; //←追加 internal コントローラー(コインメック coinMech, 商品ラック rack){ this._coinMech = coinMech; this._rack = rack; //←追加 } public bool Is購入可能(ジュース kind) { if (_coinMech.預り金 < kind.Price) return false; if (!_rack.Is在庫有り(kind)) //←追加 return false; //←追加 return true; } //(後略)
これでステップ3-1は完了です。ステップ3-2に戻ります。
ステップ3 2: コーラを購入する(続)
このステップは、ジュースを提供して在庫を減らすところまで終わっていました。次は、預り金から代金を売り上げ金額に移す部分です。
売り上げ金額プロパティ
売り上げ金額はコインメック
に管理させましょう。まず、プロパティを作ります。
[TestCase] public void 売り上げ金額Test_最初は0円() { コインメック cm = new コインメック(); Assert.AreEqual(0, cm.売り上げ金額); }
public int 売り上げ金額 { get; private set; }
これはステップ3-4も満足することになります。
代金を貰う
代金を貰う()
メソッドで、預り金から代金を売り上げ金額に移します。
[TestCase] public void 代金を貰うTest() { コインメック cm = new コインメック(); cm.お金を投入する(100, 10); cm.代金を貰う(100); Assert.AreEqual(10, cm.預り金); Assert.AreEqual(100, cm.売り上げ金額); }
public void 代金を貰う(int charge) { 預り金 -= charge; 売り上げ金額 += charge; }
購入する
これで、購入する()
メソッドを組み立てることができます。仮実装→三角測量します。
まず、買えない場合。これはステップ3-3に該当します。
[TestCase] public void 購入するTest_購入不可の時はnullが返る() { コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック()); // お金を投入していないから買えない Assert.IsNull(ctl.購入する(ジュース.コーラ)); }
public ジュース 購入する(ジュース kind) { return null; }
続いて、買える場合。購入する()
メソッドからコーラ
オブジェクトが返ってくるだけでなく、状態の預り金
と売上金額
が変化します。
[TestCase] public void 購入するTest_購入できる場合() { コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック()); ctl._coinMech.お金を投入する(100, 10, 10, 10); //130円入れたので、10円残るはず Assert.AreEqual(ジュース.コーラ, ctl.購入する(ジュース.コーラ)); Assert.AreEqual(10, ctl._coinMech.預り金); Assert.AreEqual(120, ctl._coinMech.売り上げ金額); //Assert.AreEqual(10, ctl._coinMech.お金を払い戻す()); //確認(ステップ3-5) }
public ジュース 購入する(ジュース kind) { if(!Is購入可能(kind)) return null; var product = _rack.ジュースを提供する(kind); _coinMech.代金を貰う(kind.Price); return product; }
最初からGREENになってしまうのでコメントアウトしましたが、お金を払い戻す()
メソッドは預り金から代金を引いた額だけ出力しています。つまり、ステップ3-5もできています。
これでステップ3は完成です。
コントローラーのクラス図
ここまでのコントローラー
クラスは、次の図のようになっています。
コントローラー
自体は状態を直接には持っていません。メンバー変数にコインメック
と商品ラック
を保持して、それらの連携を取ることが責務になっています。公開しているメソッドは、購入に関するものだけです。このクラスもまだまだシンプルで、これ以上の分割を考える必要性もなさそうです。
ステップ4 機能拡張
ステップ4 機能拡張
- ジュースを3種類管理できるようにする。
- 在庫にレッドブル(値段:200円、名前"レッドブル")5本を追加する。
- 在庫に水(値段:100円、名前"水")5本を追加する。
- 投入金額、在庫の点で購入可能なドリンクのリストを取得できる。
ステップ4 1: ジュースの種類を増やす
1つめは、在庫に複数種類のジュースを持てるように、商品ラック
クラスを拡張します。ただし、商品ごとにビンは1本だけ使うものとします。
レッドブルと水
まず、レッドブル
と水
を用意しておかねばなりません。
[TestCase] public void レッドブルTest() { ジュース redbull = ジュース.レッドブル; Assert.AreEqual("レッドブル", redbull.Name); Assert.AreEqual(200, redbull.Price); } [TestCase] public void 水Test() { ジュース water = ジュース.水; Assert.AreEqual("水", water.Name); Assert.AreEqual(100, water.Price); }
public static ジュース レッドブル { get { return new ジュース() { Name = "レッドブル", Price = 200, }; } } public static ジュース 水 { get { return new ジュース() { Name = "水", Price = 100, }; } }
商品ラックにレッドブル用のビンを追加する
商品ラック
クラスのメンバー変数ビン
を、コレクションに変更しなければなりません。その手順としては、ビン
型をList<ビン>
にいきなり変えてしまい、エラーになった部分を修正していく方法もあります。
しかしここでは、一歩々々進めるやり方として、既存のメンバー変数はそのままに、新しくレッドブル
専用のビン
を追加してみます。ちょっと面倒な手順になりますが、REDになっている時間は短くてすみます。
[TestCase] public void 在庫を追加するTest() { 商品ラック rack = new 商品ラック(); for (int i = 0; i < 5; i++) rack.ジュースを提供する(ジュース.コーラ); //コーラの在庫を0にしておく rack.在庫を追加する(ジュース.レッドブル, 5); Assert.IsTrue(rack.Is在庫有り(ジュース.レッドブル)); }
private ビン RedBullビン = new ビン() { 商品 = ジュース.レッドブル }; //暫定 public void 在庫を追加する(ジュース kind, int number) { RedBullビン.在庫数 += number; //暫定 } public bool Is在庫有り(ジュース kind) { if(kind == ジュース.レッドブル) //暫定 return RedBullビン.在庫数 > 0; return ビン.在庫数 > 0; }
このRedBullビン
メンバー変数は、次でList<ビン>
型に変更します。
商品ラックにGetビン()メソッドを追加する
ビンを選択するメソッドを作ることで、メンバー変数をコレクションに変えます。
[TestCase] public void GetビンTest() { 商品ラック rack = new 商品ラック(); rack.在庫を追加する(ジュース.レッドブル, 5); Assert.AreEqual(ジュース.レッドブル, rack.Getビン(ジュース.レッドブル).商品); //Assert.AreEqual(5, rack.Getビン(ジュース.レッドブル).在庫数); }
//private ビン RedBullビン = new ビン() { 商品 = ジュース.レッドブル }; //暫定 private List<ビン> RedBullビン = new List<ビン>() { new ビン() { 商品 = ジュース.レッドブル } }; public ビン Getビン(ジュース kind) { return RedBullビン.Find(b => (b.商品 == kind)); }
ただし、RedBullビン.在庫数
を参照している箇所がエラーになるので、修正します。
public void 在庫を追加する(ジュース kind, int number) { //RedBullビン.在庫数 += number; RedBullビン[0].在庫数 += number; } public bool Is在庫有り(ジュース kind) { if(kind == ジュース.レッドブル) //暫定 //return RedBullビン.在庫数 > 0; return RedBullビン[0].在庫数 > 0; return ビン.在庫数 > 0; }
複数種類の在庫を追加する
レッドブル
と水
の在庫を追加するようにしてみます。だんだんと、ちゃんとコレクションを使うコードになっていきます。ただし、まだ存在しないビンを要求された時は、自動的に生成するものとします。
[TestCase] public void 在庫を追加するTest_レッドブルと水() { 商品ラック rack = new 商品ラック(); rack.在庫を追加する(ジュース.レッドブル, 5); rack.在庫を追加する(ジュース.水, 3); Assert.AreEqual(5, rack.Getビン(ジュース.レッドブル).在庫数); Assert.AreEqual(3, rack.Getビン(ジュース.水).在庫数); }
public ビン Getビン(ジュース kind) { ビン bin = RedBullビン.Find(b => (b.商品 == kind)); if (bin == null) { bin = new ビン() { 商品 = kind, }; RedBullビン.Add(bin); } return bin; } public void 在庫を追加する(ジュース kind, int number) { //RedBullビン[0].在庫数 += number; Getビン(kind).在庫数 += number; }
複数種類のジュースを提供する
現状では、何か提供するといつでもコーラ
の在庫が減算されてしまいます。ジュースを提供する()
メソッドとIs在庫有り
プロパティを修正します。
[TestCase] public void Is在庫有りTest_コーラと水() { 商品ラック rack = new 商品ラック(); rack.在庫を追加する(ジュース.水, 5); //Assert.IsTrue(rack.Is在庫有り(ジュース.コーラ)); //確認 //Assert.IsTrue(rack.Is在庫有り(ジュース.水)); //確認 for (int i = 0; i < 5; i++) rack.ジュースを提供する(ジュース.水); //水の在庫だけを0に Assert.IsTrue(rack.Is在庫有り(ジュース.コーラ)); Assert.IsFalse(rack.Is在庫有り(ジュース.水)); }
public bool Is在庫有り(ジュース kind) { //if(kind == ジュース.レッドブル) //暫定 // //return RedBullビン.在庫数 > 0; // return RedBullビン[0].在庫数 > 0; // //return ビン.在庫数 > 0; return Getビン(kind).在庫数 > 0; } public ジュース ジュースを提供する(ジュース kind) { ビン.在庫数--; //後で消す Getビン(kind).在庫数--; return kind; }
商品ラッククラスからメンバー変数ビンを取り除く
商品ラック
クラスのメンバー変数ビン
は、製品コードでは使わなくなったので削除しましょう。
//public ビン ビン{ get; private set; } public ジュース ジュースを提供する(ジュース kind) { //ビン.在庫数--; //後で消す Getビン(kind).在庫数--; return kind; }
商品ラック
クラスのメンバー変数ビン
をテストしているところを探して、Getビン(ジュース.コーラ)
に置き換えてもGREENのままであることを確かめます。そうしたら、 メンバー変数ビン
を削除します。
[TestCase] public void ConstructorTest_初期状態でコーラが5本() { 商品ラック rack = new 商品ラック(); Assert.AreEqual(5, rack.Getビン(ジュース.コーラ).在庫数); //← Assert.AreEqual("コーラ", rack.Getビン(ジュース.コーラ).商品.Name); //← } [TestCase] public void ジュースを提供するTest_コーラ() { 商品ラック rack = new 商品ラック(); Assert.AreEqual(ジュース.コーラ, rack.ジュースを提供する(ジュース.コーラ)); Assert.AreEqual(4, rack.Getビン(ジュース.コーラ).在庫数); //← }
商品ラッククラスのリファクタリング
メンバー変数をコレクションに変更できたものの、RedBullビン
という名前はいただけません。名前をビンリスト
に変え、そのほかも少々リファクタリングしておきましょう。
internal class 商品ラック { private List<ビン> ビンリスト = new List<ビン>(); public ビン Getビン(ジュース kind) { ビン bin = ビンリスト.Find(b => (b.商品 == kind)); if (bin == null) { bin = new ビン() { 商品 = kind, }; ビンリスト.Add(bin); } return bin; } public 商品ラック() { ビンリスト.Add(new ビン() { 商品 = ジュース.コーラ, 在庫数 = 5, }); } public void 在庫を追加する(ジュース kind, int number) { Getビン(kind).在庫数 += number; } public bool Is在庫有り(ジュース kind) { return Getビン(kind).在庫数 > 0; } // TODO: int Get在庫数(ジュース) public ジュース ジュースを提供する(ジュース kind) { Getビン(kind).在庫数--; return kind; } }
このように細かいステップを踏んで、メンバー変数の型をコレクションに直してきました。REDの時間が短くてすむとは言うものの、けっこう面倒ですし、途中で間違えてしまいそうですね。コレクションにしなければならないと分かっているときには、最初からコレクションにしてしまいましょう。
商品ラックのクラス図
ここまでで商品ラック
クラスは、このようになっています。
商品ラック
はビン
のコレクションを持っており、そこにジュース
の種類と在庫数を保持しています。在庫を管理し、提供することが責務になっています。公開しているメソッドのうち、Getビン()
は責務からちょっと外れていますし、他のオブジェクト(コインメック
とコントローラー
)からも必要とされていません。テストのためだけに公開していますが、ちょっと嫌な感じです。Get在庫数(ジュース)
といったメソッドを作れば、Getビン()
メソッドをprivate
に変えられるでしょう。
ステップ4 2: 購入可能なジュースのリスト
このステップでは、投入金額で買えるジュースで在庫があるものだけのリストを取得できるようにします。まず在庫のある商品のリストを作り、その中から預り金以下の商品だけを抜き出せばよいですね。
商品ラックは在庫を持っているジュースを答えられる
まず、商品ラック
クラスに、在庫しているジュース
のリストを返す在庫ジュース
プロパティを作ります。
[TestCase] public void 在庫ジュースTest() { 商品ラック rack = new 商品ラック(); //初期状態でコーラは在庫している rack.在庫を追加する(ジュース.レッドブル, 1); rack.ジュースを提供する(ジュース.レッドブル); //レッドブルの在庫は0 rack.在庫を追加する(ジュース.水, 3); CollectionAssert.AreEqual( new List<ジュース>() { ジュース.コーラ, ジュース.水 }, rack.在庫ジュース ); }
internal IEnumerable<ジュース> 在庫ジュース { get { return ビンリスト.Where(bin => (bin.在庫数 > 0)).Select(bin => bin.商品); } }
コントローラーが購入可能な商品リストを答える
コントローラー
はコインメック
の預り金も知っていますから、購入可能な商品を答えることができます。
[TestCase] public void Get購入可能リストTest() { コントローラー ctl = new コントローラー(new コインメック(), new 商品ラック()); ctl._rack.在庫を追加する(ジュース.レッドブル, 5); ctl._rack.在庫を追加する(ジュース.水, 5); // これで、コーラ・レッドブル・水のどれも在庫が5 ctl._coinMech.お金を投入する(100, 10, 10); //120円入れたので、コーラと水だけが買える CollectionAssert.AreEquivalent( new List<ジュース>() { ジュース.コーラ, ジュース.水, }, ctl.Get購入可能リスト() ); }
public IEnumerable<ジュース> Get購入可能リスト() { return _rack.在庫ジュース.Where(kind => (kind.Price <= _coinMech.預り金)); }