接下来我们可以使用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 {
@Test
public 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, int
oneDollarCount, 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);
}
@Override
protected boolean equalsCore(Money other) {
return oneCentCount == other.oneCentCount
&& tenCentCount == other.tenCentCount
&& quarterCount == other.quarterCount
&& oneDollarCount == other.oneDollarCount
&& fiveDollarCount == other.fiveDollarCount
&& twentyDollarCount == other.twentyDollarCount;
}
@Override
protected 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
@Setter
public 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方法进行测试:
@Test
public 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分钱。
是否还有其它情况需要和领域专家讨论,我们测试的所有例子都是符合常理或和领域专家核实讨论过的。
@Test
public 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方法:
@Test
public 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方法没有找零,也没有返回零食。以上的功能将会在聚合根和仓库章节中介绍。其它的测试例子读者可以自行编写。