2009年5月15日金曜日

Poken[Rockstar]が来た。

Poken 到着〜ユーザー登録
QCon Tokyo 2009 のアンケートで当たったPokenが来た。こんなの。この子はどうも、Rockstarっていうらしい。まぁまぁかわいい…よね?

思ったよりずいぶん小さい。大きめの消しゴムくらいの大きさ。箱から出すと…

こんな。コレ以外何も入っていない。


キャップ(Rockster氏)を取ると、USB端子が。これをUSBポートに差し込むと、autorun が有効になっている(危険な)PCでは、Poken の登録サイトが表示される。
autorun が無効になっている場合は、マスストレージになっているので、中に入って"Start_Poken.html"をブラウザで表示すればOK。
# ちなみに、Chrome では正常に表示できない模様(友人談)。

あとは、表示された Poken サイトで Poken 専用アカウントを作成し、個人情報をポチポチ入れていけば完了。

Pokenの仕組み
"Start_Poken.html"の中を見てみると…
<META http-equiv="Refresh" content="1;    URL=http://p.poken.ch/u/なんかデバイス固有のトークンっぽいもの-+の大量繰り返し">

てな感じになっている。http-equiv="Refresh" で自動的にサイトに接続される仕掛けのようだ。肝心の URL は、デバイストークンと-+の繰り返し。-+ の繰り返し部分にはおそらく、交換した名刺の情報が入るのだろう。Poken で情報を交換するたびに、Poken デバイスがこの URL を書き換えているのだと思われる。
# このファイルは当然、ユーザーからは書き換え不能。

Poken on Mac
初回接続(新規ユーザー登録)時には Poken サイト上に表示されているPoken デバイス接続状態を示すアイコンが正常に表示されておらず、もしかして Win + IE でしか使えないデバイス認識プラグインとか使ってるのか!? と思ったが、2回目は正常に認識された。どうやら、デバイストークンが送付されていれば接続状態だと認識しているようだ。デバイスが接続されていても、トップページから普通に("Start_Poken.html"を使わず)ログインすると、デバイスは認識されていなかった。
というわけで、ネイティブ環境にアクセスできるプラグインを使っているわけでもなさそうだ。Mac ユーザーも安心。

セキュリティ
Poken/セキュリティ上のご注意 - オートログインはoff、でね。

仕組みのところで見たけど、接続先の scheme は https ではなくhttp になっている。上記のサイトではこれが問題視されてるんだけど、なんでだろう?情報は全部 URL で送っているんだから(それはそれで問題なわけだが)、SSL だろうがなんだろうが関係ない気がするけど。Poken サイト自体は SSL に対応しているし。昔は対応してなかったのかなぁ。セキュリティはあんま詳しくないからわからないんだけども。

デバイストークンや交換した名刺の情報がURL経由で漏洩する可能性については、どうにもならないから割り切ってあきらめるしかなさそうだ。信頼できないネットワーク環境(Proxy含む)では使わないように心がけよう。

さーて、誰か交換できる人見つかるかな。

2009年5月6日水曜日

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を確認する

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を確認する

DDD Sample Application version1.1.0: アプリケーション全体構造の確認

続いては、アプリケーション全体構造の確認。ここからようやく、ソースコードレベルでの確認に入っていく。単にソースを見るだけならダウンロードページからダウンロードすればいいが、効率よく読んでいくためには Eclipse 等 IDE のサポートが必要だろう。Maven を使っているので多少面倒くさいが、ダウンロードページには IDE で使うための情報も書かれているので、(特に Eclipse ユーザーは)参考にすると良い。

インフラストラクチャが依存するテクノロジ
実際のアプリケーションに入る前に、まずはインフラが依存する基本的なテクノロジ(ライブラリやフレームワーク)を確認しておく。採用するテクノロジは列挙にとどめる。
  • IoC (DI)コンテナ
    • Spring 2.5.6
  • Webフレームワーク
    • Spring WebMVC 2.5.6
  • ORM
    • Hibernate 3.3.1
  • ロギング
    • log4j 1.2.14

パッケージの概要
アプリケーションは、大きく以下の3つのパッケージに分割されている。
  • com.pathfinder
  • com.aggregator
  • se.citerus.dddsample
