DCIによるWebアプリケーション - 2:アーキテクチャ

前回ドメインレイヤを実装した「割り勘アプリケーション」のプレゼンテーションレイヤを実装し、全体的なアーキテクチャについて考察する。

導入

前回のエントリでは、DCIアーキテクチャの概念に従って「割り勘アプリケーション」のドメインレイヤを実装しました。その後、Wicketを使用してプレゼンテーションレイヤを実装し、「企画」から「会計」までを一通り動かすことができるようにしました。今回はこの作業を通じて得られたDCIアーキテクチャにおける考慮事項について解説していきます。(ソースコードこちらで公開しています。)

※ 配色については、こちらのサイトを参考にさせて頂きました。

DCIとMVC

フロントを含めた基本的なアーキテクチャを以下に示します。

PageクラスとActionクラスの責務分割については、「Pageクラスはコンポーネントの生成と画面遷移のみを行い、ContextおよびRepositoryへのアクセスはActionクラスが行う」方針としています。MVCフレームワークを使って開発している方であれば、特に違和感のない構成だと思います。このように、DCIアーキテクチャMVCモデルの中にすっきりと収まりますが、モデルに対する操作がコントローラ内部に散逸していない点は重要です。なお、ActionクラスがContextではなくRepositoryを直接使用する時の条件については後述します。


例として、AccountActionのコード(抜粋)を示します。

def createMainFormModel(partyId:Int):AccountingMainForm = {
        
    val mainFormModel = new AccountingMainForm
        
    val party:Party = PartyRepository.forPartyId(partyId)
    mainFormModel.partyName = party.name
    mainFormModel.schedule = format.format(party.schedule)
    mainFormModel.location = party.location
    mainFormModel.sum = party.sum.toString

    mainFormModel
}
    
def calculate(partyId:Int, slopeId:Int, 
        paymentListItems:List[PaymentListItem]) = {
        
    val accounting:Accounting = new Accounting(partyId)
    accounting.adjustBy(slopeId)
    accounting.commit
            
    val party:Party = accounting.party
    val participants = party.participants.values
    participants.foreach { participant =>
        val paymentListItem = new PaymentListItem
        paymentListItem.name = participant.name
        paymentListItem.role = participant.role 
        paymentListItem.payment  =
            PartyRepository.paymentOf(partyId, participant.name)
        paymentListItems.add(paymentListItem)
    }
}

コンテキストとデータのライフサイクル

今回プレゼンテーションレイヤを構築したことに伴い、ドメインレイヤのクラス群にも一部修正が入ってます。その中で最大のものは、データのライフサイクルに関するものでしょう。問題となった部分について、以前のテストコードを再掲します。

// id
val partyId = 20
...
// Planning
val planning:Planning = new Planning(partyId)
planning.addPerticipant("Ken", "Chief")
planning.addPerticipant("Taro", "Manager")
planning.addPerticipant("Hanako", "Member")
planning.addPerticipant("Yuji", "Novice")
planning.commit

一見良さそうなのですが、PlanningコンテキストはPartyオブジェクトが生成されるタイミングです。コード内ではpartyIdを渡して初期化していますが、本来はクライアントが恣意的に採番できるものではありません。今回はコンテキスト内部に採番ロジックを移動するように修正しました。

// Planning
val planning:Planning = new Planning
planning.setup("Chris's Birthday", _2010_06_03, "Tokyo")
planning.addPerticipant("Ken", "Chief")
planning.addPerticipant("Taro", "Manager")
planning.addPerticipant("Hanako", "Member")
planning.addPerticipant("Yuji", "Novice")
planning.commit

安易な考慮漏れと言えなくもないですが、ここから得られる教訓もあります。それは「コンテキストはデータのライフサイクルに密接に関わる」ということです。前回はユースケースという視点からのコンテキストの設計に終始しましたが、データオブジェクトがどのタイミングで生成され、どのフィールドがどのタイミングで設定されるのかという観点からも考える必要があります。

まとめ:コマンド-クエリ分離

最初に示したアーキテクチャを再掲します。

問題はActionから伸びる2本の矢印、特にRepositoryに向けられたものをどう考えるかです。これは具体的にはプルダウン内容や一覧の生成、つまりはユーザを特定のコンテキストに導くためのデータ取得ロジックです。「ActionはRepositoryを直接参照しない」というルールを立てるのであれば、これらに対しても何らかのコンテキストを割り当てることは不可能ではありません。しかし、実際にはドメインレイヤの設計がそこまで画面に引きずられるのは好ましいものではありません。この点について考えるにあたり、これまでの議論をふりかえります。

  • DCIはユーザのメンタルモデルを適切にモデリングするための手法である。
  • DCIによってデータの操作に対して適切な意味を与えることができる。
  • コンテキストはデータのライフサイクルと合わせて考えるべきである。
  • コンテキストに縛られず、ユーザをコンテキストに導くためのクエリは必要である。

コンテキストとリポジトリの責務分割を「コンテキストにおいて本来行うべきなのは状態を変化させることであり、リッチなクエリロジックは必要に応じてリポジトリに実装すれば良い」と考えれば、ここにある種の「コマンドクエリ責務分離」(Command and Query Responsibility Segregation:CQRS)を導入することができます(CQRSとはコーディングの原則であるコマンド-クエリの分離をアーキテクチャレベルで適用させるという構想です)。


この戦略を採用することによって、設計はさらにすっきりとしたものになります。実装中に頭をよぎった懸念で、CQRSの導入により解消されたものの例を示します。

  • 「会計士」のadjust処理に「管理者」が登場する必然性がない。
  • 「会計」コンテキストの参照ロジックが、名前から金額を取得するのか、一覧を出すべきなのかなどについてドメインからだけでは方針が立てられない。

これらはさらりと流して実装することもできてしまいますが、「メンタルモデルとの整合性」を突き詰めるには丁寧に考える必要がある問題であり、それが解決されることには重要な意味があると言えます。


ここまで、DCIに従ったドメインモデルをMVCに組み込み、コンテキストの役割を限定した上でCQRS風のアーキテクチャを導入してきました。これで一旦はWebアプリケーションのアーキテクチャの概要が見えたと言えるのではないかと思います。次回は、現在はメモリ上に保存されているデータの永続化を実装し、DCIアーキテクチャに関する考察を深めていきたいと思います。

補足

特にWicket使いである訳でもない私がこのフレームワークを選択した理由としては、プレゼンテーションレイヤの動作だけをサポートしてくれる軽量なフレームワークであり、Scalaでの実装に実績があったことが挙げられます。Wicketでなければならない理由は特にありません。またアプリケーションについて、一通り動くようにはなっていますが、認証、排他制御、入力チェックなどは実装していません。