ドメイン駆動式ソフトウェアの育て方

レッツゴーデベロッパー2011での発表原稿とスライド

導入

2011年05月28日「レッツゴーデベロッパー2011@仙台」が開催されました。このイベントのテーマは「共有と交流」。"「共有」には、最新技術、知識、復興への想い、それぞれの決意を共有することを、「交流」には、東北と東北圏外のデベロッパーやコミュニティ同士の交流を深めることを込めて。" このイベントにてDDDセッションに登壇させて頂きましたので、そのときの発表原稿とスライドを公開致します。なお、当日はワークとして参加者の方にペアモデリングを行って頂きましたが、このドラフトではその部分を割愛しています。


さて今年4/9にDDD日本語版が出版されました。それから2ヶ月弱、翔泳社様から、はやくも増刷のお知らせを頂きました。多くの方々とおかげと深く感謝しています。さて、この増刷が意味しているのは今や千人単位の方の手元にDDD日本語版があるということです。出版と時期を同じくして著者のエリックも来日しました。読書会も同時多発的に開催され始めています。日本におけるDDDは、間違いなく次のステージへと進んでいます。いわば、「読むことが目的だった時代の終焉」。読み、理解したら、その次に来るのは実践でしょう。DDDの考え方に従ってアプリケーションを作ったらどうなるのか、ということを考えることが今回のテーマです。

ドメイン駆動設計とは?

ドメイン駆動設計の考え方

先日、とある方とお話していたときに、非常に鋭い言葉を聞かせて頂きました。「ユーザさんには、自分が「ユーザ」であるという意識はない。彼らは自分の仕事をやっているだけなんだ」と。これは非常に的確でありながら、開発者としてはなかなか気づくことのできない視点だと思いました。DDDの根本的な関心もやはり同じようなところにあります。「システムを作る上ではまず顧客がどのようなビジネスを行っているかが重要である」、言葉にしてしまうと当たり前すぎるこのメッセージがドメイン駆動設計の出発点になります。逆に言えばこれまで我々は技術などにとらわれて、顧客の仕事を理解するという本当に重要なことを忘れていた、ということでもあるでしょう。


ドメイン駆動設計ではさらに、顧客のビジネスを顧客自身の言葉で理解することが求められます。単純にとらえれば、「同じ言葉で会話をする」、つまり「顧客が使っている言葉を自分も使う」ということです。ただ同時に、顧客が描いているモデルを共有するということでもあります。「こういう場合は特殊ケースなのでフラグを立てて・・・」ということを、勝手にやってはいけないということですね。勝手なフラグは言葉として相手が理解できないだけでなく、それを成立させる別のモデルを導入してしまっていることも示唆しています。会話やドキュメントなど、あらゆる場所で顧客の言葉を使うこと、それがユビキタス言語というパターンです。


「モデル」という言葉が出てきました*1。ここで言うモデルとは、「顧客の目から見た業務の姿」だと考えてください。モデルは現実そのものではありません。人間が現実をとらえたときの姿です。その際には、適切な抽象化が行われ、不要な詳細は捨て去られます。「何が重要で、何が重要ではないか」、その取捨選択の中にモデルの本質があります。開発者には、顧客の持つそういうモデルを共有することが求められます。先ほどのフラグの例で言えば、一見「特殊ケース」に見えるものが、業務の全貌を理解している人からすれば正常パターンの1つでしかない、ということはあり得ることです。


さて、せっかくモデルを共有しても、実装時に失われてしまっては元も子もありません。共有したモデルをソフトウェアの中に持ち込むこと、これがモデル駆動設計と呼ばれる重要パターンです。モデルと実装を結びつける上ではなんらかのパラダイムが必要になりますが、それにあたってDDDが準拠するのがオブジェクト指向です。概念をクラスとして表現することで、顧客の描くモデルをソフトウェアの中に反映するのです。

アーキテクチャ

そうした場合ソフトウェアはどのようなアーキテクチャになるのでしょうか。それについては第2部で語られることになります。中でも概要を理解する上でポイントとなるのがレイヤ化アーキテクチャという考え方です。レイヤ化アーキテクチャでは次の4つのレイヤが説明されています。

