首先我们对Snack和SnackMachine重构:

    1. package com.lugew.springbootddd;
    2. import lombok.Getter;
    3. import lombok.Setter;
    4. /**
    5. * @author 夏露桂
    6. * @since 2021/6/16 10:45
    7. */
    8. @Getter
    9. @Setter
    10. public class Snack extends AggregateRoot {
    11. private String name;
    12. public Snack() {
    13. }
    14. public Snack(String name) {
    15. this.name = name;
    16. }
    17. }
    1. package com.lugew.springbootddd.snackmachine;
    2. import com.lugew.springbootddd.AggregateRoot;
    3. import com.lugew.springbootddd.Slot;
    4. import com.lugew.springbootddd.Snack;
    5. import com.lugew.springbootddd.SnackMachineDto;
    6. import lombok.Getter;
    7. import lombok.Setter;
    8. import java.util.ArrayList;
    9. import java.util.Arrays;
    10. import java.util.List;
    11. import static com.lugew.springbootddd.snackmachine.Money.None;
    12. @Getter
    13. @Setter
    14. public final class SnackMachine extends AggregateRoot {
    15. private Money moneyInside;
    16. private Money moneyInTransaction;
    17. private List<Slot> slots;
    18. public SnackMachine() {
    19. moneyInside = None;
    20. moneyInTransaction = None;
    21. slots = new ArrayList<>();
    22. slots.add(new Slot(this, 1, null, 0, 1));
    23. slots.add(new Slot(this, 2, null, 0, 1));
    24. slots.add(new Slot(this, 3, null, 0, 1));
    25. }
    26. public void insertMoney(Money money) {
    27. Money[] coinsAndNotes = {Money.Cent, Money.TenCent, Money.Quarter,
    28. Money.Dollar, Money.FiveDollar,
    29. Money.TwentyDollar};
    30. if (!Arrays.asList(coinsAndNotes).contains(money))
    31. throw new IllegalStateException();
    32. moneyInTransaction = Money.add(moneyInTransaction, money);
    33. }
    34. public void returnMoney() {
    35. moneyInTransaction = None;
    36. }
    37. public void buySnack(int position) {
    38. Slot slot = slots.stream().filter(x -> x.getPosition() ==
    39. position).findAny().orElse(null);
    40. slot.setQuantity(slot.getQuantity() - 1);
    41. moneyInside = Money.add(moneyInside, moneyInTransaction);
    42. moneyInTransaction = None;
    43. }
    44. public SnackMachineDto convertToSnackMachineDto() {
    45. SnackMachineDto snackMachineDto = new SnackMachineDto();
    46. snackMachineDto.setId(id);
    47. snackMachineDto.setMoneyInTransaction(moneyInTransaction.getAmount());
    48. snackMachineDto.setOneCentCount(moneyInside.getOne
    49. CentCount());
    50. snackMachineDto.setTenCentCount(moneyInside.getTenCentCount());
    51. snackMachineDto.setQuarterCount(moneyInside.getQuarterCount());
    52. snackMachineDto.setOneDollarCount(moneyInside.getOneDollarCount());
    53. snackMachineDto.setFiveDollarCount(moneyInside.getFiveDollarCount());
    54. snackMachineDto.setTwentyDollarCount(moneyInside.getTwentyDollarCount());
    55. return snackMachineDto;
    56. }
    57. public void loadSnacks(int position, Snack snack, int quantity, float price) {
    58. Slot slot = slots.stream().filter(x -> x.getPosition() ==
    59. position).findAny().orElse(null);
    60. if (slot != null) {
    61. slot.setSnack(snack);
    62. slot.setQuantity(quantity);
    63. slot.setPrice(price);
    64. }
    65. }
    66. }

    此时,我们还有几处需要重构,一,List对外暴露,这是应该避免的,二,Slot类也是能够被外界访问的,这违背了我们之前提到过的外界隔离原则。避免对外暴露通常不会太容易,在我们的例子中,只要将getter和setter方法隐藏即可:

    1. package com.lugew.springbootddd.snackmachine;
    2. import com.lugew.springbootddd.AggregateRoot;
    3. import com.lugew.springbootddd.Slot;
    4. import com.lugew.springbootddd.Snack;
    5. import com.lugew.springbootddd.SnackMachineDto;
    6. import lombok.Getter;
    7. import lombok.Setter;
    8. import java.util.ArrayList;
    9. import java.util.Arrays;
    10. import java.util.List;
    11. import static com.lugew.springbootddd.snackmachine.Money.None;
    12. public final class SnackMachine extends AggregateRoot {
    13. @Getter
    14. @Setter
    15. private Money moneyInside;
    16. @Getter
    17. @Setter
    18. private Money moneyInTransaction;
    19. private List<Slot> slots;
    20. public SnackMachine() {
    21. moneyInside = None;
    22. moneyInTransaction = None;
    23. slots = new ArrayList<>();
    24. slots.add(new Slot(this, 1, null, 0, 1));
    25. slots.add(new Slot(this, 2, null, 0, 1));
    26. slots.add(new Slot(this, 3, null, 0, 1));
    27. }
    28. public void insertMoney(Money money) {
    29. Money[] coinsAndNotes = {Money.Cent, Money.TenCent, Money.Quarter,
    30. Money.Dollar, Money.FiveDollar,
    31. Money.TwentyDollar};
    32. if (!Arrays.asList(coinsAndNotes).contains(money))
    33. throw new IllegalStateException();
    34. moneyInTransaction = Money.add(moneyInTransaction, money);
    35. }
    36. public void returnMoney() {
    37. moneyInTransaction = None;
    38. }
    39. public void buySnack(int position) {
    40. Slot slot = slots.stream().filter(x -> x.getPosition() ==
    41. position).findAny().orElse(null);
    42. slot.setQuantity(slot.getQuantity() - 1);
    43. moneyInside = Money.add(moneyInside, moneyInTransaction);
    44. moneyInTransaction = None;
    45. }
    46. public SnackMachineDto convertToSnackMachineDto() {
    47. SnackMachineDto snackMachineDto = new SnackMachineDto();
    48. snackMachineDto.setId(id);
    49. snackMachineDto.setMoneyInTransaction(moneyInTransaction.getAmount());
    50. snackMachineDto.setOneCentCount(moneyInside.getOneCentCount());
    51. snackMachineDto.setTenCentCount(moneyInside.getTenCentCount());
    52. snackMachineDto.setQuarterCount(moneyInside.getQuarterCount());
    53. snackMachineDto.setOneDollarCount(moneyInside.getOneDollarCount());
    54. snackMachineDto.setFiveDollarCount(moneyInside.getFiveDollarCount());
    55. snackMachineDto.setTwentyDollarCount(moneyInside.getTwentyDollarCount());
    56. return snackMachineDto;
    57. }
    58. public void loadSnacks(int position, Snack snack, int quantity, float price) {
    59. Slot slot = slots.stream().filter(x -> x.getPosition() ==
    60. position).findAny().orElse(null);
    61. if (slot != null) {
    62. slot.setSnack(snack);
    63. slot.setQuantity(quantity);
    64. slot.setPrice(price);
    65. }
    66. }
    67. }

    image.png
    为了解决上述问题,我们可能会添加一个返回指定位置的插槽中零食数量的方法:snackMachine.getQuantityOfSnacksInSlot(1),且该方法的返回值是9。但这种方式会带来其他的问题,当我们需要插槽的其他信息时,如零食信息及其价格,那么我们必须再添加两个方法:getSnackInSlot和``getPriceInSlot。当要获取所有插槽和零食时,使用这些方法就变得异常繁琐。我们必须采取另一种方式,之前的文章中我们说过,当代码使用变得冗余时,意味着存在隐藏的抽象,Slot类中含有三个属性:Snack、Quantity和Price。这三个属性始终绑定在一起,所以我们能将之重构为一个值对象。这很有用,值对象将紧耦合的属性同时暴露。我们为之新建值对象的抽象:SnackPile:

    1. package com.lugew.springbootddd;
    2. import lombok.Getter;
    3. /**
    4. * @author 夏露桂
    5. * @since 2021/6/22 17:52
    6. */
    7. @Getter
    8. public class SnackPile extends ValueObject<SnackPile> {
    9. private Snack snack;
    10. private int quantity;
    11. private float price;
    12. private SnackPile() {
    13. }
    14. public SnackPile(Snack snack, int quantity, float price) {
    15. this.snack = snack;
    16. this.quantity = quantity;
    17. this.price = price;
    18. }
    19. public SnackPile subtractOne() {
    20. return new SnackPile(snack, getQuantity() - 1, getPrice());
    21. }
    22. @Override
    23. protected boolean equalsCore(SnackPile other) {
    24. return this.snack == other.snack && this.getQuantity() == other.getQuantity()
    25. && this.getPrice() == other.getPrice();
    26. }
    27. @Override
    28. protected int getHashCodeCore() {
    29. final int prime = 31;
    30. int result = super.hashCode();
    31. result = prime * result + Float.floatToIntBits(price);
    32. result = prime * result + quantity;
    33. result = prime * result + ((snack == null) ? 0 : snack.hashCode());
    34. return result;
    35. }
    36. }

    我们只给值对象生成get方法,保持其不可变性。随后我们重构SnackMachine属性和方法:

    1. package com.lugew.springbootddd.snackmachine;
    2. import com.lugew.springbootddd.AggregateRoot;
    3. import com.lugew.springbootddd.Slot;
    4. import com.lugew.springbootddd.SnackMachineDto;
    5. import com.lugew.springbootddd.SnackPile;
    6. import lombok.Getter;
    7. import lombok.Setter;
    8. import java.util.ArrayList;
    9. import java.util.Arrays;
    10. import java.util.List;
    11. import static com.lugew.springbootddd.snackmachine.Money.None;
    12. public final class SnackMachine extends AggregateRoot {
    13. @Getter
    14. @Setter
    15. private Money moneyInside;
    16. @Getter
    17. @Setter
    18. private Money moneyInTransaction;
    19. private List<Slot> slots;
    20. public SnackMachine() {
    21. moneyInside = None;
    22. moneyInTransaction = None;
    23. slots = new ArrayList<>();
    24. slots.add(new Slot(this, 1));
    25. slots.add(new Slot(this, 2));
    26. slots.add(new Slot(this, 3));
    27. }
    28. public void insertMoney(Money money) {
    29. Money[] coinsAndNotes = {Money.Cent, Money.TenCent, Money.Quarter,
    30. Money.Dollar, Money.FiveDollar,
    31. Money.TwentyDollar};
    32. if (!Arrays.asList(coinsAndNotes).contains(money))
    33. throw new IllegalStateException();
    34. moneyInTransaction = Money.add(moneyInTransaction, money);
    35. }
    36. public void returnMoney() {
    37. moneyInTransaction = None;
    38. }
    39. public void buySnack(int position) {
    40. Slot slot = slots.stream().filter(x -> x.getPosition() ==
    41. position).findAny().orElse(null);
    42. slot.setSnackPile(slot.getSnackPile().subtractOne());
    43. moneyInside = Money.add(moneyInside, moneyInTransaction);
    44. moneyInTransaction = None;
    45. }
    46. public SnackMachineDto convertToSnackMachineDto() {
    47. SnackMachineDto snackMachineDto = new SnackMachineDto();
    48. snackMachineDto.setId(id);
    49. snackMachineDto.setMoneyInTransaction(moneyInTransaction.getAmount());
    50. snackMachineDto.setOneCentCount(moneyInside.getOneCentCount());
    51. snackMachineDto.setTenCentCount(moneyInside.getTenCentCount());
    52. snackMachineDto.setQuarterCount(moneyInside.getQuarterCount());
    53. snackMachineDto.setOneDollarCount(moneyInside.getOneDollarCount());
    54. snackMachineDto.setFiveDollarCount(moneyInside.getFiveDollarCount());
    55. snackMachineDto.setTwentyDollarCount(moneyInside.getTwentyDollarCount());
    56. return snackMachineDto;
    57. }
    58. public void loadSnacks(int position, SnackPile snackPile) {
    59. Slot slot = slots.stream().filter(x -> x.getPosition() ==
    60. position).findAny().orElse(null);
    61. if (slot != null) {
    62. slot.setSnackPile(snackPile);
    63. }
    64. }
    65. public SnackPile getSnackPile(int position) {
    66. return slots.stream().filter(x -> x.getPosition() ==
    67. position).findAny().orElse(null).getSnackPile();
    68. }
    69. }

    我们还需要为SnackPile添加数量减少的功能,一种方法直接设置quality,这种方式非常不可取,它破坏了值对象的不可变性,取而代之的是提供公共方法,返回一个除quality外都相同的新对象。

    1. package com.lugew.springbootddd;
    2. import lombok.Getter;
    3. /**
    4. * @author 夏露桂
    5. * @since 2021/6/22 17:52
    6. */
    7. @Getter
    8. public class SnackPile extends ValueObject<SnackPile> {
    9. private Snack snack;
    10. private int quantity;
    11. private float price;
    12. private SnackPile() {
    13. }
    14. public SnackPile(Snack snack, int quantity, float price) {
    15. this.snack = snack;
    16. this.quantity = quantity;
    17. this.price = price;
    18. }
    19. public SnackPile subtractOne() {
    20. return new SnackPile(snack, getQuantity() - 1, getPrice());
    21. }
    22. @Override
    23. protected boolean equalsCore(SnackPile other) {
    24. return this.snack == other.snack && this.getQuantity() == other.getQuantity()
    25. && this.getPrice() == other.getPrice();
    26. }
    27. @Override
    28. protected int getHashCodeCore() {
    29. final int prime = 31;
    30. int result = super.hashCode();
    31. result = prime * result + Float.floatToIntBits(price);
    32. result = prime * result + quantity;
    33. result = prime * result + ((snack == null) ? 0 : snack.hashCode());
    34. return result;
    35. }
    36. }

    最后重构测试:

    1. @Test
    2. public void buySnack_trades_inserted_money_for_a_snack() {
    3. SnackMachine snackMachine = new SnackMachine();
    4. snackMachine.loadSnacks(1, new SnackPile(new Snack("Some snack"), 10, 1));
    5. snackMachine.insertMoney(Dollar);
    6. snackMachine.buySnack(1);
    7. assertEquals(snackMachine.getMoneyInTransaction(), Money.None);
    8. assertEquals(snackMachine.getMoneyInside().getAmount(), 1, 0.5);
    9. assertEquals(snackMachine.getSnackPile(1).getQuantity(), 9);
    10. }

    接下来还有一些方法需要重构:

    1. public void buySnack(int position) {
    2. Slot slot = getSlot(position);
    3. slot.setSnackPile(slot.getSnackPile().subtractOne());
    4. moneyInside = Money.add(moneyInside, moneyInTransaction);
    5. moneyInTransaction = None;
    6. }
    7. public Slot getSlot(int position) {
    8. return slots.stream().filter(x -> x.getPosition() ==
    9. position).findAny().orElse(null);
    10. }
    11. public SnackPile getSnackPile(int position) {
    12. return getSlot(position).getSnackPile();
    13. }

    同时还有SnackPile:

    1. package com.lugew.springbootddd;
    2. import lombok.Getter;
    3. /**
    4. * @author 夏露桂
    5. * @since 2021/6/22 17:52
    6. */
    7. @Getter
    8. public class SnackPile extends ValueObject<SnackPile> {
    9. private Snack snack;
    10. private int quantity;
    11. private float price;
    12. private SnackPile() {
    13. }
    14. public SnackPile(Snack snack, int quantity, float price) {
    15. if (quantity < 0)
    16. throw new IllegalStateException();
    17. if (price < 0)
    18. throw new IllegalStateException();
    19. this.snack = snack;
    20. this.quantity = quantity;
    21. this.price = price;
    22. }
    23. public SnackPile subtractOne() {
    24. return new SnackPile(snack, getQuantity() - 1, getPrice());
    25. }
    26. @Override
    27. protected boolean equalsCore(SnackPile other) {
    28. return this.snack == other.snack && this.getQuantity() == other.getQuantity()
    29. && this.getPrice() == other.getPrice();
    30. }
    31. @Override
    32. protected int getHashCodeCore() {
    33. final int prime = 31;
    34. int result = super.hashCode();
    35. result = prime * result + Float.floatToIntBits(price);
    36. result = prime * result + quantity;
    37. result = prime * result + ((snack == null) ? 0 : snack.hashCode());
    38. return result;
    39. }
    40. }

    添加异常判断,此部分的单元测试读者可自行实现。