接下来我们可以使用TDD的方式实现SnackMachine的returenMoney方法。首先创建SnackMachineTest类:
    image.png

    1. package com.lugew.springbootddd.snackmachine;
    2. import org.junit.jupiter.api.Test;
    3. import static com.lugew.springbootddd.snackmachine.Money.Dollar;
    4. import static org.junit.jupiter.api.Assertions.assertEquals;
    5. /**
    6. * @author 夏露桂
    7. * @since 2021/6/9 18:12
    8. */
    9. public class SnackMachineTest {
    10. @Test
    11. public void return_money_empties_money_in_transaction() {
    12. SnackMachine snackMachine = new SnackMachine();
    13. snackMachine.insertMoney(Dollar);
    14. snackMachine.returnMoney();
    15. assertEquals(snackMachine.getMoneyInTransaction().getAmount(), 0, 0);
    16. }
    17. }

    Dollar对象是Money类中的字段:

    1. package com.lugew.springbootddd.snackmachine;
    2. import com.lugew.springbootddd.ValueObject;
    3. /**
    4. * @author 夏露桂
    5. * @since 2021/6/7 11:59
    6. */
    7. public class Money extends ValueObject<Money> {
    8. public static Money None = new Money(0, 0, 0, 0, 0, 0);
    9. public static Money Cent = new Money(1, 0, 0, 0, 0, 0);
    10. public static Money TenCent = new Money(0, 1, 0, 0, 0, 0);
    11. public static Money Quarter = new Money(0, 0, 1, 0, 0, 0);
    12. public static Money Dollar = new Money(0, 0, 0, 1, 0, 0);
    13. public static Money FiveDollar = new Money(0, 0, 0, 0, 1, 0);
    14. public static Money TwentyDollar = new Money(0, 0, 0, 0, 0, 1);
    15. private final int oneCentCount;
    16. private final int tenCentCount;
    17. private final int quarterCount;
    18. private final int oneDollarCount;
    19. private final int fiveDollarCount;
    20. private final int twentyDollarCount;
    21. private float amount;
    22. public float getAmount() {
    23. return oneCentCount * 0.01f + tenCentCount * 0.10f + quarterCount * 0.25f +
    24. oneDollarCount * 1f
    25. + fiveDollarCount * 5f + twentyDollarCount * 20f;
    26. }
    27. public Money(int oneCentCount, int tenCentCount, int quarterCount, int
    28. oneDollarCount, int fiveDollarCount, int twentyDollarCount) {
    29. if (oneCentCount < 0)
    30. throw new IllegalStateException();
    31. if (tenCentCount < 0)
    32. throw new IllegalStateException();
    33. if (quarterCount < 0)
    34. throw new IllegalStateException();
    35. if (oneDollarCount < 0)
    36. throw new IllegalStateException();
    37. if (fiveDollarCount < 0)
    38. throw new IllegalStateException();
    39. if (twentyDollarCount < 0)
    40. throw new IllegalStateException();
    41. this.oneCentCount = oneCentCount;
    42. this.tenCentCount = tenCentCount;
    43. this.quarterCount = quarterCount;
    44. this.oneDollarCount = oneDollarCount;
    45. this.fiveDollarCount = fiveDollarCount;
    46. this.twentyDollarCount = twentyDollarCount;
    47. }
    48. public Money substract(Money other) {
    49. return new Money(
    50. oneCentCount - other.oneCentCount,
    51. tenCentCount - other.tenCentCount,
    52. quarterCount - other.quarterCount,
    53. oneDollarCount - other.oneDollarCount,
    54. fiveDollarCount - other.fiveDollarCount,
    55. twentyDollarCount - other.twentyDollarCount);
    56. }
    57. public static Money add(Money money1, Money money2) {
    58. return new Money(
    59. money1.oneCentCount + money2.oneCentCount,
    60. money1.tenCentCount + money2.tenCentCount,
    61. money1.quarterCount + money2.quarterCount,
    62. money1.oneDollarCount + money2.oneDollarCount,
    63. money1.fiveDollarCount + money2.fiveDollarCount,
    64. money1.twentyDollarCount + money2.twentyDollarCount);
    65. }
    66. @Override
    67. protected boolean equalsCore(Money other) {
    68. return oneCentCount == other.oneCentCount
    69. && tenCentCount == other.tenCentCount
    70. && quarterCount == other.quarterCount
    71. && oneDollarCount == other.oneDollarCount
    72. && fiveDollarCount == other.fiveDollarCount
    73. && twentyDollarCount == other.twentyDollarCount;
    74. }
    75. @Override
    76. protected int getHashCodeCore() {
    77. int hashCode = oneCentCount;
    78. hashCode = (hashCode * 397) ^ tenCentCount;
    79. hashCode = (hashCode * 397) ^ quarterCount;
    80. hashCode = (hashCode * 397) ^ oneDollarCount;
    81. hashCode = (hashCode * 397) ^ fiveDollarCount;
    82. hashCode = (hashCode * 397) ^ twentyDollarCount;
    83. return hashCode;
    84. }
    85. }

    同时我们也对SnackMachine进行重构:

    1. package com.lugew.springbootddd.snackmachine;
    2. import com.lugew.springbootddd.Entity;
    3. import lombok.Getter;
    4. import lombok.Setter;
    5. import static com.lugew.springbootddd.snackmachine.Money.None;
    6. @Getter
    7. @Setter
    8. public final class SnackMachine extends Entity {
    9. private Money moneyInside;
    10. private Money moneyInTransaction;
    11. public SnackMachine() {
    12. moneyInside = None;
    13. moneyInTransaction = None;
    14. }
    15. public void insertMoney(Money money) {
    16. moneyInTransaction = Money.add(moneyInTransaction, money);
    17. }
    18. public void returnMoney() {
    19. // moneyInTransaction = None;
    20. }
    21. public void buySnack() {
    22. moneyInside = Money.add(moneyInside, moneyInTransaction);
    23. // moneyInTransaction = None;
    24. }
    25. }

    SnackMachine初始化了账户金额和交易金额,我们运行测试查看结果:
    image.png
    测试失败,可以清晰地看到,调用returnMoney时返回的是0.0,而期望值是1.0。因为在SnackMachine中我们并没有实现功能。

    1. public void returnMoney() {
    2. // moneyInTransaction = None;
    3. }

    TDD的使用可以参考我翻译的Java Testing with Spock

    returnMoney的功能是清空投币金额,有一种方法是给Money添加一个clear功能:

    1. moneyInTransaction.clear();

    但是这违背了值对象不可变的原则,所以不推荐这种做法。我们可以直接将投币金额清零:

    1. public void returnMoney() {
    2. moneyInTransaction = new Money(0,0,0,0,0,0);
    3. }

    此时,再运行测试:
    image.png
    测试通过。我们再次重构进行优化:

    1. public static Money None = new Money(0, 0, 0, 0, 0, 0);
    2. public static Money Cent = new Money(1, 0, 0, 0, 0, 0);
    3. public static Money TenCent = new Money(0, 1, 0, 0, 0, 0);
    4. public static Money Quarter = new Money(0, 0, 1, 0, 0, 0);
    5. public static Money Dollar = new Money(0, 0, 0, 1, 0, 0);
    6. public static Money FiveDollar = new Money(0, 0, 0, 0, 1, 0);
    7. public static Money TwentyDollar = new Money(0, 0, 0, 0, 0, 1);
    1. public void returnMoney() {
    2. moneyInTransaction = None;
    3. }

    在Money类中添加静态变量,同时重构returnMoney方法。

    添加除Dollar外的变量违背了YAGNI原则,望知晓。

    接下来我们对insertMoney方法进行测试:

    1. @Test
    2. public void inserted_money_goes_to_money_in_transaction() {
    3. SnackMachine snackMachine = new SnackMachine();
    4. snackMachine.insertMoney(Cent);
    5. snackMachine.insertMoney(Dollar);
    6. assertEquals(snackMachine.getMoneyInTransaction().getAmount(), 1.01f, 0);
    7. }

    结果:
    image.png
    投币金额是相符的。我们还需测试另一种情况:不能投2分钱。

    是否还有其它情况需要和领域专家讨论,我们测试的所有例子都是符合常理或和领域专家核实讨论过的。

    1. @Test
    2. public void cannot_insert_more_than_one_coin_or_note_at_a_time() {
    3. SnackMachine snackMachine = new SnackMachine();
    4. Money twoCent = Money.add(Cent, Cent);
    5. assertThrows(IllegalStateException.class, () -> {
    6. snackMachine.insertMoney(twoCent);
    7. });
    8. }

    如果投入2分钱时,方法会抛出IllegalStateException异常,那么测试通过。下面是实现步骤:
    image.png

    1. public void insertMoney(Money money) {
    2. Money[] coinsAndNotes = {Money.Cent, Money.TenCent, Money.Quarter,
    3. Money.Dollar, Money.FiveDollar,
    4. Money.TwentyDollar};
    5. if (!Arrays.asList(coinsAndNotes).contains(money))
    6. throw new IllegalStateException();
    7. moneyInTransaction = Money.add(moneyInTransaction, money);
    8. }

    image.png
    最后,我们再完善buySnack方法:

    1. @Test
    2. public void money_in_transaction_goes_to_money_inside_after_purchase() {
    3. SnackMachine snackMachine = new SnackMachine();
    4. snackMachine.insertMoney(Dollar);
    5. snackMachine.insertMoney(Dollar);
    6. snackMachine.buySnack();
    7. assertEquals(snackMachine.getMoneyInTransaction(), None);
    8. assertEquals(snackMachine.getMoneyInside().getAmount(), 2, 0);
    9. }

    购买零食后投币金额清零,售货机金额为2。
    image.png

    1. public void buySnack() {
    2. moneyInside = Money.add(moneyInside, moneyInTransaction);
    3. moneyInTransaction = None;
    4. }

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