com.pathfinder は配送ルート候補を計算するために使われているのだが、これは一般的なグラフ構造を扱うパッケージになっており、Distillation の観点から見ると COHESIVE MECHANISMS(一般的な問題を解決するためのメカニズムを表す部分) になっている。もしかすると、GENERIC SUBDOMAINS(ドメインの観点から見て重要性が低い部分)とみなしても良いのかもしれない。

aggregator は 配送状況を一括更新するための Java-XML マッピングと Webサービスを扱っているパッケージだが、この部分を interfaces に含めず独立したパッケージとして扱っている理由がよくわからない。Webインタフェース用の Servlet や Spring WebMVC の Controller が interfaces パッケージに含まれることを考えると、Webサービス用だけが独立しているのは理解しづらい。

se.citerus.dddsample は、CORE DOMAIN(ドメインの観点から重要度の最も高い部分)、またはCORE DOMAINを含むパッケージとなっているはずで、主要な機能はすべてここにおさめられている。

レイヤの確認
se.citerus.dddsample パッケージはさらに、以下の4つのパッケージで構成されている。
  • interfaces
  • infrastructure
  • application
  • domain
このうち、interfaces を除く3つは、the book で基本的なレイヤとして紹介されているもので、それがそのままパッケージ構造に反映されている。これらレイヤの概要は、Architectureと題されたページに書かれている。
# なお、テストパッケージにはこれらに加えて「scenario」が存在し、シナリオテストが収められている。

INTERFACES
今回から新たに導入されたレイヤ。Webブラウザ等のヒューマンインタフェースを含む、外部システムとのインタフェースすべてが配置される。入力データの解釈、検証、変換や、出力データのシリアライズはこのレイヤが担う。DTO も含まれる。
# だったらなおさら、なんで aggregator が独立パッケージなのかわからないが

このパッケージは、さらに以下の3つのパッケージで構成されている。
  • tracking
  • booking
  • handling
これは、ビデオで紹介されている主要なインタフェースに対応…つまり、 tracking が Customer による配送状況の確認に、booking が配送手配者による積荷の管理に、handling が配送業者による配送状況の更新に対応している。

これらのパッケージは、それぞれがさらにテクノロジ依存のサブパッケージ(web, file, wsなど)を持っているが、この部分の詳細は次回以降で確認する。

APPLICATION
積荷の新規登録(Booking)と配送状況更新(Handling)に関するアプリケーションサービスとその実装、それに ApplicationEvent というイベント通知用のインタフェースから構成されている。アプリケーションサービスに配送状況確認(Tracking)がないのは、おそらくシンプルすぎてサービスが必要ないからだと思われる。Tracking は参照のみなので、Repository のメソッドを呼ぶだけですんでしまう。次回参照するが、この処理は interfaces パッケージ内の Controller に直接記述されている。
ApplicationEvent はいわゆるイベントオブジェクトではなく、ステートレスなイベント通知インタフェースで、ほぼサービスと考えても良いくらいのものだ。ApplicationEvent の実装はメッセージングを使うようになっており、そのインフラ色の強さからか infrastructure パッケージに配置されている。

テストやサンプルデータ生成用のユーティリティも配置されているが、これらは本来ならテストソースフォルダなりリソースフォルダなりに移すべきものだろう。

INFRASTRUCTURE
infrastructure パッケージは、以下の3つのパッケージで構成されている。
  • persistence.hibernate
  • messaging.jms
  • routing
persistence.hibernate パッケージには、何度も the book で述べられているとおり(ドメイン層にインタフェースだけ存在する) Repository の Hibernate 実装が配置されている。

messaging.jms にはその名のとおり JMS 関連のコードが配置されており、infrastructure の観点では特に興味深い点はないが、ApplicationEvent のような application レイヤの実装も含まれている点は面白い。Sample Application 1.0 にはinfrastructure パッケージが存在せず、すべて application につっこまれていたことから、application は infrastructure-aware で良いのかと思っていたが、application もきちんとインフラから分離するようだ。

routing パッケージに唯一存在する ExternalRoutingSerivice は、なんとドメインサービスの実装だ。ドメインサービスの実装が infrastructure に配置されるなんて構成は考えたこともなかった。このサービスは、COHESIVE MECHANISMS として紹介した pathfinder へのリモート接続を含むため、infrastructure に配置されているようだ。接続先の pathfinder サービスは javax.rmi.Remote を実装しているため、RemoteException をスローする。

