ユースケース駆動開発実践_実装
ユースケース駆動開発実践_実装
これまで進めてきたモデリングで実装をしてみました。ソースコードはこちらになります。
Javaで実装しDBアクセスのライブラリとしてMyBatisを使用しています。
とりあえず、以下のユースケースに対応しています。
- ログイン
- 商品検索
- カートの商品更新
- カートの内容確認
- 注文確定
それぞれ以下の様にリクエストを投げれる様にしています。
ログイン
curl -i --cookie-jar cookie http://localhost:8080/preflight TOKEN=$( cat cookie | grep 'XSRF' | cut -f7 ) SESSION=`curl -i --cookie cookie -d "_csrf=$TOKEN" -d name=user1 -d password=user1 -L http://localhost:8080/login | grep -E "SESSION\=[^ ]+" -o` または SESSION=`curl -i --cookie cookie -H "X-XSRF-TOKEN:$TOKEN" -d name=user1 -d password=user1 -L http://localhost:8080/login | grep -E "SESSION\=[^ ]+" -o`
商品検索
curl -i -XGET --cookie cookie -b "$SESSION" -d "_csrf=$TOKEN" http://localhost:8080/product/search?limit=20&offset=0
カートの商品更新
curl -i -XPOST --cookie cookie -b "$SESSION" -H "X-XSRF-TOKEN:$TOKEN" -H 'Content-Type:application/json' -d '{"itemId": 1, "number": 2 }' http://localhost:8080/order/updateItem
カートの内容確認
curl -i -XGET --cookie cookie -b "$SESSION" -H "X-XSRF-TOKEN:$TOKEN" http://localhost:8080/order/curt_info
購入確定
curl -i -XPOST --cookie cookie -b "$SESSION" -H "X-XSRF-TOKEN:$TOKEN" -H 'Content-Type:application/json' -d '{"paymentType": 1 }' http://localhost:8080/order/confirm
集約
前回までのモデリングで集約のルートとなっていたのは注文クラスとなっていましたが、 対象のクラスは jp.co.teruuu.ec_modeling.app.domain.order.OrderEntity
になっています。OrderEntity
では注文周りの操作に関するメソッドを提供しています。具体的にはカート内の商品検索や注文確定のを提供していて、集約内のデータの整合性を取れる様にしています。
商品更新のメソッドは以下の様にしています。
public OrderEntity updateItem(ProductId productId, int price, int number) throws OrderException{ if(!order.canItemUpdate()){ throw new ItemUpdateException("cant item update"); } Optional<Item> kizon = this.items.stream() .filter(i->i.getProductId().equals(productId)).findAny(); if(kizon.isPresent()){ Item item = kizon.get(); item.setPrice(price); item.setCurrentPrice(price); item.setNumber(number); item.setUpdateDate(LocalDateTime.now()); } else { this.items.add(new Item(productId, price, price, number, LocalDateTime.now())); } return this; }
この関数内では order.canItemUpdate()
で品目更新が可能かどうかをチェックし、もし品目更新が不可能な状態でこの関数が呼ばれたらエラーを投げる様にしています。また品目更新が可能かどうかは Order
クラスのメソッドを呼び出しているのですが、これは Order
クラスの属性としてOrderStatus
を持っており品目の更新が可能かどうかを管理する責務をここに与えているからです。集約のルートは特にそうですがクラス内の属性を決めれば、そのクラスの責務と外から呼び出せるメソッドが決まると思います。クラス内の属性を操作をそのクラスに限定することでクラス感は疎結合で、データの整合性を取りやすくなります。
リポジトリ
データの登録や更新、削除をするときリポジトリからデータを取り出せる様にしているのですが、集約のオブジェクトをするときは集約全体のオブジェクトをリポジトリから取り出す様にしています。注文の集約のリポジトリとしてjp.co.teruuu.ec_modeling.app.domain.order.repository.OrderRepository
を使っていまして、現在のカートを取り出すときは以下の関数を使っています。
public OrderEntity getCurt(UserId userId){ Order order = orderDao.findByUserIdStatus(userId.getId(), OrderStatus.Shopping.getCode()); if(order == null){ return new OrderEntity(userId.getId()); } else { return makeOrderEntity(order); } } private OrderEntity makeOrderEntity(Order order){ OrderId orderId = order.getOrderId(); List<Item> items = itemDao.findByOrderId(orderId); items.forEach(i-> { i.setCurrentPrice(productDao.findById(i.getProductId().getId()).getPrice()); }); PaymentInfo paymentInfo = paymentInfoDao.findByOrderId(orderId.getId()); List<UsedCoupon> usedCoupons = usedCouponDao.findByOrderId(orderId.getId()); return new OrderEntity(order, items, Optional.ofNullable(paymentInfo), usedCoupons); }
ユーザの現在のカートはorderテーブルからuserIdとOrderStatusを指定して取り出せる様になっているのですが、存在しない場合はOrderEntityを新規で作成するためのコンストラクタを呼び出しています。テーブル内でデータをどう持っているかはリポジトリ内で考慮する様にしており、集約内ではテーブル内でデータをどう保持しているかとかは気にしないでも良い様にしています。
登録や更新、削除の操作はこの様に集約のオブジェクトを取り出す様にしているのですが、検索をするときにリポジトリごとで対象の集約を制限すると面倒だったりします。例えばカート内容確認で現在の品目と品目に紐づく商品の情報を返したい場合、品目詳細のリポジトリから取り出せる様にしています。そのためのdtoとしてjp.co.teruuu.ec_modeling.app.app_service.dto.ItemDetail
を以下の様に用意しています。
public class ItemDetail { int itemId; String name; int price; int number; int currentPrice; String description; LocalDateTime updateDate; ,,, }
ここでは注文と商品の集約を横断してデータを取り出しています。
アプリケーションサービス
アプリケーションサービスはリポジトリから生成したドメインオブジェクトを操作したり、検索結果を返したりします。ここではトランザクションの管理もここでやっています。
@Service public class OrderService { @Autowired OrderRepository orderRepository; @Autowired ProductRepository productRepository; @Autowired ItemDetailRepository itemDetailRepository; public boolean updateItem(LoginUser user, int productId, int number){ return updateItemFunc(user, productId, number); } public boolean confirm(LoginUser user, PaymentType paymentType){ return confirmFunc(user, paymentType); } public List<ItemDetail> curtInfo(LoginUser user){ return curtInfoFunc(user); } @Transactional(readOnly = false) private boolean updateItemFunc(LoginUser user, int productId, int number){ OrderEntity orderEntity = orderRepository.getCurt(user.getUserId()); Optional<Product> product = productRepository.findByProductId(productId); if(product.isPresent()){ try{ orderEntity.updateItem(product.get().getProductId(), product.get().getPrice(), number); orderRepository.updateItem(orderEntity, product.get().getProductId()); return true; } catch(OrderException e) { return false; } } else { return false; } } @Transactional(readOnly = false) private boolean confirmFunc(LoginUser user, PaymentType paymentType){ OrderEntity orderEntity = orderRepository.getCurt(user.getUserId()); try { orderEntity.confirm(paymentType); orderRepository.save(orderEntity); } catch(OrderException e){ return false; } return true; } @Transactional(readOnly = true) private List<ItemDetail> curtInfoFunc(LoginUser user){ OrderEntity orderEntity = orderRepository.getCurt(user.getUserId()); return itemDetailRepository.findByOrderId(orderEntity.getOrder().getOrderId()); } }