2009年5月6日水曜日

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

前回まででユースケース、アプリの全体構造までを確認したところで、今回はユースケースごと(ユーザーイベントごと)のアプリケーション構造を追っていく。ユーザーイベントを再掲する。
  • 配送状況の詳細表示
  • 積荷の一覧・詳細表示
  • 積荷の新規登録
  • 積荷の状態更新
  • 配送ルート候補検索
  • 積荷への配送ルート設定
  • 配送状況の更新
  • 配送状況の更新(バッチ)
まとめてやろうかと思っていたが、思った以上に長くなったので1イベント per 1エントリで進めることにした。今回は一番上、配送状況の詳細表示。

配送状況詳細表示の概要

主に、顧客が発注商品の配送状況を確認するために使う機能。アプリケーション内では、Tracking という言葉で呼ばれている。
Web インタフェースから情報を参照するだけの機能なので、Spring WebMVCのコントローラから Repository 経由でデータを取得して表示するだけのシンプルな作りになっている。アプリケーション層のクラスは存在しない。


INTERFACES
se.citerus.dddsample.interfaces.tracking の CargoTrackingController がエントリポイントになっている。このクラスは、Spring WebMVC のSimpleFormController を拡張している。

補足:Spring WebMVC の仕組み
中身に行く前に、これから何度も出てくるであろう Spring WebMVC の仕組みを確認しておく。SimpleFormController は Form のサブミットをハンドリングするために用意されたクラスだ。onSubmit メソッドを決められたメソッドシグネチャで作成しておくと、Form が送信された際に自動的に呼び出される。メソッド引数は、HttpServletRequest, HttpServletResponse[, AnyObject as Command] [, AnyExceptionClass] のようになっているらしい。
リクエストが送信された際には、CommandClass プロパティに設定されたクラスがインスタンス化され、リクエストパラメータが設定されて onSubmit の引数として渡されてくることになる。

[CargoTrackingController.java]
public final class CargoTrackingController extends SimpleFormController {

private CargoRepository cargoRepository;
private HandlingEventRepository handlingEventRepository;

public CargoTrackingController() {
setCommandClass(TrackCommand.class);
}

@Override
protected ModelAndView onSubmit(final HttpServletRequest request, final HttpServletResponse response,
final Object command, final BindException errors) throws Exception {

final TrackCommand trackCommand = (TrackCommand) command;
final String trackingIdString = trackCommand.getTrackingId();

final TrackingId trackingId = new TrackingId(trackingIdString);
final Cargo cargo = cargoRepository.find(trackingId);

final Map model = new HashMap();
if (cargo != null) {
final MessageSource messageSource = getApplicationContext();
final Locale locale = RequestContextUtils.getLocale(request);
final List handlingEvents = handlingEventRepository.lookupHandlingHistoryOfCargo(trackingId).distinctEventsByCompletionTime();
model.put("cargo", new CargoTrackingViewAdapter(cargo, messageSource, locale, handlingEvents));
} else {
errors.rejectValue("trackingId", "cargo.unknown_id", new Object[]{trackCommand.getTrackingId()}, "Unknown tracking id");
}
return showForm(request, response, errors, model);
}

public void setCargoRepository(CargoRepository cargoRepository) {
this.cargoRepository = cargoRepository;
}

public void setHandlingEventRepository(HandlingEventRepository handlingEventRepository) {
this.handlingEventRepository = handlingEventRepository;
}

}
設定可能なプロパティには以下のようなものがある。
  • sessionForm
    • Commandオブジェクトをセッションに保存するかどうか
  • commandName
    • CommandオブジェクトをRequest/Sessionにバインドする際の名前
  • formView
    • 初回アクセスまたはバリデーションエラー時に表示するビュー名
  • successView
    • onSubmitが成功した場合に表示するビュー名
  • validator
    • Commandのバリデータ
これらは、Bean定義ファイルを用いて以下のように設定されている。

[tracking-servlet.xml]
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="openSessionInViewInterceptor"/>
</list>
</property>
</bean>

<bean name="/track" class="se.citerus.dddsample.interfaces.tracking.CargoTrackingController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="trackCommand"/>
<property name="formView" value="track"/>
<property name="successView" value="start"/>
<property name="validator" ref="trackCommandValidator"/>
<property name="cargoRepository" ref="cargoRepository"/>
<property name="handlingEventRepository" ref="handlingEventRepository"/>
</bean>

