首先我们对Snack和SnackMachine重构:
package com.lugew.springbootddd;
import lombok.Getter;
import lombok.Setter;
/**
* @author 夏露桂
* @since 2021/6/16 10:45
*/
@Getter
@Setter
public class Snack extends AggregateRoot {
private String name;
public Snack() {
}
public Snack(String name) {
this.name = name;
}
}
package com.lugew.springbootddd.snackmachine;
import com.lugew.springbootddd.AggregateRoot;
import com.lugew.springbootddd.Slot;
import com.lugew.springbootddd.Snack;
import com.lugew.springbootddd.SnackMachineDto;
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;
@Getter
@Setter
public final class SnackMachine extends AggregateRoot {
private Money moneyInside;
private Money moneyInTransaction;
private List<Slot> slots;
public SnackMachine() {
moneyInside = None;
moneyInTransaction = None;
slots = new ArrayList<>();
slots.add(new Slot(this, 1, null, 0, 1));
slots.add(new Slot(this, 2, null, 0, 1));
slots.add(new Slot(this, 3, null, 0, 1));
}
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);
}
public void returnMoney() {
moneyInTransaction = None;
}
public void buySnack(int position) {
Slot slot = slots.stream().filter(x -> x.getPosition() ==
position).findAny().orElse(null);
slot.setQuantity(slot.getQuantity() - 1);
moneyInside = Money.add(moneyInside, moneyInTransaction);
moneyInTransaction = None;
}
public SnackMachineDto convertToSnackMachineDto() {
SnackMachineDto snackMachineDto = new SnackMachineDto();
snackMachineDto.setId(id);
snackMachineDto.setMoneyInTransaction(moneyInTransaction.getAmount());
snackMachineDto.setOneCentCount(moneyInside.getOne、
CentCount());
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, Snack snack, int quantity, float price) {
Slot slot = slots.stream().filter(x -> x.getPosition() ==
position).findAny().orElse(null);
if (slot != null) {
slot.setSnack(snack);
slot.setQuantity(quantity);
slot.setPrice(price);
}
}
}
此时,我们还有几处需要重构,一,List
package com.lugew.springbootddd.snackmachine;
import com.lugew.springbootddd.AggregateRoot;
import com.lugew.springbootddd.Slot;
import com.lugew.springbootddd.Snack;
import com.lugew.springbootddd.SnackMachineDto;
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 Money moneyInTransaction;
private List<Slot> slots;
public SnackMachine() {
moneyInside = None;
moneyInTransaction = None;
slots = new ArrayList<>();
slots.add(new Slot(this, 1, null, 0, 1));
slots.add(new Slot(this, 2, null, 0, 1));
slots.add(new Slot(this, 3, null, 0, 1));
}
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);
}
public void returnMoney() {
moneyInTransaction = None;
}
public void buySnack(int position) {
Slot slot = slots.stream().filter(x -> x.getPosition() ==
position).findAny().orElse(null);
slot.setQuantity(slot.getQuantity() - 1);
moneyInside = Money.add(moneyInside, moneyInTransaction);
moneyInTransaction = None;
}
public SnackMachineDto convertToSnackMachineDto() {
SnackMachineDto snackMachineDto = new SnackMachineDto();
snackMachineDto.setId(id);
snackMachineDto.setMoneyInTransaction(moneyInTransaction.getAmount());
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, Snack snack, int quantity, float price) {
Slot slot = slots.stream().filter(x -> x.getPosition() ==
position).findAny().orElse(null);
if (slot != null) {
slot.setSnack(snack);
slot.setQuantity(quantity);
slot.setPrice(price);
}
}
}
为了解决上述问题,我们可能会添加一个返回指定位置的插槽中零食数量的方法:snackMachine.getQuantityOfSnacksInSlot(1)
,且该方法的返回值是9。但这种方式会带来其他的问题,当我们需要插槽的其他信息时,如零食信息及其价格,那么我们必须再添加两个方法:getSnackInSlot和``getPriceInSlot
。当要获取所有插槽和零食时,使用这些方法就变得异常繁琐。我们必须采取另一种方式,之前的文章中我们说过,当代码使用变得冗余时,意味着存在隐藏的抽象,Slot类中含有三个属性:Snack、Quantity和Price。这三个属性始终绑定在一起,所以我们能将之重构为一个值对象。这很有用,值对象将紧耦合的属性同时暴露。我们为之新建值对象的抽象:SnackPile:
package com.lugew.springbootddd;
import lombok.Getter;
/**
* @author 夏露桂
* @since 2021/6/22 17:52
*/
@Getter
public class SnackPile extends ValueObject<SnackPile> {
private Snack snack;
private int quantity;
private float price;
private SnackPile() {
}
public SnackPile(Snack snack, int quantity, float price) {
this.snack = snack;
this.quantity = quantity;
this.price = price;
}
public SnackPile subtractOne() {
return new SnackPile(snack, getQuantity() - 1, getPrice());
}
@Override
protected boolean equalsCore(SnackPile other) {
return this.snack == other.snack && this.getQuantity() == other.getQuantity()
&& this.getPrice() == other.getPrice();
}
@Override
protected int getHashCodeCore() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Float.floatToIntBits(price);
result = prime * result + quantity;
result = prime * result + ((snack == null) ? 0 : snack.hashCode());
return result;
}
}
我们只给值对象生成get方法,保持其不可变性。随后我们重构SnackMachine属性和方法:
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 Money moneyInTransaction;
private List<Slot> slots;
public SnackMachine() {
moneyInside = None;
moneyInTransaction = None;
slots = new ArrayList<>();
slots.add(new Slot(this, 1));
slots.add(new Slot(this, 2));
slots.add(new Slot(this, 3));
}
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);
}
public void returnMoney() {
moneyInTransaction = None;
}
public void buySnack(int position) {
Slot slot = slots.stream().filter(x -> x.getPosition() ==
position).findAny().orElse(null);
slot.setSnackPile(slot.getSnackPile().subtractOne());
moneyInside = Money.add(moneyInside, moneyInTransaction);
moneyInTransaction = None;
}
public SnackMachineDto convertToSnackMachineDto() {
SnackMachineDto snackMachineDto = new SnackMachineDto();
snackMachineDto.setId(id);
snackMachineDto.setMoneyInTransaction(moneyInTransaction.getAmount());
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 SnackPile getSnackPile(int position) {
return slots.stream().filter(x -> x.getPosition() ==
position).findAny().orElse(null).getSnackPile();
}
}
我们还需要为SnackPile添加数量减少的功能,一种方法直接设置quality,这种方式非常不可取,它破坏了值对象的不可变性,取而代之的是提供公共方法,返回一个除quality外都相同的新对象。
package com.lugew.springbootddd;
import lombok.Getter;
/**
* @author 夏露桂
* @since 2021/6/22 17:52
*/
@Getter
public class SnackPile extends ValueObject<SnackPile> {
private Snack snack;
private int quantity;
private float price;
private SnackPile() {
}
public SnackPile(Snack snack, int quantity, float price) {
this.snack = snack;
this.quantity = quantity;
this.price = price;
}
public SnackPile subtractOne() {
return new SnackPile(snack, getQuantity() - 1, getPrice());
}
@Override
protected boolean equalsCore(SnackPile other) {
return this.snack == other.snack && this.getQuantity() == other.getQuantity()
&& this.getPrice() == other.getPrice();
}
@Override
protected int getHashCodeCore() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Float.floatToIntBits(price);
result = prime * result + quantity;
result = prime * result + ((snack == null) ? 0 : snack.hashCode());
return result;
}
}
最后重构测试:
@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);
assertEquals(snackMachine.getMoneyInside().getAmount(), 1, 0.5);
assertEquals(snackMachine.getSnackPile(1).getQuantity(), 9);
}
接下来还有一些方法需要重构:
public void buySnack(int position) {
Slot slot = getSlot(position);
slot.setSnackPile(slot.getSnackPile().subtractOne());
moneyInside = Money.add(moneyInside, moneyInTransaction);
moneyInTransaction = None;
}
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();
}
同时还有SnackPile:
package com.lugew.springbootddd;
import lombok.Getter;
/**
* @author 夏露桂
* @since 2021/6/22 17:52
*/
@Getter
public class SnackPile extends ValueObject<SnackPile> {
private Snack snack;
private int quantity;
private float price;
private SnackPile() {
}
public SnackPile(Snack snack, int quantity, float price) {
if (quantity < 0)
throw new IllegalStateException();
if (price < 0)
throw new IllegalStateException();
this.snack = snack;
this.quantity = quantity;
this.price = price;
}
public SnackPile subtractOne() {
return new SnackPile(snack, getQuantity() - 1, getPrice());
}
@Override
protected boolean equalsCore(SnackPile other) {
return this.snack == other.snack && this.getQuantity() == other.getQuantity()
&& this.getPrice() == other.getPrice();
}
@Override
protected int getHashCodeCore() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Float.floatToIntBits(price);
result = prime * result + quantity;
result = prime * result + ((snack == null) ? 0 : snack.hashCode());
return result;
}
}
添加异常判断,此部分的单元测试读者可自行实现。