この中で最も重要なのがドメイン層です。これはまさに、モデルが存在するための空間となります。


ソフトウェアの中にドメイン層を確保する上で重要な役割を果たすのが、リポジトリです。通常のトランザクションスクリプトの場合、SQLを発行した結果得られるリザルトセットを直接使うケースがほとんどだと思いますが、DDDの提唱するアーキテクチャでは常にモデルオブジェクトを操作します。そのためリポジトリがクライアントからの要求に応じて、必要なオブジェクトをクライアントに渡すことになります。こうすることでクライアントは、オブジェクトがメモリ上にあるかのように操作することができます。

プロセス

「顧客はドメインをとらえるモデルを持っている」と言っても、そのモデルをアップフロントにすべて理解し実装することはできません。第一にドメインが複雑であれば、開発者側がすべてを一度に理解することはできませんし、顧客自身も実装可能なレベルで完璧なモデルを事前に持っているわけではないからです。したがって、顧客とドメインモデルを共有するプロセスは当然イテレーティブなものになります。


これについては、現在Ericが整備を進めているプロセスがあります。それが「モデルを探究するうずまき」です。このうずまきはまず「シナリオ」から始まります。顧客からシナリオを聞き出しつつ、それをモデル化していきます。モデルができたらすぐに新しいシナリオを聞いて、モデルに揺さぶりをかけます。この内側のループにかける時間は1日2日程度だそうです。ある程度モデルができあがったら、「コードプローブ」に入ります。シナリオをテストとして実装し、モデルが実際に使えるかどうかを確認します。結果はシナリオにフィードバックし、また新しいループが始まります。

イベント参加受け付けをモデリングする

2011年4月9日のDDD前夜祭では、@t_wadaさんによる"DDD Boot Camp"が開催されました*2。そのときのお題は「DDD前夜祭の申し込みをモデリングする」でした。今回は「レッツゴーデベロッパー2011の申し込みをモデリングする」と置き換えて考えてみたいと思います。


ポイントは先ほどの「モデルを探究するうずまき」の内側のループ、シナリオとモデルの往復を試すことにあります。その際、主要な概念とその関連性が見えてくれば成功だと言えるのですが、実際にやってみると意外と行き詰まります。シナリオを考える際に、「申し込みの締め切りをどうするか」という重要かつやや複雑な処理にいきなり飛び込んでしまうと「上限がどう決まるか?」「当日キャンセルは見込むか?」といった疑問がわいてしまって手が止まってしまうのです。できれば、わかりやすくシンプルなところから始めて、すこしずつ複雑なものに取り組んでいくという段階を踏みたいところです。シナリオの例を示しましょう。

  • イベント情報を表示する
  • 申し込みを受け付ける
  • 参加者一覧を表示する
  • 申し込みを締め切る

ソフトウェアを「育てる」

このようにすこしずつソフトウェアとモデルを膨らませていく上で大きなヒントを与えてくれるのが、「Growing Object-Oriented Software, Guided by Tests (Addison-Wesley Signature Series (Beck))」です。この本ではソフトウェアを「育てる」ために、受け入れテストとユニットテスト(場合によってはインテグレーションテスト)というレベルの違うテストで2重のループを構成することが提唱されています*3


具体的には、まず「Walking Skeleton」と呼ばれる、動くための最低限の機能を作ります。その上で、エンドツーエンドの受け入れテストを書きながら、1機能ずつ作っていくのです。これを実際に試してみるにあたり、今回は技術スタックとして以下に示すものを使います*4


ケルトンでは、シナリオ「イベント画面の初期表示」を実装します。

def "イベント画面初期表示"() {
    when:
        go "/PlanTheEvent/event/show"
    then:
        $("h1").text() == "イベント情報"
        $("td#detail").text() == "レッツゴーデベロッパー"
}

これだけでも、やることは意外とあります。画面とモデルオブジェクト、データベースのテーブルを作る必要がありますし*5アーキテクチャも定めなければなりません。また、フレームワークに慣れていなければ、使い方も一通り調べる必要があります。