<bean id="trackCommandValidator" class="se.citerus.dddsample.interfaces.tracking.TrackCommandValidator"/>

</beans>

onSubmitの処理内容
onSubmit では、コマンドオブジェクトから取り出したトラッキングID を使って CargoRepository から Cargo オブジェクトを、HandlingEventRepository から HandlingEvent のリストを取得してビューに返しているだけだ。ただし、ビューにはドメインオブジェクトをそのまま返すのではなく、アダプタを作って返している。この点は後述する。
TrackingCommand は trackingId しか持っておらず、TrackingCommand のバリデーターは、trackingId が空でないことを確認しているだけだ。

プレゼンテーションモデルの選択
DDD では、プレゼンテーション層のモデルとしてどのようなものを採用するかがよく議論になっている。一般的なものとしては、以下のような候補がある。
  • DomainObject をそのまま返す
  • DomainObject のアダプタを作り、View にはアダプタインタフェースを返す
  • 専用のプレゼンテーションモデルを作り、DomainObject のデータをコピーした上で返す
今回は2番目の、アダプタを作ってビューに返す方法を取っている。

OpenSessionInView の採用
Hibernate の Lazy 用として、openSessionInViewInterceptor がインターセプタとして設定されているのが面白い。Seasar コミュニティでは Dxo の導入が一般的になっていた気がするけど、こちらは OpenSessionInView パターンを使っているのか。OpenSessionInView は嫌われているようだけど、自分は別に嫌いではない。常に Dxo 層導入するの面倒くさいし。

APPLICATION
存在しない。

INFRASTRUCTURE
ここで出てくるのは、(上記で説明したSrpring WebMVCを除けば)Repository のHibernate 実装の部分だ。中を見てみると実際はあまり大したことをしておらず、ほとんどの処理を Hibernate に丸投げしている。ORM 万歳。

[CargoRepositoryHibernate.java]
@Repository
public class CargoRepositoryHibernate extends HibernateRepository implements CargoRepository {

public Cargo find(TrackingId tid) {
return (Cargo) getSession().
createQuery("from Cargo where trackingId = :tid").
setParameter("tid", tid).
uniqueResult();
}

public void store(Cargo cargo) {
getSession().saveOrUpdate(cargo);
// Delete-orphan does not seem to work correctly when the parent is a component
getSession().createSQLQuery("delete from Leg where cargo_id = null").executeUpdate();
}

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("-"))
);
}

public List findAll() {
return getSession().createQuery("from Cargo").list();
}

}

今回は、find メソッドしか使っていない。これは、単に JPQL を使って一件検索しているだけだ。すべての XxxxRepositoryHibernate は HibernateRepository を継承しているが、HibernateRepository はサブクラスからの getSession 呼び出しに応じて Session を提供しているだけだ。

[HibernateRepository.java]
public abstract class HibernateRepository {

private SessionFactory sessionFactory;

@Required
public void setSessionFactory(final SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}

protected Session getSession() {
return sessionFactory.getCurrentSession();
}

}

HandlingEventRepository はさらにシンプルで、メソッドは1つしかない。こちらも、JPQL で一覧検索しているだけ。

[HandlingEventRepository.java]
@Repository
public class HandlingEventRepositoryHibernate extends HibernateRepository implements HandlingEventRepository {

@Override
public void store(final HandlingEvent event) {
getSession().save(event);
}

@Override
public HandlingHistory lookupHandlingHistoryOfCargo(final TrackingId trackingId) {
return new HandlingHistory(getSession().createQuery(
"from HandlingEvent where cargo.trackingId = :tid").
setParameter("tid", trackingId).
list()
);
}

}

まとめ
シンプルな検索は、(アプリケーションレイヤをスキップして)Web-FWから直接 Repository を呼び出すことで実現している。プレゼンテーションモデルとしては、Adapter アプローチを取っている。

もともとシンプルなユースケースであることも手伝って、なんだか依存テクノロジの説明がほとんどになってしまった。次回からはこういった説明は少なくなるはずなので、もっとさくさく進むはず!

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

1 件のコメント:

kentaro714 さんのコメント...

なんか、ドメインから先にやった方が良い気がしてきたな…。