commit 2d3bf1222c4aa1931a4f88a602d46d52ec79eeb5 Author: MIN Date: Wed Mar 11 00:35:38 2020 +0900 Domain Driven Development 01 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b015ae3 --- /dev/null +++ b/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.4.RELEASE + + + com.example + demo + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/example/demo/DemoApplication.java b/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..0123d45 --- /dev/null +++ b/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/common/ErrorMessages.java b/src/main/java/com/example/demo/common/ErrorMessages.java new file mode 100644 index 0000000..02490c8 --- /dev/null +++ b/src/main/java/com/example/demo/common/ErrorMessages.java @@ -0,0 +1,21 @@ +package com.example.demo.common; + +import lombok.Getter; + +@Getter +public enum ErrorMessages { + + ALREADY_PAID("already paid"), + NOT_READY("not preparing state"), + NOT_SHIPPED("not shipped state"), + NOT_DELIVERING("not delivering state"), + ALREADY_SHIPPED("already shipped"), + NO_ORDER_LINE("no OrderLine"), + NO_SHIPPING_INFO("no Shipping Information"); + + String msg; + + ErrorMessages(String msg) { + this.msg = msg; + } +} diff --git a/src/main/java/com/example/demo/order/Address.java b/src/main/java/com/example/demo/order/Address.java new file mode 100644 index 0000000..c2f9e88 --- /dev/null +++ b/src/main/java/com/example/demo/order/Address.java @@ -0,0 +1,19 @@ +package com.example.demo.order; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.util.StringUtils; + +@Getter +@AllArgsConstructor +@EqualsAndHashCode +public class Address { + private String address1; + private String address2; + private String zipCode; + + public boolean isBlank() { + return StringUtils.isEmpty(address1) || StringUtils.isEmpty(address2) || StringUtils.isEmpty(zipCode); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/order/Money.java b/src/main/java/com/example/demo/order/Money.java new file mode 100644 index 0000000..3934ef3 --- /dev/null +++ b/src/main/java/com/example/demo/order/Money.java @@ -0,0 +1,20 @@ +package com.example.demo.order; + +import lombok.Getter; + +@Getter +public class Money { + private int value; + + public Money(int value) { + this.value = value; + } + + public Money add(Money money) { + return new Money(this.value + money.getValue()); + } + + public Money multiply(Money money) { + return new Money(this.value * money.getValue()); + } +} diff --git a/src/main/java/com/example/demo/order/Order.java b/src/main/java/com/example/demo/order/Order.java new file mode 100644 index 0000000..c9830f1 --- /dev/null +++ b/src/main/java/com/example/demo/order/Order.java @@ -0,0 +1,121 @@ +package com.example.demo.order; + +import com.example.demo.common.ErrorMessages; +import lombok.Getter; + +import java.util.List; +import java.util.UUID; + +/** + * 주문 정보 + */ +@Getter +public class Order { + + private OrderNo id; + private List orderLines; + private OrderState state; + private ShippingInfo shippingInfo; + private Money totalAmounts; + + public Order(List orderLines, ShippingInfo shippingInfo) { + id = new OrderNo(UUID.randomUUID().toString()); + setOrderLines(orderLines); + setShippingInfo(shippingInfo); + state = OrderState.PAYMENT_WAITING; + } + + private void setOrderLines(List orderLines) { + if (isEmptyLOrderLines(orderLines)) { + throw new IllegalStateException(ErrorMessages.NO_ORDER_LINE.getMsg()); + } + + this.orderLines = orderLines; + calculateTotalAmounts(); + } + + private boolean isEmptyLOrderLines(List orderLines) { + return orderLines == null || orderLines.isEmpty(); + } + + private void setShippingInfo(ShippingInfo shippingInfo) { + if (isBlankShippingInfo(shippingInfo)) { + throw new IllegalStateException(ErrorMessages.NO_SHIPPING_INFO.getMsg()); + } + + this.shippingInfo = shippingInfo; + } + + private boolean isBlankShippingInfo(ShippingInfo shippingInfo) { + return shippingInfo == null || shippingInfo.isBlank(); + } + + public void payment() { + if (state != OrderState.PAYMENT_WAITING) { + throw new IllegalStateException(ErrorMessages.ALREADY_PAID.getMsg()); + } + + this.state = OrderState.PREPARING; + } + + public void shipped() { + if (state != OrderState.PREPARING) { + throw new IllegalStateException(ErrorMessages.NOT_READY.getMsg()); + } + + this.state = OrderState.SHIPPED; + } + + public void startDelivery() { + if (state != OrderState.SHIPPED) { + throw new IllegalStateException(ErrorMessages.NOT_SHIPPED.getMsg()); + } + + this.state = OrderState.DELIVERING; + } + + public void completeDelivery() { + if (state != OrderState.DELIVERING) { + throw new IllegalStateException(ErrorMessages.NOT_DELIVERING.getMsg()); + } + + this.state = OrderState.DELIVERING_COMPLETED; + } + + public void changeShippingInfo(ShippingInfo newShippingInfo) { + verifyBeforeShipping(); + this.shippingInfo = newShippingInfo; + } + + public void cancel() { + verifyBeforeShipping(); + this.state = OrderState.CANCELED; + } + + private void verifyBeforeShipping() { + if (state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING) { + throw new IllegalStateException(ErrorMessages.ALREADY_SHIPPED.getMsg()); + } + } + + private void calculateTotalAmounts() { + this.totalAmounts = new Money(orderLines.stream() + .mapToInt(x -> x.getAmounts().getValue()).sum()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (obj.getClass() != Order.class) return false; + if (this.id.getValue().isEmpty()) return false; + return this.id.equals(((Order) obj).id); + } + + @Override + public int hashCode() { + final int prime = 31; + int hash = 1; + return prime * hash + (id.getValue().isEmpty()? 0 : id.hashCode()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/order/OrderLine.java b/src/main/java/com/example/demo/order/OrderLine.java new file mode 100644 index 0000000..61060a6 --- /dev/null +++ b/src/main/java/com/example/demo/order/OrderLine.java @@ -0,0 +1,26 @@ +package com.example.demo.order; + +import lombok.Getter; + +@Getter +public class OrderLine { + private Product product; + private Money price; + private int quantity; + private Money amounts; + + public OrderLine(Product product, Money price, int quantity) { + this.product = product; + this.price = price; + this.quantity = quantity; + this.amounts = calculateAmounts(); + } + + private Money calculateAmounts() { + return new Money(price.getValue() * quantity); + } + + public Money getAmounts() { + return amounts; + } +} diff --git a/src/main/java/com/example/demo/order/OrderNo.java b/src/main/java/com/example/demo/order/OrderNo.java new file mode 100644 index 0000000..937c1ba --- /dev/null +++ b/src/main/java/com/example/demo/order/OrderNo.java @@ -0,0 +1,10 @@ +package com.example.demo.order; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class OrderNo { + private String value; +} diff --git a/src/main/java/com/example/demo/order/OrderState.java b/src/main/java/com/example/demo/order/OrderState.java new file mode 100644 index 0000000..caab2c2 --- /dev/null +++ b/src/main/java/com/example/demo/order/OrderState.java @@ -0,0 +1,28 @@ +package com.example.demo.order; + +/** + * 주문 상태 + * 결제 대기 -> 준비중 -> 배송준비 -> 배송중 -> 배송완료 // 취소됨 + */ +public enum OrderState { + PAYMENT_WAITING { + public boolean isShippingChangeable() { + return true; + } + }, + + PREPARING { + public boolean isShippingChangeable() { + return true; + } + }, + + SHIPPED, + DELIVERING, + DELIVERING_COMPLETED, + CANCELED; + + public boolean isShippingChangeable() { + return false; + } +} diff --git a/src/main/java/com/example/demo/order/Product.java b/src/main/java/com/example/demo/order/Product.java new file mode 100644 index 0000000..6346aba --- /dev/null +++ b/src/main/java/com/example/demo/order/Product.java @@ -0,0 +1,11 @@ +package com.example.demo.order; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Product { + private String id; + private ProductType type; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/order/ProductType.java b/src/main/java/com/example/demo/order/ProductType.java new file mode 100644 index 0000000..82f5979 --- /dev/null +++ b/src/main/java/com/example/demo/order/ProductType.java @@ -0,0 +1,8 @@ +package com.example.demo.order; + +public enum ProductType { + MOUSE, + KEYBOARD, + MONITOR, + SPEAKER +} diff --git a/src/main/java/com/example/demo/order/Receiver.java b/src/main/java/com/example/demo/order/Receiver.java new file mode 100644 index 0000000..86e202d --- /dev/null +++ b/src/main/java/com/example/demo/order/Receiver.java @@ -0,0 +1,18 @@ +package com.example.demo.order; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.springframework.util.StringUtils; + +@Getter +@AllArgsConstructor +@EqualsAndHashCode +public class Receiver { + private String name; + private String phone; + + public boolean isBlank() { + return StringUtils.isEmpty(name) || StringUtils.isEmpty(phone); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/order/ShippingInfo.java b/src/main/java/com/example/demo/order/ShippingInfo.java new file mode 100644 index 0000000..d6793b4 --- /dev/null +++ b/src/main/java/com/example/demo/order/ShippingInfo.java @@ -0,0 +1,22 @@ +package com.example.demo.order; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * 배송지 정보 + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ShippingInfo { + private Receiver receiver; + private Address address; + + public boolean isBlank() { + return receiver == null || receiver.isBlank() || address == null || address.isBlank(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/test/java/com/example/demo/OrderTests.java b/src/test/java/com/example/demo/OrderTests.java new file mode 100644 index 0000000..834b3ee --- /dev/null +++ b/src/test/java/com/example/demo/OrderTests.java @@ -0,0 +1,164 @@ +package com.example.demo; + +import com.example.demo.order.*; +import org.junit.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +public class OrderTests { + + private ProductType[] productTypes = ProductType.values(); + + @Test(expected = IllegalStateException.class) + public void noShippingInfo() { + + List orderLines = createOrderLines(3); + ShippingInfo shippingInfo = new ShippingInfo(); + + Order order = new Order(orderLines, shippingInfo); + } + + @Test(expected = IllegalStateException.class) + public void noOrderLine() { + + List orderLines = createOrderLines(0); + ShippingInfo shippingInfo = createShippingInfo(); + + Order order = new Order(orderLines, shippingInfo); + } + + @Test + public void order() { + // 주문 + Order order = createOrder(); + assertThat(order.getState()).isEqualTo(OrderState.PAYMENT_WAITING); + + // 금액 지불 + order.payment(); + assertThat(order.getState()).isEqualTo(OrderState.PREPARING); + + // 출고 + order.shipped(); + assertThat(order.getState()).isEqualTo(OrderState.SHIPPED); + + // 배송 시작 + order.startDelivery(); + assertThat(order.getState()).isEqualTo(OrderState.DELIVERING); + + // 배송 완료 + order.completeDelivery(); + assertThat(order.getState()).isEqualTo(OrderState.DELIVERING_COMPLETED); + } + + @Test + public void orderCancelBeforePayment() { + Order order = createOrder(); + // 주문 후 취소 + order.cancel(); + assertThat(order.getState()).isEqualTo(OrderState.CANCELED); + } + + @Test + public void orderCancelBeforeShipping() { + Order order = createOrder(); + order.payment(); + // 결제 후 취소 + order.cancel(); + assertThat(order.getState()).isEqualTo(OrderState.CANCELED); + } + + @Test(expected = IllegalStateException.class) + public void failOrderCancelInShipped() { + Order order = createOrder(); + order.payment(); + order.shipped(); + order.cancel(); + assertThat(order.getState()).isEqualTo(OrderState.CANCELED); + } + + @Test(expected = IllegalStateException.class) + public void failOrderCancelInDelivering() { + Order order = createOrder(); + order.payment(); + order.shipped(); + order.startDelivery(); + order.cancel(); + assertThat(order.getState()).isEqualTo(OrderState.CANCELED); + } + + @Test(expected = IllegalStateException.class) + public void failOrderCancelInDeliveringComplete() { + Order order = createOrder(); + order.payment(); + order.shipped(); + order.startDelivery(); + order.completeDelivery(); + order.cancel(); + assertThat(order.getState()).isEqualTo(OrderState.CANCELED); + } + + @Test + public void changeShippingInfo() { + Order order = createOrder(); + + ShippingInfo info = order.getShippingInfo(); + ShippingInfo created = createShippingInfo(); + + assertThat(info.getReceiver().equals(created.getReceiver())).isEqualTo(true); + assertThat(info.getAddress().equals(created.getAddress())).isEqualTo(true); + + order.changeShippingInfo(ShippingInfo.builder() + .receiver(new Receiver("KIM", "010-1234-5678")) + .address(new Address("Seoul", "Korea", "12345")) + .build()); + + info = order.getShippingInfo(); + assertThat(info.getReceiver().equals(created.getReceiver())).isEqualTo(false); + assertThat(info.getAddress().equals(created.getAddress())).isEqualTo(false); + } + + @Test(expected = IllegalStateException.class) + public void failChangeShippingInfo() { + Order order = createOrder(); + order.payment(); + order.shipped(); + + order.changeShippingInfo(ShippingInfo.builder() + .receiver(new Receiver("KIM", "010-1234-5678")) + .address(new Address("Seoul", "Korea", "12345")) + .build()); + } + + public Order createOrder() { + List orderLines = createOrderLines(3); + ShippingInfo shippingInfo = createShippingInfo(); + return new Order(orderLines, shippingInfo); + } + + public ShippingInfo createShippingInfo() { + return ShippingInfo.builder() + .receiver(new Receiver("MIN", "010-1234-5678")) + .address(new Address("Incheon", "Korea", "12345")) + .build(); + } + + public List createOrderLines(int orderLineCount) { + + List orderLines = new ArrayList<>(); + + for (int i = 0; i < orderLineCount; i++) { + Product product = new Product(UUID.randomUUID().toString(), productTypes[i % productTypes.length]); + Money price = new Money(10000 * i); + + orderLines.add(new OrderLine(product, price, i)); + } + + return orderLines; + } +} \ No newline at end of file