DOMAIN
すべての中心となる部分。パッケージは以下のように分割されている。
  • model
  • service
  • shared
model には、ENTITIES, VALUE OBJECTS, DOMAIN EVENTS, REPOSITORIES, FACTORIES といったDDDのビルディングブロックが含まれている。また、各種 Exception も含まれている。あまり説明はいらないだろう。

service には、ドメインサービスが含まれている。今回は、先ほど infrastructure で説明した RoutingService しか配置されていない。

shared には、DDDビルディングブロックを表す各種インタフェースや、Specification 関連の基本クラスなど、まさにドメインレイヤの基盤になるクラスたちが配置されている。

まとめ
まずは主要な部分である se.citerus.dddsample と補助的な部分である com.aggregator/com.pathfinder とは明確に分離できる。主要なドメインである dddsample は、(interfacesが追加されているものの)基本的には the book に忠実に分割されていることがわかる。

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

DDD Sample Application version1.1.0: ユースケースの確認

さて、まずは Sample Applicatin のユースケースを確認する。これには、実際に動かしてみるのが一番だが、そこまでするのが面倒な人のために、丁寧にもビデオが用意されている。なので、確認はこのビデオに沿って進めていきたい。

概要
細かい説明に入る前に断っておきたいのだが、このアプリケーションは the book(Evans の DDD本)で何度も例としてあげられている、積荷の出荷とトラッキングを扱ったアプリケーションがもとになっている。イメージとしては、DELL やら Amazon やらで商品を買った際に通知されてくる、配送状況確認ページを思い浮かべてもらうとわかりやすいと思う。
このアプリケーションではさらに、上記のような商品を買った側(Customer)からの配達状況の確認だけでなく、商品提供側(DELL や Amazon)による配送の手配や、配送業者による配達状況の更新までできるようになっている。最初にこれくらいのイメージを持っているとわかりやすい。
というわけで、ビデオはこれ。とりあえずは、ざっと画面のイメージだけ確認できればOK。細かい内容はこの後にも書いてます。逆に、ビデオの内容が完璧にわかれば、この後はほとんど読む必要ないです ^^;



このアプリケーションには、以下のような3つのインタフェースがある。
  • 積荷(Cargo)のトラッキングインタフェース
  • 配送予約とルートの管理用インタフェース
  • 配送状況の更新インタフェース
最初の2つは Web、3つ目には Swing によるリッチクライアントアプリケーションと、ファイルトリガのバッチアプリケーションの2種類が用意されている。

積荷のトラッキング
これは、顧客(Customer)が注文した積荷の配送状況を確認するためのもので、トラッキングIDを入力できるだけのシンプルなインタフェースになっている。ここにトラッキングIDを入力して送信すると、以下の情報が表示される。
  • 積荷の到着予定時刻
  • 現在の積荷の場所
  • 次に積荷が向かう場所
  • 積荷の配送履歴
積荷が間違って(予定とは異なるルートで)配送されてしまった場合には、画面上に「Cargo is misdirected」とエラーメッセージが表示され、次に積荷が向かう場所は表示されず、積荷の到着予定時刻は不明と表示される。また、配送履歴のうちミスがあった部分に「×」がつく。
# 実際、顧客に直接こんな情報表示するんかいなという感じがするが…。
後で出てくるのだが「配送状況の更新インタフェース」にて誤った情報を送信した場合にこのように表示されることになる。

配送予約とルートの管理
商品の配送者(DELLとかAmazonとか)が、積荷の新規登録や配送ルートの選択を行うインタフェース。以下のようなことが可能。
  • 積荷の状況確認
  • 積荷情報の更新
  • 積荷の新規登録(配送予約)
  • 積荷の配送ルート候補の検索および確定
積荷の状況確認機能では、積荷情報の一覧表示、詳細表示ができる。詳細画面では積荷情報の更新に進むことができ、配送先のみが変更できる。「モノ」を管理するための典型的なインタフェース構成で、業務システムを作ったことがある人なら見飽きているだろう。
積荷の新規登録では、一度にすべての情報を入力しきるのではなく、まずは出発地点と到着地点、そして到着デッドラインのみを登録する。この時点でトラッキングIDが払い出されるが、配送ルートはまだ決まっていない。配送ルートは、配送ルート候補の検索および確定機能で別途確定させる。

