DCIアーキテクチャの理論とGrailsによる実装

第12回G*ワークショップにて行った講演のスライドと発表原稿

はじめに

11/9にJGGUG様主催にて第12回G*ワークショップが開催されました。そこで、「Model On Grails - DCIアーキテクチャへの道すじ-」と題しまして、DCIアーキテクチャについて講演させて頂きましたので、その際のスライドと発表原稿を公開します。


スライドはこちら


アジェンダは以下の通りです。



導入

今回のテーマは「DCIアーキテクチャ」です。日本では、2010年の1月にJames O. Coplien氏が来日して講演したことで知られるようになりました。Googleグループのメーリングリストを見る限りは未だ黎明期にありますが、何か大きく化けそうな気配を漂わせていると言えるのではないでしょうか。


このDCIアーキテクチャを提唱したのは、Trygve Reenskaug氏です。日本ではそれほど知名度が高くないような気もしますが、実は彼はMVCの提唱者でもあります。モデル・ビュー・コントローラと題された原稿が書かれたのは、1979年のことですから、MVCから30年を経て、今新しいアーキテクチャが生まれようとしている、と言うことができるでしょう。このDCIアーキテクチャの普及にあたって、先頭に立っているのが、James O. Coplien氏です。角谷さんをはじめとした方々の積極的な活動によって、やっと日本でも彼の名前が知られるようになって来ましたが、アジャイルの適用に関する「組織パターン」などを書いている、アジャイル業界ではかなり重要な人物の一人です。


両氏の著作についてはこのブログでもいくつか翻訳していますので、ここでご紹介します。


それでは両氏によって推進されているDCIアーキテクチャとは一体どのようなものなのか、それを生み出した問題意識から始めて、その特徴を確認し、実際にGrails上に乗せるとどのようなコードになるのかを見ていきます。

先駆者たちが遺したもの

まずは、以下の年表を見て下さい。

  • 1963年 Doug Englebart 氏によるマウスの発明
  • 1972年 Alan Kay氏によるダイナブック構想
    • A Personal Computer for Children of All Agesの発表
  • 1979年 Trygve Reenskaug氏によるMVCの提唱

いささか奇異な組み合わせに見えるかもしれませんが、これらの流れの根底には、マウスの発明者であるDoug Engelbart氏の「コンピュータとは人間の精神を拡張したものである」という思想が流れています。Reenskaug氏とCoplien氏によれば、こうした潮流が現在に残してくれたのが、「インタラクティブでグラフィカルなユーザインタフェースと、世の中のプログラミングにおけるオブジェクト指向言語の隆盛である」[Reenskaug & Coplien 2009]とされます。


GUI」と「オブジェクト指向」- これはどういう組み合わせなのでしょうか?モデル・ビュー・コントローラの構想は、ユーザのメンタルモデルをコンピュータの内部に写しとることを意図されたものでした。メンタルモデルとはユーザの世界のとらえ方、ユーザにとっての世界を写し取った精神の構造です*1。この精神のあり方は、概念とその構造を写しとるというオブジェクト指向パラダイムを用いてソフトウェアの中に組み込まれます。


こうしてソフトウェアの内部に組み込まれたモデルをユーザが操作するにあたり、ソフトウェアが提供するインタフェースを使用することになります。この時、ユーザに対してソフトウェアの内部にある自分のメンタルモデルを直接操作しているような感覚を与えるべきだ、という考え方が「直接操作メタファ」と呼ばれるものですが、それを実現する手段が、グラフィカルでインタラクティブユーザインタフェースである、ということになります。


ここまではきわめてシンプルで分かりやすいモデルだと思うのですが、実はこの時、1つ前提とされているものがあります。それが、構造によって思考をとらえるというオブジェクト指向パラダイムであり、さらに言えばその背後にある、人間の思考は構造によってとらえられるという仮説、あるいは、とらえようという戦略です。

オブジェクト指向の死角

