首先我们对Snack和SnackMachine重构:
package com.lugew.springbootddd;import lombok.Getter;import lombok.Setter;/*** @author 夏露桂* @since 2021/6/16 10:45*/@Getter@Setterpublic 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@Setterpublic 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@Setterprivate Money moneyInside;@Getter@Setterprivate 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*/@Getterpublic 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());}@Overrideprotected boolean equalsCore(SnackPile other) {return this.snack == other.snack && this.getQuantity() == other.getQuantity()&& this.getPrice() == other.getPrice();}@Overrideprotected 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@Setterprivate Money moneyInside;@Getter@Setterprivate 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*/@Getterpublic 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());}@Overrideprotected boolean equalsCore(SnackPile other) {return this.snack == other.snack && this.getQuantity() == other.getQuantity()&& this.getPrice() == other.getPrice();}@Overrideprotected 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;}}
最后重构测试:
@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);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*/@Getterpublic 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());}@Overrideprotected boolean equalsCore(SnackPile other) {return this.snack == other.snack && this.getQuantity() == other.getQuantity()&& this.getPrice() == other.getPrice();}@Overrideprotected 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;}}
添加异常判断,此部分的单元测试读者可自行实现。