配送ルートの検索と確定は、このシステムで一番面白いところだろう。入力された情報をもとに、配送ルートの候補がいくつか表示される。管理者はこの候補を確認し、好ましいルートを選択して配送ルートを確定させる。

なお、ビデオでは到着地点の更新は実行されていないが、既に配送ルートが選択された状態で到着地点を変更しても、エラーになることもなく警告メッセージのようなものも表示されない。かなり寛容な作りなようだ。

個人的には、こんなことに興味がある。後で確認してみたい。
  • Cargoに別々の状態を持たせているのかどうか
  • トラッキングIDをどのように払い出しているのか
  • 配送ルート候補の検索で使われる、ルート選択ポリシーの種類とその扱い

配送状況の更新
配送業者(クロネコとか佐川とか)が、配送状況を随時更新するためのインタフェース。このアプリケーションはシンプルで、1画面しかない。ここに、以下のような情報を入力する。
  • 更新時刻
  • トラッキングID
  • 便(Voyage)
  • イベント発生場所のコード
  • イベントタイプ
便は必須項目ではないらしく、イベントタイプによっては必要ないようだ。例えば、工場から積荷を受け取るイベントが発生した場合、この時点では便は関係ないので入力は不要だ。

項目名に「イベント」とあるように、これは DDDのビルディングブロックの1つである「DomainEvent」を発生させるためのインタフェースであるらしい。これだけがリッチクライアントとして作られているのは、配送の現地(倉庫や船)から更新することを考えるとバーコードなどの専用端末からの更新が現実的なためで、Swingは端末を簡易にエミュレートしているのだろう。

個人的には、このあたりに興味がある。
  • バリデーションが存在しないのは、DomainEventパターンの適用によるものなのか、端末エミュレートによるものなのか

配送状況の更新(バッチ)
Swingアプリケーションとは別に、配送状況を一括更新するインタフェースも存在する。これは、タブ区切りのテキストデータが指定のフォルダに置かれると、そのデータをもとに自動的に配送状況が一括更新されるというものだ。一括更新であること以外の機能は Swingアプリケーションと同じで、テキストデータの1行として記述する項目も、Swing アプリケーションと同じ。ビデオでは、配送状況の更新インタフェースの一部として扱われている。

まとめ
というわけで、ビデオを見るとだいたい何をやっているかはつかめると思う。一応、次回以降で確認するイベントをあげておく。
  • 配送状況の詳細表示
  • 積荷の一覧・詳細表示
  • 積荷の新規登録
  • 積荷の状態更新
  • 配送ルート候補検索
  • 積荷への配送ルート設定
  • 配送状況の更新
  • 配送状況の更新(バッチ)
次回は、これらイベントの個別のハンドリング方法を確認する前段階として、アプリケーションの全体構造を確認する。

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

DDD Sample Application version1.1.0 を確認する

ようやく Evans の DDD本(the book)をすべて読み終わった(長かった!)ので、オフィシャルに提供されているサンプルアプリケーションである DDD Sample Application を確認し、具体的な実装方法やビルディングブロックの実例を見つつ理解を深めておきたい。昨年リリースされたバージョン1.0は軽く確認したのだが、3月25日にリリースされたバージョン1.1.0はまた結構変わっているようなので、あらためて最新のDDDアプリケーション構造を確認していく。
とりあえずは、こんな感じで進めていく予定。
  1. ユースケースの確認
  2. アプリケーション全体構造の確認
  3. ユースケーススライスから見たアプリケーション構造の確認
  4. ドメイン構造の確認

ユースケースの確認
このアプリケーションが何を目的として作られ、どんな機能を提供するのかをユーザー視点で確認する。


アプリケーション全体構造の確認
細かいコードの確認に入る前に、アプリケーション全体の構造がどのようになっているのかを確認する。主に、Distillation や Large-Scale Structure の観点からアプリケーション全体構造の特徴を把握する。


ユースケーススライスから見たアプリケーション構造の確認
機能的な観点から見たアプリケーションの構造を確認する。各ユーザーイベントに対してどのようなソフトウェア要素が対応し、どのようにドメインレイヤにつなげていくのかを把握する。主にプレゼンテーション層(interfacesパッケージ)、サービス・アプリケーション層(applicationパッケージ)、インフラ層(infrastructureパッケージ)を確認する。