Reenskaug氏は「オブジェクト指向の常識」と題された論文の中で、GoFデザインパターンに書かれているある示唆に着目しています[Reenskaug 2008 p.5]。その示唆とは、「システムがどのように動作するのかということについて、コードがすべてを明らかにしているわけではない」[Gamma et al 1995 813-24]というものです。少し文脈を補足する必要があるでしょう。これはオブジェクト指向プログラミングにおいて、実行時の構成がコードの構成とは異なっていることに起因します。コードの構成は「固定化された継承関係」から成り立っており、コンパイル時に確定するのに対して、実行時の構成は「コミュニケーションするオブジェクトという急速に変化するネットワーク」から成り立っています [Gamma et al 1995 788-99]。こうした実行時の構成を見渡せる場所はコードの中にはなく、むしろ、設計者によって規定されるのです[Gamma et al 1995 813-24]。


こうしたオブジェクト指向の性質から生じる帰結について、Reenskaug氏とCoplien氏は、人間の「思考」と「行動」、およびシステムの「構造」と「ふるまい」とを区別した上で、オブジェクト指向は後者をとらえることができていないと指摘します。すなわち、構造をとらえ、表現することはできたが、全体としてのふるまいをとらえることはできなかったというのです。その結果、現在の私たちが置かれているのは、アルゴリズムが各オブジェクトの間に断片化されてしまっており、可変性を担保するのに継承を使わなければならないという状況だとされます[Reenskaug & Coplien 2009]。こうしたとらえ方については批判的すぎるという見方もあるでしょうし、実際に継承を濫用することを戒める風潮はオブジェクト指向プログラミングの内部からも上がっています。Joshua Block氏による「継承よりもコンポジションを好め」という指摘が好例でしょう[Block 2008 p.81]。しかしここでは、オブジェクト指向のプラクティスとして、何が好ましいのかを考えるのではなく、そうした問題をReenskaug氏らがどう乗り越えようとしているのかを見ていきたいと思います。

DCIアーキテクチャ

DCIアーキテクチャの構想の根底には、すべてをクラスで表現することに対する疑問があります。システムがどのようなものであるのか、すなわちシステムに対する「思考」と、システムが何をするのか、というユーザの「行動」を分けて考えれば、これらは重なり合いながらも異なる位相に存在するものであり、同時に異なる境界を引くべきものとなります。ここで、クラスという概念は既に構造を表現するのに使われてしまっているため、別の概念を導入する必要が出てきます。こうしてDCIアーキテクチャの構成要素が登場してきます。


1つめの要素は「データ」です。これは従来のオブジェクト指向と同様にシステムの構造を表現するものです。ただし、ここにはリッチなふるまいが与えられることはなく、基本的な属性とそれに対するアクセッサを持つだけの純粋な構造となります。


次に、肝心のふるまいを表現するにあたっては、新しい概念が導入されます。それが「ロール(役割)」です。前述の記事「新しい構想」では、これを説明するのに「振込」が例として挙げられます。ある口座から別の口座に振り込む際、ユーザのメンタルモデル上はある口座が普通預金口座なのか、定期預金口座なのかということはあまり意識されず、むしろ、「振込元口座から振込先口座へ」というその行為において果たす役割に焦点が合わせられているというのです。こうしてロール間のインタラクションによってシステムとしてのふるまいが実現されることになるのですが、データは自分がどのようなロールを演じる可能性があるかを全て知っている必要はなく、ロールを演じる上で必要な「能力」を持ってさえいれば良いことになります。


これで、データとロールという形でシステムの構造とふるまいを表現する手段が手に入ったことになります。これによって異なる境界が表現できるようになった訳ですが、実行時にはこの2つはあるオブジェクトにおいて重ね合わせることができなければなりません。この、データとロールを紐づけるものが、「コンテキスト(文脈)」であるとされます。あるデータがどのロールを演じるかはコンテキストが決定する、ということですね。


こうしてDCIアーキテクチャの構成要素が出そろったことになります[Reenskaug 2008 p.5]。

  • DはDataを意味しています。データはシステムの構造あるいは状態を表現します。
  • CはContextを意味しています。コンテキストは必要なふるまいを実現するのに必要なオブジェクトのネットワークを構成します。
  • IはInteractionを意味しています。これはシステムのふるまいを表現するためのオブジェクト同士の相互作用です。この相互作用はロールメソッドとして実装されます。