こうしたことを行ってはじめてテストが通るのですが、その後ある程度リファクタリングも行わなければなりません。たとえば、前述したテストコードからはHTMLに依存した記述を取り除く必要があります。

def "イベント画面初期表示"() {
    when:
        go "/PlanTheEvent/event/show"
    then:
        $("#pageTitle").text() == "イベント情報"
        $("#detail").text() == "レッツゴーデベロッパー"
}

この段階でモデルオブジェクトが1つできあがっています。


次は、申し込み受け付け画面への画面遷移を経て、登録処理のテストを書きます。

def "参加者一件登録"() {
    when:
        go "/PlanTheEvent/participant/apply"
        $("form").twitterId = "@digitalsoul0124"
        $("form").message = "よろしくお願いします"
        $("#register").click()
    then:
        $("#pageTitle").text() == "イベント情報"
        $("#detail").text() == "レッツゴーデベロッパー"
        $("#participantsCount").text() == "1"
}

このテストがグリーンになるころにはモデルに参加者オブジェクトが追加され、申し込み受け付けの基本モデルが完成します。


先ほど話題にあげた要件「申し込みの締め切り」はこのモデルをベースに考えることになります*6。申し込みの上限をどう決めるかは、その業務を実際に行っている人に聞くしかありません。仮に「イベントを行う部屋の広さに加えて、ある程度のキャンセルを見込む」という答えが返ってきたとしましょう。ここに「部屋」という概念が新しく追加されます。


10%のキャンセルを見込むと考えて、次のようなコードを書けば、とりあえずテストは通るようになります。

// 満席判定
boolean fullToCapacity() {
    participantsCount() >= (roomsCapacity() * 1.1)
}

しかし、ここには概念が1つ暗黙的に潜んでいます。


「キャンセルを見込んで、多めに予約を取る」という考え方は通常「オーバーブッキング」と呼ばれます。このオーバーブッキングのポリシーをモデルで表現します。

具体的にどのようなメソッドを持たせるべきかは、コードを書いて確認します。

def "満席/11人で満席"() {
    when:
        def overbookingPolicy = new OverbookingPolicy()
        def room = new Room(capacity:10)
        def LIMIT = overbookingPolicy.limitFor(room)
        def event = new Event(room:room)
        for(i in 1..LIMIT){
            event.addParticipant(new Participant())
        }
    then:
        overbookingPolicy.fullToCapacity(event)
}

ここまでで、前述したシナリオを実現するモデルがいったん完成します。

※ここまでのコードはgithubにて公開しています*7

まとめ

今回は「ドメイン駆動設計を実践する」という観点から、シンプルなところから始めて、すこしずつ育てていくという手法を実際に試してみました。シナリオと紐づけながらモデルを作っていくことについて、ある程度のイメージを持って頂けたのではないでしょうか。


このような開発はユーザにとっても開発者にとっても理想だと思うのですが、今の日本ではなかなかできないのが実情でしょう。しかし、「できない」と嘆くだけではなく、「実際に自分はできるのだろうか?」と自問し、チャンスがあったときに確実に実践できるようにしておくことが求められる時代になってきているのではないかと思うのです。




エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

Growing Object-Oriented Software, Guided by Tests (Addison-Wesley Signature Series (Beck))

Growing Object-Oriented Software, Guided by Tests (Addison-Wesley Signature Series (Beck))

*1:この点については、ドメイン駆動設計入門 - Digital Romanticismで詳しく説明しています

*2:http://www.slideshare.net/t_wada/devlove-dddbc

*3:この本の中ではモックを使ったユニットテストがきわめて重要なものと位置づけられていますが、今回の発表ではその点には触れていません。

*4:SpockとGebの選定にあたっては、@bikisukeさんにご支援頂きました。ありがとうございました。

*5:今回、永続化は行わずオンメモリで実装しています。

*6:参加者一覧の表示はこのモデルを使って実装できますので、ここでは割愛します。

*7:デモ時にあった警告は@nobeansさんにより修正して頂いています。また、@kiy0takaさんが受け入れテストをPageクラスを使って書き換えてくださっています。お2人ともありがとうございます!!