ドメイン構造の確認
ドメイン層がどのように構成されているのかを確認する。メインの部分。


最後までできるかな…てか予定どおりいくかな…。

コンテンツ一覧:
  1. DDD Sample Application version1.1.0: ユースケースの確認
  2. DDD Sample Application version1.1.0: アプリケーション全体構造の確認

2009年5月2日土曜日

DSM(DomainSpecificModeling) + DDD

DDDオフィシャルサイトに掲載されている、DSM(Domain Specific Modeling)に関するインタビューを起こしてみた。オリジナルのビデオはここ。どうでもいいと思ったところや一部よくわからなかったところは飛ばしてるし、間違っているところもあると思います ^^;

はじめに
Interviewer(以下"I"):
Peter Bellが来てくれた。自己紹介してくれる?

Peter Bell(以下"P"):
カンファレンスなど、色んな場面でこのトピックについて話している。DSMやコードジェネレーションについて話すのにベストな人間だと思う。

DSMとは
I: 今日はDSMについて話したいが、まずはDSMが何か教えてもらえるだろうか?

P: 役に立つ良い定義としては「開発プロセスのスピードアップ、アプリケーションの一部または全体の生成、コードベースのメンテナンスなどのためにDSLを使うこと」のように言えると思う。

I: じゃあ、DSLって何?

P: 3つのコンポーネントがあると思っている。
まず1つは、Javaなどの汎用言語とは異なり、特定の問題を解決するためのものであるということ。
2つ目は、コンストラクションのレベルをひきあげるものであること。JavaやC#とおなじようにかかなくても良い。
3つ目は、実行可能であるということ。ユビキタス言語は必ずしも実行可能でなくてもいいが、DSLはジェネレーションやインタープリタを通して実行できる。

I: それがユビキタス言語とDSLの違いなんだね?

P: その通り。DSMはアプリケーションを生成したり実行したりするのに十分なユースケースのセットにフォーカスしていて、ユビキタス言語から直接アプリケーションを生成する。こうすることで色々メリットがある。

I: プロジェクトでは、会話で使われるようなユビキタス言語もあればコードにあらわれるユビキタス言語もある。後者は実行可能なユビキタス言語、つまりDSLなのか?

P: そう言える。えーとここで、DSMの世界で重要なDSLの興味深い区別について話しておきたい。内部DSLと外部DSLだ。内部DSLはAPIとかRuby Groovyのような言語を使って、人間が読めるようなDSLをコード内に作るアプローチだ。このアプローチではDSLの利点をフルに受けることはできない。なぜなら、ホスト言語の構文上の問題を気にしなければならないからだ。
外部DSLはよりパワフルで、DSLをコードの外部に持ち出し、DSLのステートメントを書くためのツールを別途用意できる。これは、箱と線で書けるようにもできるし、テキストで書くようにもできるし、スプレッドシートで書くようにもできる。
外部DSLの利点はジェネレートされた個々のステートメントをテストしなくていいので開発やメンテナンスのスピードをあげることができること。

I: 利点についての話があったが、他に利点はある?
P: 最も大きなものとしては、テスティングコストの低下があげられる。よくできたユビキタス言語は確かに、ステークホルダーとプログラマの間のコミュニケーションの問題を改善できる。しかし、コードへの変換時にはインピーダンスミスマッチがあるため、ここで理解の齟齬が発生し、バグの発生につながってしまう。DSMではこのような事態を完全に避けられる。ステークホルダーとコミュニケートし、彼らが話すことをそのまま書き、コードをジェネレートできる。

I: 言語が正しければ、テストの必要はない?
P: セマンティクスはもちろんテストする。DSLのステートメントが正しくその意図を表しているか、つまり与えられたステートメントが正しくジェネレートされるか、もしくは正しくインタープリタで実行されるかどうかはテストするということだ。しかし、すべての個々のステートメントをテストする必要はない。

I: 開発のスピードがあがるとのことだが、アップグレードも頻繁になるのでは?
P: その通り。頻繁にビルドするようにもなる。だが、メンテナンスの際に細かなコードブロックを見るのではなく、意図が明示されたDSL(ユビキタス言語)を見ればいいからメンテナンスも楽になる。