次の課題は、このアーキテクチャの実現方法でしょう。データとロールはコンパイル時には分離されており、実行時に融合しなければなりません。これを実現するための手段としてCoplien氏が提示しているのは”Trait”という概念です。これはまさにScalaの言語仕様に与えられた名前そのものでもあるのですが、もちろんScalaでなければ実装できないということはなく、Groovyであれば、mixinを使って実現することができます。


それでは実際にGrailsを使ったアプリケーションにDCIアーキテクチャを組み込んでみましょう。

サンプル

今回使用するサンプルは給与計算システムです。仕様は以下の通りです。

  • 基本は時給制
  • 商品が売れれば、値段に応じてインセンティブが付く
  • インセンティブの割合は従業員ごとに定められたランクによって決まる
    • SENIOR → 3%
    • MEMBER → 2%
    • NOVICE → 1%
  • 給与計算は月単位で行う。

以下に示す通り、データ構造はかなりシンプルです。

ここに示した基本的なエンティティに対するCRUDは、Grailsが持つscaffoldの機能を使うことで苦労せずに作成できます。DCIアーキテクチャは、給与計算ロジックに登場します。時給とインセンティブを計算するには、以下のようなHQLを使って従業員ごとの労働時間合計および売り上げ合計を集計する必要があります。

SELECT 
    sum(salesReceipt.item.price), salesReceipt.employee.id 
FROM 
    SalesReceipt salesReceipt
WHERE 
    date >= :startOfMonth and :endOfMonth >= date
GROUP BY 
    salesReceipt.employee.id

通常であればこの結果として得られるリストをメソッド間で受け渡して処理をすることになると思いますが、今回はDCIアーキテクチャの思想に従い、このデータ構造に対して「○○合計取得(XxxTotalizer)」というロールを与えることにします。そうすることで、ふるまいは次のように表現できます。


各従業員ごとの給与計算ロジックを集約しているクラスを見ると、ふるまいが一目で分かる状態になっていることが見てとれると思います。

class PaymentCalculator {

    def calculate(workingHoursTotalizer, salesAmountTotalizer) {
        int totalWorkingHours = workingHoursTotalizer.workingHoursOf(id)
        int monthlyBase = hourlyRate * totalWorkingHours
        int totalSalesAmount = salesAmountTotalizer.salesAmountOf(id)
        int incentive = totalSalesAmount * incentivePercentage()
        monthlyBase + incentive
    }

    private def incentivePercentage() {
        switch(rank) {
            case Rank.SENIOR:
                return 0.03
            case Rank.MEMBER:
                return 0.02
            case Rank.NOVICE:
                return 0.01
        }
    }
}


使用したサンプルはこちらで公開しています。

最後に

mixinというやや特殊な言語仕様を必要とするため、DCIアーキテクチャは新しいギミックのように見えるかもしれませんが、実際にはより普遍的なモデリングパラダイムという文脈からとらえるべきです。すなわち、システムを「構造」と「ふるまい」の2つに分解して考える視点は、可変なものを安定したものと切り離すことによって全体の構造を安定させるというアーキテクチャの考え方に基づくものです。また、個別のオブジェクトのふるまいを見るのではなく、それが組み合わされた時のシステムとしてのふるまいに目を向けようという意図や、人間のメンタルモデルに関する深い洞察などがここには含まれているのです。


こうした思想を生み出した根底にある「メンタルモデルをどう捉えるべきか」という探求からは、学ぶべきものが多いと言えるのではないでしょうか。

参考文献一覧

補足

空のリストにmixinを行い、asTypeでmixinしたクラスにキャストすると、元のリストが持っているメソッドが使えなくなってしまうようです。サンプルではこの問題を回避するため、ロールメソッドの先頭にエラーハンドリング処理を入れています。

*1:このあたりについては、ドメイン駆動設計入門において詳しく説明しています