DDD Sample Application version1.1.0: ユースケーススライスから見たアプリケーション構造の確認(2)

前回は、シンプルな参照の機能が、サンプル上でどのように構成されているかを確認した。今回は、積荷の一覧・詳細表示で参照機能の構成を確認しつつ、積荷の新規登録・状態更新まで確認してみたい。
  • 配送状況の詳細表示
  • 積荷の一覧・詳細表示
  • 積荷の新規登録
  • 積荷の状態更新
  • 配送ルート候補検索
  • 積荷への配送ルート設定
  • 配送状況の更新
  • 配送状況の更新(バッチ)

積荷一覧・詳細表示の概要
これは、商品発送側が現在管理対象になっている積荷の情報を確認するためのもの。おなじみの一覧・詳細表示アプリだ。アプリケーション内では、Booking と呼ばれている。Booking (Trackingもそうだが)はコンテキストなのだろうか?
この機能は、一見すると配送状況の詳細表示と同じように思えるが、内部的には異なった作りになっており、Web層の Controller からリモートサービスとして構成された BookingServiceFacade を呼び出す形になっている。



BookingServiceFacadeの先も、前回のように単純にRepositoryを呼ぶだけではなく Application層(BookingService)を経由しているため、多少複雑な構成になっている。

積荷の一覧・詳細表示
よくあるエンティティの一覧・詳細表示。検索の種類が違うだけで、アプリケーションの構成はどちらもほとんど同じ。なので、ここでは一覧検索だけを見ていくこととする。

INTERFACES
エントリポイントは、上記の図でわかるとおり interafaces パッケージの CargoAdminController。