I: 利点にはすべてトレードオフがある。モデルが変更された場合、言語も変更することになると思うが、既存のツールでこの変更に対応するのはどれくらい難しい?
P: シンプルなリファクタリングサポートは既に存在しており、軽微な変更には問題なく対応できるが、根本的な変更に対しては無力。ただし、これはJavaやらC#を使っていても同じ。
ドメイン(に関する理解)が安定するまで、ジェネレータやインタープリタの作成を先延ばしにするのも良いかもしれない。

DSMにまつわる誤解
I: 誤解について話したい。DSMについては色んな話(誤解?)を聞いたが、そのうちのいくつかはUMLについてだった。
P: そりゃDSMではもっとも有害な誤解だと思う。仮に私がDSMをMDAやUMLからはじめていたとしても、UMLは使い続けなかったと思う。UMLは世界中で多くの人が使っていて、標準化もされており、多くの利点がある。しかしUMLの問題は、記述のレベルは抽象的すぎるということ。ステレオタイプでドメインのコンセプトを表せるようになってはいるが、クラスやメソッドが何をすべきかは何も決められていない。クラスごとに箱が1つ用意されるだけだ。なので、モデルからモデルへの変換やモデルからのテキストへの変換は簡単ではない。XMIみたいに。MDAがDSMの小さなサブセットにすぎないことを理解しておくことは非常に重要だと思う。
MetaCaseとかOpenArchitectureWareはEclipseプラグインも提供していて、開発者はテキスト指向のDSLを書く事ができる。これは、MDAに比べるとだいぶ使いやすいと思う。

DSLとドメインエキスパート
I: DSLはドメインエキスパートが書けるようにするべき?
P: ここはDSMに関してさまざまな関心がある部分だ。秘書が会計システムを書けるようにしようとしている、とかね。DSMアプローチでドメインエキスパートがDSLを書けるようにすることはできる。が、より利点があるのはドメインエキスパートが読めるようなDSLを作る事。ドメインエキスパートとプログラマの間のミスコミュニケーションを防ぐ事ができる。ユビキタス言語が引き起こしがちな、実装時の齟齬を防ぐ事ができる。

I: 少し自分の経験について話させてもらうと、ユニットテストやシナリオテストを書いてドメインエキスパートと共有した時、ドメインエキスパートはJavaなどのシンタックスノイズに悩まされていた。ユビキタス言語を使っていたとしても、ドメインエキスパートには(コードが何を意味しているのか)説明しなくてはならない。DSLを使えばわかりやすくできるように思うが、どうか?

P: BDDのようなアプローチが既に存在しているよね。統合テストではあまりできていないが、DSLが貢献できるいい例だと思う。統合テストにDSLのアプローチを持ち込むことで、ドメインエキスパートにとってとても読みやすくなる。将来はもっとよくなって欲しい。

Big Design Upfront or Agile
I: ドメインを前もって知っていれば、急激な変更が少なくなり、DSLにとって利点が大きいとするなら、前もってヘビーな分析作業をしなくてはならないのでは?
P: Big Design Upfrontをやりたがる人は最近ではほとんどいないと思う。コミュニティは、よりアジャイルなアプローチを取ろうとしている。MetaEdit + MetaCaseなどが良い例だ。これは、頻繁なモデルの変更を助けるような機能が豊富に用意されている。DSMは決してBDUではないことを強調しておきたい。

どの程度コード生成すべきか
I: アプリケーション全体を生成すべきか?
P: コード生成については色んな考えがある。アプリケーション全体を生成しなければならない、というものもあれば、アプリケーション全体を生成なんてできっこない、というものもある。これは、どちらも間違っていると思う。
100%の自動生成は可能ではある。変数のないテンプレートのようなものを使えば、既存のコードに1対1対応するようなコード生成はできる。だが現実には、(このようなアプローチで)すべてのコードを生成するのは意味がない。
DSMの世界では、色んなコードジェネレーションのツールがあり、コードを書くのが非常につらい、クラスやAOP、イベントドリブンプログラミングなどを書かなくてすむようにしている。これらのツールではジェネレートされたコードとカスタムコードは分離して扱うことができ、これら両方でアプリケーション全体を構成することができる。100%のコードを自動生成する必要はない。ドメインモデルが比較的リッチな場所を探し、ドメインエキスパートとコミュニケートして、DSLを作る、といった作業にスタッフをあてるといいだろう。そういうエリアにDSMを適用するのは労力に見合った利点があるからだ。

