下一个需求不像想象中那么简单,当用户投4个0.25元和1元,购买1美元的零食,我们应该接受哪种面额的货币?这种情况是没有料想到的,这时可以和领域专家讨论。在这里,售货机有限接收低面值的货币,返回高面值的1元。
package com.lugew.springbootddd.snackmachine;import com.lugew.springbootddd.ValueObject;import lombok.Getter;/*** @author 夏露桂* @since 2021/6/7 11:59*/@Getterpublic 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(float amount, int oneCentCount, int tenCentCount, int quarterCount, intoneDollarCount, int fiveDollarCount, int twentyDollarCount) {this(oneCentCount, tenCentCount, quarterCount, oneDollarCount, fiveDollarCount, twentyDollarCount);if (amount < 0) {throw new IllegalStateException();}this.amount = amount;}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);}public Money add(Money other) {return new Money(other.oneCentCount + this.oneCentCount,other.tenCentCount + this.tenCentCount,other.quarterCount + this.quarterCount,other.oneDollarCount + this.oneDollarCount,other.fiveDollarCount + this.fiveDollarCount,other.twentyDollarCount + this.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;}@Overridepublic String toString() {if (getAmount() < 1)return "¢" + getAmount() * 100;return "$" + getAmount();}}
package com.lugew.springbootddd.snackmachine;import com.lugew.springbootddd.AggregateRoot;import com.lugew.springbootddd.Slot;import com.lugew.springbootddd.SnackMachineDto;import com.lugew.springbootddd.SnackPile;import lombok.Getter;import lombok.Setter;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import static com.lugew.springbootddd.snackmachine.Money.None;public final class SnackMachine extends AggregateRoot {@Getter@Setterprivate Money moneyInside;@Getter@Setterprivate float moneyInTransaction;private List<Slot> slots;public SnackMachine() {moneyInside = None;moneyInTransaction = 0;slots = new ArrayList<>();slots.add(new Slot(this, 1));slots.add(new Slot(this, 2));slots.add(new Slot(this, 3));}public Slot getSlot(int position) {return slots.stream().filter(x -> x.getPosition() ==position).findAny().orElse(null);}public SnackPile getSnackPile(int position) {return getSlot(position).getSnackPile();}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 = moneyInTransaction + money.getAmount();moneyInside = moneyInside.add(money);}public void returnMoney() {moneyInTransaction = 0;}public void buySnack(int position) {Slot slot = getSlot(position);if (slot.getSnackPile().getPrice() > moneyInTransaction) {throw new IllegalStateException();}slot.setSnackPile(slot.getSnackPile().subtractOne());moneyInTransaction = 0;}public SnackMachineDto convertToSnackMachineDto() {SnackMachineDto snackMachineDto = new SnackMachineDto();snackMachineDto.setId(id);snackMachineDto.setMoneyInTransaction(moneyInTransaction);snackMachineDto.setOneCentCount(moneyInside.getOneCentCount());snackMachineDto.setTenCentCount(moneyInside.getTenCentCount());snackMachineDto.setQuarterCount(moneyInside.getQuarterCount());snackMachineDto.setOneDollarCount(moneyInside.getOneDollarCount());snackMachineDto.setFiveDollarCount(moneyInside.getFiveDollarCount());snackMachineDto.setTwentyDollarCount(moneyInside.getTwentyDollarCount());return snackMachineDto;}public void loadSnacks(int position, SnackPile snackPile) {Slot slot = slots.stream().filter(x -> x.getPosition() ==position).findAny().orElse(null);if (slot != null) {slot.setSnackPile(snackPile);}}}
package com.lugew.springbootddd;import com.lugew.springbootddd.snackmachine.SnackMachine;import lombok.RequiredArgsConstructor;import org.springframework.web.bind.annotation.*;import static com.lugew.springbootddd.snackmachine.Money.*;/*** @author 夏露桂* @since 2021/6/10 11:55*/@RestController@RequestMapping("snackmachines")@RequiredArgsConstructorpublic class SnackMachineController {private final SnackMachineRepository snackMachineRepository;@GetMapping("/{id}")@ResponseBodypublic SnackMachineDto getSnackMachine(@PathVariable("id") long id) {return snackMachineRepository.findById(id).orElse(null);}@PutMapping("/{id}/{slotNumber}")public void buySnack(@PathVariable("id") long id, @PathVariable("slotNumber")int slotNumber) {SnackMachineDto snackMachineDto =snackMachineRepository.findById(id).orElse(null);SnackMachine snackMachine = snackMachineDto.convertToSnackMachine();snackMachine.buySnack(slotNumber);snackMachineRepository.save(snackMachine.convertToSnackMachineDto());}@PutMapping("/{id}/moneyInTransaction/{coinOrNote}")public void insertCoinOrNote(@PathVariable("id") long id, @PathVariable("coinOrNote") String coinOrNote) {SnackMachineDto snackMachineDto =snackMachineRepository.findById(id).orElse(null);SnackMachine snackMachine = snackMachineDto.convertToSnackMachine();if (coinOrNote.equalsIgnoreCase("Cent")) snackMachine.insertMoney(Cent);else if (coinOrNote.equalsIgnoreCase("TenCent"))snackMachine.insertMoney(TenCent);else if (coinOrNote.equalsIgnoreCase("Quarter"))snackMachine.insertMoney(Quarter);else if (coinOrNote.equalsIgnoreCase("Dollar"))snackMachine.insertMoney(Dollar);else if (coinOrNote.equalsIgnoreCase("FiveDollar"))snackMachine.insertMoney(FiveDollar);else if (coinOrNote.equalsIgnoreCase("TwentyDollar"))snackMachine.insertMoney(TwentyDollar);snackMachineRepository.save(snackMachine.convertToSnackMachineDto());}@PutMapping("/{id}/moneyInTransaction")public void returnMoney(@PathVariable("id") long id) {SnackMachineDto snackMachineDto =snackMachineRepository.findById(id).orElse(null);SnackMachine snackMachine = snackMachineDto.convertToSnackMachine();snackMachine.returnMoney();snackMachineRepository.save(snackMachine.convertToSnackMachineDto());}/* public Money getWholeMoney(SnackMachine snackMachine) {return Money.add(snackMachine.getMoneyInside(), snackMachine.getMoneyInTransaction());}*/}
package com.lugew.springbootddd;import com.lugew.springbootddd.snackmachine.Money;import com.lugew.springbootddd.snackmachine.SnackMachine;import lombok.Getter;import lombok.Setter;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;/*** @author 夏露桂* @since 2021/6/10 12:00*/@Getter@Setter@Entitypublic class SnackMachineDto {@Id@GeneratedValueprivate long id;private int oneCentCount;private int tenCentCount;private int quarterCount;private int oneDollarCount;private int fiveDollarCount;private int twentyDollarCount;private float moneyInTransaction;public SnackMachine convertToSnackMachine() {SnackMachine snackMachine = new SnackMachine();snackMachine.setId(id);snackMachine.setMoneyInTransaction(moneyInTransaction);snackMachine.setMoneyInside(newMoney(oneCentCount, tenCentCount, quarterCount, oneDollarCount, fiveDollarCount, twentyDollarCount));return snackMachine;}}
package com.lugew.springbootddd.snackmachine;import com.lugew.springbootddd.Snack;import com.lugew.springbootddd.SnackPile;import org.junit.jupiter.api.Test;import static com.lugew.springbootddd.snackmachine.Money.Cent;import static com.lugew.springbootddd.snackmachine.Money.Dollar;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertThrows;/*** @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(), 0, 0);}@Testpublic void inserted_money_goes_to_money_in_transaction() {SnackMachine snackMachine = new SnackMachine();snackMachine.insertMoney(Cent);snackMachine.insertMoney(Dollar);assertEquals(snackMachine.getMoneyInTransaction(), 1.01f, 0);}@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);});}@Testpublic void buySnack_trades_inserted_money_for_a_snack() {SnackMachine snackMachine = new SnackMachine();snackMachine.loadSnacks(1, new SnackPile(new Snack("Some snack"), 10, 1));snackMachine.insertMoney(Dollar);snackMachine.buySnack(1);assertEquals(snackMachine.getMoneyInTransaction(), Money.None.getAmount());assertEquals(snackMachine.getMoneyInside().getAmount(), 1, 0.5);assertEquals(snackMachine.getSnackPile(1).getQuantity(), 9);}@Testpublic void cannot_make_purchase_when_there_is_no_snacks() {SnackMachine snackMachine = new SnackMachine();assertThrows(IllegalStateException.class, () -> {snackMachine.buySnack(1);});}@Testpublic void cannot_make_purchase_if_not_enough_money_inserted() {SnackMachine snackMachine = new SnackMachine();snackMachine.loadSnacks(1, new SnackPile(new Snack("Some snack"), 1, 2));snackMachine.insertMoney(Dollar);assertThrows(IllegalStateException.class, () -> {snackMachine.buySnack(1);});}/* @Testpublic void snack_machine_returns_money_with_highest_denomination_first() {SnackMachine snackMachine = new SnackMachine();snackMachine.loadMoney(Dollar);snackMachine.insertMoney(Quarter);snackMachine.insertMoney(Quarter);snackMachine.insertMoney(Quarter);snackMachine.insertMoney(Quarter);snackMachine.returnMoney();assertEquals(snackMachine.getMoneyInside().getQuarterCount(), 4);assertEquals(snackMachine.getMoneyInside().getOneDollarCount(), 0);}*/}
我们将moneyInTransaction重构为float类型,因为moneyInTransaction和找零的数额可能是不同的(找零优先高面额)。在此重构了和此字段有关的所有类。还需要重构Controller和SnackMachine:
package com.lugew.springbootddd;import com.lugew.springbootddd.snackmachine.SnackMachine;import lombok.RequiredArgsConstructor;import org.springframework.web.bind.annotation.*;import static com.lugew.springbootddd.snackmachine.Money.*;/*** @author 夏露桂* @since 2021/6/10 11:55*/@RestController@RequestMapping("snackmachines")@RequiredArgsConstructorpublic class SnackMachineController {private final SnackMachineRepository snackMachineRepository;@GetMapping("/{id}")@ResponseBodypublic SnackMachineDto getSnackMachine(@PathVariable("id") long id) {return snackMachineRepository.findById(id).orElse(null);}@PutMapping("/{id}/moneyInTransaction/{coinOrNote}")public void insertCoinOrNote(@PathVariable("id") long id, @PathVariable("coinOrNote") String coinOrNote) {SnackMachineDto snackMachineDto =snackMachineRepository.findById(id).orElse(null);SnackMachine snackMachine = snackMachineDto.convertToSnackMachine();if (coinOrNote.equalsIgnoreCase("Cent")) snackMachine.insertMoney(Cent);else if (coinOrNote.equalsIgnoreCase("TenCent"))snackMachine.insertMoney(TenCent);else if (coinOrNote.equalsIgnoreCase("Quarter"))snackMachine.insertMoney(Quarter);else if (coinOrNote.equalsIgnoreCase("Dollar"))snackMachine.insertMoney(Dollar);else if (coinOrNote.equalsIgnoreCase("FiveDollar"))snackMachine.insertMoney(FiveDollar);else if (coinOrNote.equalsIgnoreCase("TwentyDollar"))snackMachine.insertMoney(TwentyDollar);snackMachineRepository.save(snackMachine.convertToSnackMachineDto());}@PutMapping("/{id}/moneyInTransaction")public void returnMoney(@PathVariable("id") long id) {SnackMachineDto snackMachineDto =snackMachineRepository.findById(id).orElse(null);SnackMachine snackMachine = snackMachineDto.convertToSnackMachine();snackMachine.returnMoney();snackMachineRepository.save(snackMachine.convertToSnackMachineDto());}@PutMapping("/{id}/{slotNumber}")public void buySnack(@PathVariable("id") long id, @PathVariable("slotNumber")int slotNumber) {SnackMachineDto snackMachineDto =snackMachineRepository.findById(id).orElse(null);SnackMachine snackMachine = snackMachineDto.convertToSnackMachine();snackMachine.buySnack(slotNumber);snackMachineRepository.save(snackMachine.convertToSnackMachineDto());}}
package com.lugew.springbootddd.snackmachine;import com.lugew.springbootddd.*;import lombok.Getter;import lombok.Setter;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import static com.lugew.springbootddd.snackmachine.Money.None;public final class SnackMachine extends AggregateRoot {@Getter@Setterprivate Money moneyInside;@Getter@Setterprivate float moneyInTransaction;@Setterprivate List<Slot> slots;public SnackMachine() {moneyInside = None;moneyInTransaction = 0;slots = new ArrayList<>();slots.add(new Slot(this, 1));slots.add(new Slot(this, 2));slots.add(new Slot(this, 3));}public Slot getSlot(int position) {return slots.stream().filter(x -> x.getPosition() ==position).findAny().orElse(null);}public SnackPile getSnackPile(int position) {return getSlot(position).getSnackPile();}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 = moneyInTransaction + money.getAmount();moneyInside = moneyInside.add(money);}public void returnMoney() {moneyInTransaction = 0;}public void buySnack(int position) {Slot slot = getSlot(position);if (slot.getSnackPile().getPrice() > moneyInTransaction) {throw new IllegalStateException();}slot.setSnackPile(slot.getSnackPile().subtractOne());moneyInTransaction = 0;}public SnackMachineDto convertToSnackMachineDto() {SnackMachineDto snackMachineDto = new SnackMachineDto();snackMachineDto.setId(id);snackMachineDto.setMoneyInTransaction(moneyInTransaction);List<SlotDto> slotDtoList = new ArrayList<>();for (Slot slot : slots) slotDtoList.add(slot.convertToSlotDto());snackMachineDto.setSlotDtoList(slotDtoList);snackMachineDto.setOneCentCount(moneyInside.getOneCentCount());snackMachineDto.setTenCentCount(moneyInside.getTenCentCount());snackMachineDto.setQuarterCount(moneyInside.getQuarterCount());snackMachineDto.setOneDollarCount(moneyInside.getOneDollarCount());snackMachineDto.setFiveDollarCount(moneyInside.getFiveDollarCount());snackMachineDto.setTwentyDollarCount(moneyInside.getTwentyDollarCount());return snackMachineDto;}public void loadSnacks(int position, SnackPile snackPile) {Slot slot = slots.stream().filter(x -> x.getPosition() ==position).findAny().orElse(null);if (slot != null) {slot.setSnackPile(snackPile);}}public void loadMoney(Money money) {moneyInside = moneyInside.add(money);}}
package com.lugew.springbootddd;import com.lugew.springbootddd.snackmachine.Money;import com.lugew.springbootddd.snackmachine.SnackMachine;import lombok.Getter;import lombok.Setter;import javax.persistence.Entity;import javax.persistence.*;import java.util.ArrayList;import java.util.List;/*** @author 夏露桂* @since 2021/6/10 12:00*/@Getter@Setter@Entitypublic class SnackMachineDto {@Id@GeneratedValueprivate long id;private int oneCentCount;private int tenCentCount;private int quarterCount;private int oneDollarCount;private int fiveDollarCount;private int twentyDollarCount;private float moneyInTransaction;@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)@JoinColumn(name = "snackMachineId")private List<SlotDto> slotDtoList;public SnackMachine convertToSnackMachine() {SnackMachine snackMachine = new SnackMachine();snackMachine.setId(id);snackMachine.setMoneyInTransaction(moneyInTransaction);snackMachine.setMoneyInside(newMoney(oneCentCount, tenCentCount, quarterCount,oneDollarCount, fiveDollarCount, twentyDollarCount));List<Slot> slotList = new ArrayList<>();for (SlotDto slotDto : slotDtoList) {slotList.add(slotDto.convertToSlot());}snackMachine.setSlots(slotList);return snackMachine;}}
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)@JoinColumn(name = "snackMachineId")private List<SlotDto> slotDtoList;
一对多关系会改变数据库,Springboot已帮我们实现。
@Testpublic void snack_machine_returns_money_with_highest_denomination_first() {SnackMachine snackMachine = new SnackMachine();snackMachine.loadMoney(Dollar);snackMachine.insertMoney(Quarter);snackMachine.insertMoney(Quarter);snackMachine.insertMoney(Quarter);snackMachine.insertMoney(Quarter);snackMachine.returnMoney();assertEquals(snackMachine.getMoneyInside().getQuarterCount(), 4);assertEquals(snackMachine.getMoneyInside().getOneDollarCount(), 0);}
显然测试是失败的:
这是我们需要重构returnMoney方法。我们给Money类引入allocate累加方法:
public void returnMoney() {Money moneyToReturn = moneyInside.allocate(moneyInTransaction);moneyInside = moneyInside.substract(moneyToReturn);moneyInTransaction = 0;}
public boolean canAllocate(float amount) {Money money = allocateCore(amount);return money.amount == amount;}public Money allocate(float amount) {if (!canAllocate(amount))throw new IllegalStateException();return allocateCore(amount);}private Money allocateCore(float amount) {int twentyDollarCount = Math.min((int) (amount / 20), this.twentyDollarCount);amount = amount - twentyDollarCount * 20;int fiveDollarCount = Math.min((int) (amount / 5), this.fiveDollarCount);amount = amount - fiveDollarCount * 5;int oneDollarCount = Math.min((int) amount, this.oneDollarCount);amount = amount - oneDollarCount;int quarterCount = Math.min((int) (amount / 0.25f), this.quarterCount);amount = amount - quarterCount * 0.25f;int tenCentCount = Math.min((int) (amount / 0.1f), this.tenCentCount);amount = amount - tenCentCount * 0.1f;int oneCentCount = Math.min((int) (amount / 0.01f), this.oneCentCount);return new Money(oneCentCount,tenCentCount,quarterCount,oneDollarCount,fiveDollarCount,twentyDollarCount);}
另一个是找零功能:
@Testpublic void after_purchase_change_is_returned() {SnackMachine snackMachine = new SnackMachine();snackMachine.loadSnacks(1, new SnackPile(new Snack("Some snack"), 1,0.5f));snackMachine.loadMoney(new Money(0, 10, 0, 0, 0, 0));snackMachine.insertMoney(Dollar);snackMachine.buySnack(1);assertEquals(snackMachine.getMoneyInside().getAmount(), 1.5, 0);assertEquals(snackMachine.getMoneyInTransaction(), 0, 0);}

最后是无法找零禁止购买:
@Testpublic void cannot_buy_snack_if_not_enough_change() {SnackMachine snackMachine = new SnackMachine();snackMachine.loadSnacks(1, new SnackPile(new Snack("Some snack"), 1, 0.5f));snackMachine.insertMoney(Dollar);assertThrows(IllegalStateException.class, () -> {snackMachine.buySnack(1);});}