[CargoAdminController]
public final class CargoAdminController extends MultiActionController {

このクラスは Spring WebMVC のMultiActionController を継承しているが、これは複数のリクエストを1つのController で処理するためのクラス。デフォルトでは、リクエストURIのスラッシュで区切られた末尾部分と同じ名前のメソッドを呼び出す。その他は SimpleFormController と大差ない。

一覧表示は index.jsp から「/admin/list」で呼び出されているが、「/admin」は web.xml で BookingDispatcherServlet に割り当てられており、booking-servlet.xml 経由で上述のCargoAdminControllerに割り振られ、最終的に list メソッドが呼び出されている。

[CargoAdminController#list]
  public Map list(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map map = new HashMap();
List cargoList = bookingServiceFacade.listAllCargos();

map.put("cargoList", cargoList);
return map;
}

BookingServiceFacade#listAllCargos() を呼び直しているだけだ。

[BookingServiceFacadeImpl#listAllCargos]
  public List listAllCargos() {
final List cargoList = cargoRepository.findAll();
final List dtoList = new ArrayList(cargoList.size());
final CargoRoutingDTOAssembler assembler = new CargoRoutingDTOAssembler();
for (Cargo cargo : cargoList) {
dtoList.add(assembler.toDTO(cargo));
}
return dtoList;
}

Repository で Cargo の一覧を取得している。ただし BookingFacadeService はリモートサービスなので、呼び出し元に結果を返却するために DTO を作っている。DTO には Cargo の情報だけでなく、関連する配送ルートの情報まで展開した上で返却している。

[CargoRoutingDTOAssembler#toDTO]
  public CargoRoutingDTO toDTO(final Cargo cargo) {
final CargoRoutingDTO dto = new CargoRoutingDTO(
cargo.trackingId().idString(),
cargo.origin().unLocode().idString(),
cargo.routeSpecification().destination().unLocode().idString(),
cargo.routeSpecification().arrivalDeadline(),
cargo.delivery().routingStatus().sameValueAs(RoutingStatus.MISROUTED));
for (Leg leg : cargo.itinerary().legs()) {
dto.addLeg(
leg.voyage().voyageNumber().idString(),
leg.loadLocation().unLocode().idString(),
leg.unloadLocation().unLocode().idString(),
leg.loadTime(),
leg.unloadTime());
}
return dto;
}

ちなみに、CargoAdminController から BookingServiceFacade へのリモート呼び出しは RmiProxyFactoryBean でうまく隠蔽されている。

[booking-servlet.xml]
  <bean id="remoteBookingService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
<property name="serviceUrl" value="rmi://localhost:1099/BookingService"/>
<property name="serviceInterface" value="se.citerus.dddsample.interfaces.booking.facade.BookingServiceFacade"/>
</bean>

<bean name="/*" class="se.citerus.dddsample.interfaces.booking.web.CargoAdminController">
<property name="bookingServiceFacade" ref="remoteBookingService"/>
</bean>

ここまでがINTERFACES。というか、ここまででほとんど終わりだ。

APPLICATION
今回はアプリケーションサービスとして BookingService が存在するが、このシナリオでは使われていない。

INFRASTRUCTURE
Tracking との目立った違いはない。

積荷の新規登録
エンティティの新規登録。新規登録画面の初期化・表示と、入力された積荷情報の登録の2ステップにわかれている。
新規登録画面の初期化時は、出発地・到着地を選択するためのドロップダウンメニューに設定する情報を取得しているのみであるため、ここでは積荷情報の登録を見ていく。

INTERFACES
エントリポイントは、CargoAdminController の register メソッド。

[CargoAdminController#register]
  public void register(HttpServletRequest request, HttpServletResponse response,
RegistrationCommand command) throws Exception {
Date arrivalDeadline = new SimpleDateFormat("M/dd/yyyy").parse(command.getArrivalDeadline());
String trackingId = bookingServiceFacade.bookNewCargo(
command.getOriginUnlocode(), command.getDestinationUnlocode(), arrivalDeadline
);
response.sendRedirect("show.html?trackingId=" + trackingId);
}

BookingServiceFacade に丸投げ。

[BookingServiceFacadeImpl#bookNewCargo]
  public String bookNewCargo(String origin, String destination, Date arrivalDeadline) {
TrackingId trackingId = bookingService.bookNewCargo(
new UnLocode(origin),
new UnLocode(destination),
arrivalDeadline
);
return trackingId.idString();
}

さらにアプリケーションレイヤに丸投げ。

APPLICATION
ついにアプリケーションサービスが登場。

[BookingServiceImpl#bookNewCargo]
  @Override
@Transactional
public TrackingId bookNewCargo(final UnLocode originUnLocode,
final UnLocode destinationUnLocode,
final Date arrivalDeadline) {
// TODO modeling this as a cargo factory might be suitable
final TrackingId trackingId = cargoRepository.nextTrackingId();
final Location origin = locationRepository.find(originUnLocode);
final Location destination = locationRepository.find(destinationUnLocode);
final RouteSpecification routeSpecification = new RouteSpecification(origin, destination, arrivalDeadline);

final Cargo cargo = new Cargo(trackingId, routeSpecification);

cargoRepository.store(cargo);
logger.info("Booked new cargo with tracking id " + cargo.trackingId().idString());

return cargo.trackingId();
}

処理の流れとしては、Command から取得したコードをもとに ENTITIES や VALUE OBJECTS を復元して新規 Cargo に設定し、最後に Repository で保存している。ごくごく基本的な流れではあるが、設定項目が多すぎて少しぎこちなくなっているため、将来的には CargoFactory を作ってそこに移すのが良いと考えているようだ。
この時点で既にトラッキングIDが発行されていることから、ビジネス的には配送ルートが設定されていなくても、正当な積荷らしい。また、Cargo が状態を持っている様子もない。
@Transaction でこのメソッドをトランザクション境界に設定している点にも注意。

INFRASTRUCTURE
一件検索系はもういいので無視して、トラッキングIDの新規発行を担っている CargoRepository#nextTrackingId を確認したい。

[CargoRepositoryHibernate#nextTrackingId]
  public TrackingId nextTrackingId() {
// TODO use an actual DB sequence here, UUID is for in-mem
final String random = UUID.randomUUID().toString().toUpperCase();
return new TrackingId(
random.substring(0, random.indexOf("-"))
);
}

あぁ…TODO とかついちゃってるよ…。現段階では UUID が使われているが、本来はDBシーケンスを使う、とのこと。もうバージョン1.1.0なんですけど :-D

積荷情報(到着地点)の更新
基本的な流れは積荷の新規登録と同じなので省略する。

まとめ
リモートファサードを使う場合の実装方針がわかった、ってところだろうか。あえてドメインの構造には立ち入っていないが、このユースケースではドメイン層に入る余地もなさそう。おそらく、配送ルートの検索あたりでないとロジックらしいロジックは出てこないだろうな。

目次:
DDD Sample Application version1.1.0を確認する

コメント