DSMに適したプロジェクト
I: DSMはどんなプロジェクトに向いている?
P: 良い質問だ。プロジェクトをよりDSMに適したものにしてくれるいくつかの要素があると思う。
1つ目、これはあまり言ってないことだが、DSMの熟達度だ。SpringやAOPを導入する時と同じで、最初に技術を導入するときには色んな面倒なことがある。また、動的言語、パーシャルクラス、クロージャなどを使う事でエレガントなアプリケーションをすばやく作り上げる事ができるが、開発者がそれをメンテナンスできるとは限らない。
なので、DSMの経験がある人をプロジェクトにいれて、経験をプロジェクトに持ち込むことが重要だ。これは非常に助けになる。

他の話としては、能力が許す範囲でいくつものプロジェクトを並行にすすめられるしても、投資を回収するには、ドメインの特定のエリアにとどまり、DSMがより長い時間使われるようにしなければならないということだ。DSMにはかなりのオーバーヘッドがあるので、6回以上やりたいかもしれないが…外部DSL用のツールがこの種の問題を解決しようとしてはいる。

水平的ドメインと垂直的ドメイン
ドメインは水平的にも垂直的にもなり得ることを理解するのも重要だ。
垂直的なドメインでは、成果は(例えば)1つの保険会社のためだけに使われる。

だが一方で、Webの部分は何度も同じようなものを生成することになる。例えば、ドレスショップ向けのeコマースと法律事務所向けの文書リポジトリなど。これが水平的で技術的なドメイン。水平的な技術ドメインはどこでも変わらないので、我々はモデル、コントローラー、バリデーション、リレーションシップなどWebアプリケーションを迅速に構築するための一連のDSLを既にもっている。
1つの企業で働いている人…つまり垂直的ドメインを対象にしている人たちは、領域を限定することで、DSMの能力を最大限に生かし、素早くアプリケーションを作ったりメンテナンスしたりしている。多量のJavaのコードに戻ることなく、少ない文で正確に意図が表現されているからだ。

現実のDSL/DSLの構築方法
I: HibernateはDSL?
P: HibernateはORMに関するDSLをいくつか示している。
こういう言語を作る場合、どのように始めればいいか聞かれることがあるのだが、盗めばいい。
ORM, DI, AOPのような水平的ドメインを扱うなら、既に多数のDSLが存在するので、この上にかぶせる形でシンプルな、カスタマイズされた自分のユースケース向けの小さなサブセットを作れば良い。
水平的ドメイン(たぶん垂直的の間違い)では、XMLスキーマも良い出発点になるだろう。銀行や保険の業界では、標準がすでに存在している。それらのサブセットを作る形で、必要に応じて自分の状況に応じてカスタマイズすればいい。

ドメインのコアコンセプトを理解する方法
I: ある特定のドメインを扱うような場合、ドメインに関する市販本などもあるが、読んでる?ドメインのコアコンセプトをどうやって知っている?
P: DDDと連携していくのがいいと思っている。
DDDは(DSLにつながる)ユビキタス言語を作るためのアプローチをあげており、DSMはジェネレートやインタープリタを使って効率的に実行する方法を持っている。これら2つのコミュニティは、お互いにディスカッションするといいと思う。既に2つのコミュニティでは活発なコミュニケーションがあると思っているが、より強くしていきたい。DSMコミュニティはDDDコミュニティからよりよく言語を設計する方法を学べるし、DDDは言語を実行可能にする方法を学べる。

I: 今日はどうもありがとう。

雑感
一部マーケ的においはするものの、全体としてはまっとうなことを話している印象。コード生成に関するスタンスも現実的だとは思う。しかし、DSMという新たな用語は必要なんだろうか?世の中を混乱させたり、コミュニティの評判を下げかねないので"DSL"を使い続けた方が良いのではないかと感じた。
また、DDDが向かうべき方向として良いのかどうか、今ひとつ確信が持てない。実行可能なユビキタス言語を作ることで、色んなロスがなくなるというが、それが本当かどうかは具体例を見てみないとなんとも言えないな。ドメインエキスパートの可読性を保ったままDSLを構築できるのだろうか?ユビキタス言語全体がDSLとして構築されるとは思えないのでサブセットになるのだろうが、一部だけ妙に形式的になるのだろうか?モノが見てみたい。