下一个需求不像想象中那么简单,当用户投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
*/
@Getter
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(float amount, int oneCentCount, int tenCentCount, int quarterCount, int
oneDollarCount, 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);
}
@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;
}
@Override
public 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
@Setter
private Money moneyInside;
@Getter
@Setter
private 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")
@RequiredArgsConstructor
public class SnackMachineController {
private final SnackMachineRepository snackMachineRepository;
@GetMapping("/{id}")
@ResponseBody
public 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
@Entity
public class SnackMachineDto {
@Id
@GeneratedValue
private 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(new
Money(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 {
@Test
public void return_money_empties_money_in_transaction() {
SnackMachine snackMachine = new SnackMachine();
snackMachine.insertMoney(Dollar);
snackMachine.returnMoney();
assertEquals(snackMachine.getMoneyInTransaction(), 0, 0);
}
@Test
public void inserted_money_goes_to_money_in_transaction() {
SnackMachine snackMachine = new SnackMachine();
snackMachine.insertMoney(Cent);
snackMachine.insertMoney(Dollar);
assertEquals(snackMachine.getMoneyInTransaction(), 1.01f, 0);
}
@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);
});
}
@Test
public 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);
}
@Test
public void cannot_make_purchase_when_there_is_no_snacks() {
SnackMachine snackMachine = new SnackMachine();
assertThrows(IllegalStateException.class, () -> {
snackMachine.buySnack(1);
});
}
@Test
public 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);
});
}
/* @Test
public 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")
@RequiredArgsConstructor
public class SnackMachineController {
private final SnackMachineRepository snackMachineRepository;
@GetMapping("/{id}")
@ResponseBody
public 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
@Setter
private Money moneyInside;
@Getter
@Setter
private float moneyInTransaction;
@Setter
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);
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
@Entity
public class SnackMachineDto {
@Id
@GeneratedValue
private 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(new
Money(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已帮我们实现。
@Test
public 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
);
}
另一个是找零功能:
@Test
public 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);
}
最后是无法找零禁止购买:
@Test
public 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);
});
}