ステップ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は完成です。
コントローラーのクラス図
ここまでのコントローラークラスは、次の図のようになっています。
コントローラーのクラス図(ステップ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のままであることを確かめます。そうしたら、 メンバー変数ビンを削除します。
ビンをGetビン(ジュース.コーラ)に置き換え
[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の時間が短くてすむとは言うものの、けっこう面倒ですし、途中で間違えてしまいそうですね。コレクションにしなければならないと分かっているときには、最初からコレクションにしてしまいましょう。
商品ラックのクラス図
ここまでで商品ラッククラスは、このようになっています。
商品ラックのクラス図(ステップ4-1終了時点)
商品ラックはビンのコレクションを持っており、そこにジュースの種類と在庫数を保持しています。在庫を管理し、提供することが責務になっています。公開しているメソッドのうち、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.預り金));
}
