接下来我们可以使用TDD的方式实现SnackMachine的returenMoney方法。首先创建SnackMachineTest类:
package com.lugew.springbootddd.snackmachine;import org.junit.jupiter.api.Test;import static com.lugew.springbootddd.snackmachine.Money.Dollar;import static org.junit.jupiter.api.Assertions.assertEquals;/*** @author 夏露桂* @since 2021/6/9 18:12*/public class SnackMachineTest {@Testpublic void return_money_empties_money_in_transaction() {SnackMachine snackMachine = new SnackMachine();snackMachine.insertMoney(Dollar);snackMachine.returnMoney();assertEquals(snackMachine.getMoneyInTransaction().getAmount(), 0, 0);}}
Dollar对象是Money类中的字段:
package com.lugew.springbootddd.snackmachine;import com.lugew.springbootddd.ValueObject;/*** @author 夏露桂* @since 2021/6/7 11:59*/public class Money extends ValueObject<Money> {public static Money None = new Money(0, 0, 0, 0, 0, 0);public static Money Cent = new Money(1, 0, 0, 0, 0, 0);public static Money TenCent = new Money(0, 1, 0, 0, 0, 0);public static Money Quarter = new Money(0, 0, 1, 0, 0, 0);public static Money Dollar = new Money(0, 0, 0, 1, 0, 0);public static Money FiveDollar = new Money(0, 0, 0, 0, 1, 0);public static Money TwentyDollar = new Money(0, 0, 0, 0, 0, 1);private final int oneCentCount;private final int tenCentCount;private final int quarterCount;private final int oneDollarCount;private final int fiveDollarCount;private final int twentyDollarCount;private float amount;public float getAmount() {return oneCentCount * 0.01f + tenCentCount * 0.10f + quarterCount * 0.25f +oneDollarCount * 1f+ fiveDollarCount * 5f + twentyDollarCount * 20f;}public Money(int oneCentCount, int tenCentCount, int quarterCount, intoneDollarCount, int fiveDollarCount, int twentyDollarCount) {if (oneCentCount < 0)throw new IllegalStateException();if (tenCentCount < 0)throw new IllegalStateException();if (quarterCount < 0)throw new IllegalStateException();if (oneDollarCount < 0)throw new IllegalStateException();if (fiveDollarCount < 0)throw new IllegalStateException();if (twentyDollarCount < 0)throw new IllegalStateException();this.oneCentCount = oneCentCount;this.tenCentCount = tenCentCount;this.quarterCount = quarterCount;this.oneDollarCount = oneDollarCount;this.fiveDollarCount = fiveDollarCount;this.twentyDollarCount = twentyDollarCount;}public Money substract(Money other) {return new Money(oneCentCount - other.oneCentCount,tenCentCount - other.tenCentCount,quarterCount - other.quarterCount,oneDollarCount - other.oneDollarCount,fiveDollarCount - other.fiveDollarCount,twentyDollarCount - other.twentyDollarCount);}public static Money add(Money money1, Money money2) {return new Money(money1.oneCentCount + money2.oneCentCount,money1.tenCentCount + money2.tenCentCount,money1.quarterCount + money2.quarterCount,money1.oneDollarCount + money2.oneDollarCount,money1.fiveDollarCount + money2.fiveDollarCount,money1.twentyDollarCount + money2.twentyDollarCount);}@Overrideprotected boolean equalsCore(Money other) {return oneCentCount == other.oneCentCount&& tenCentCount == other.tenCentCount&& quarterCount == other.quarterCount&& oneDollarCount == other.oneDollarCount&& fiveDollarCount == other.fiveDollarCount&& twentyDollarCount == other.twentyDollarCount;}@Overrideprotected int getHashCodeCore() {int hashCode = oneCentCount;hashCode = (hashCode * 397) ^ tenCentCount;hashCode = (hashCode * 397) ^ quarterCount;hashCode = (hashCode * 397) ^ oneDollarCount;hashCode = (hashCode * 397) ^ fiveDollarCount;hashCode = (hashCode * 397) ^ twentyDollarCount;return hashCode;}}
同时我们也对SnackMachine进行重构:
package com.lugew.springbootddd.snackmachine;import com.lugew.springbootddd.Entity;import lombok.Getter;import lombok.Setter;import static com.lugew.springbootddd.snackmachine.Money.None;@Getter@Setterpublic final class SnackMachine extends Entity {private Money moneyInside;private Money moneyInTransaction;public SnackMachine() {moneyInside = None;moneyInTransaction = None;}public void insertMoney(Money money) {moneyInTransaction = Money.add(moneyInTransaction, money);}public void returnMoney() {// moneyInTransaction = None;}public void buySnack() {moneyInside = Money.add(moneyInside, moneyInTransaction);// moneyInTransaction = None;}}
SnackMachine初始化了账户金额和交易金额,我们运行测试查看结果:
测试失败,可以清晰地看到,调用returnMoney时返回的是0.0,而期望值是1.0。因为在SnackMachine中我们并没有实现功能。
public void returnMoney() {// moneyInTransaction = None;}
TDD的使用可以参考我翻译的Java Testing with Spock
returnMoney的功能是清空投币金额,有一种方法是给Money添加一个clear功能:
moneyInTransaction.clear();
但是这违背了值对象不可变的原则,所以不推荐这种做法。我们可以直接将投币金额清零:
public void returnMoney() {moneyInTransaction = new Money(0,0,0,0,0,0);}
此时,再运行测试:
测试通过。我们再次重构进行优化:
public static Money None = new Money(0, 0, 0, 0, 0, 0);public static Money Cent = new Money(1, 0, 0, 0, 0, 0);public static Money TenCent = new Money(0, 1, 0, 0, 0, 0);public static Money Quarter = new Money(0, 0, 1, 0, 0, 0);public static Money Dollar = new Money(0, 0, 0, 1, 0, 0);public static Money FiveDollar = new Money(0, 0, 0, 0, 1, 0);public static Money TwentyDollar = new Money(0, 0, 0, 0, 0, 1);
public void returnMoney() {moneyInTransaction = None;}
在Money类中添加静态变量,同时重构returnMoney方法。
添加除Dollar外的变量违背了
YAGNI原则,望知晓。
接下来我们对insertMoney方法进行测试:
@Testpublic void inserted_money_goes_to_money_in_transaction() {SnackMachine snackMachine = new SnackMachine();snackMachine.insertMoney(Cent);snackMachine.insertMoney(Dollar);assertEquals(snackMachine.getMoneyInTransaction().getAmount(), 1.01f, 0);}
结果:
投币金额是相符的。我们还需测试另一种情况:不能投2分钱。
是否还有其它情况需要和领域专家讨论,我们测试的所有例子都是符合常理或和领域专家核实讨论过的。
@Testpublic void cannot_insert_more_than_one_coin_or_note_at_a_time() {SnackMachine snackMachine = new SnackMachine();Money twoCent = Money.add(Cent, Cent);assertThrows(IllegalStateException.class, () -> {snackMachine.insertMoney(twoCent);});}
如果投入2分钱时,方法会抛出IllegalStateException异常,那么测试通过。下面是实现步骤:
public void insertMoney(Money money) {Money[] coinsAndNotes = {Money.Cent, Money.TenCent, Money.Quarter,Money.Dollar, Money.FiveDollar,Money.TwentyDollar};if (!Arrays.asList(coinsAndNotes).contains(money))throw new IllegalStateException();moneyInTransaction = Money.add(moneyInTransaction, money);}

最后,我们再完善buySnack方法:
@Testpublic void money_in_transaction_goes_to_money_inside_after_purchase() {SnackMachine snackMachine = new SnackMachine();snackMachine.insertMoney(Dollar);snackMachine.insertMoney(Dollar);snackMachine.buySnack();assertEquals(snackMachine.getMoneyInTransaction(), None);assertEquals(snackMachine.getMoneyInside().getAmount(), 2, 0);}
购买零食后投币金额清零,售货机金额为2。
public void buySnack() {moneyInside = Money.add(moneyInside, moneyInTransaction);moneyInTransaction = None;}

👆至此,我们完成了售货机的初稿,还差很多细节,比如buySnack方法没有找零,也没有返回零食。以上的功能将会在聚合根和仓库章节中介绍。其它的测试例子读者可以自行编写。
