uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

DCIアーキテクチャについて語ってみるよ

Trygve Reenskaug氏とJames O. Coplien氏らが提唱する「DCIアーキテクチャ」について、id:digitalsoulさんが論文を翻訳してくださり、またその解説とサンプル実装(groovy, scala)を示してくださっており、読んでみたところ、大変興味深いので理解した限りを書いてみます。

おじさん登場

たとえば、あるおじさんがいたとします。

このおじさんは、白いスーツ、グラデーションの入ったサングラスと金ぴかのネックレスをつけて新宿歌舞伎町に出かけ「やくざ」として振るまいます。とおりかかったお兄さんがそのおじさんに出会い、目が合ってしまい、因縁を付けられ、お金を巻き上げられてしまいます。

さて、おじさんは家に帰ります。実は、このおじさんは家では良いお父さんとして振る舞います。赤ちゃんはこのおじさんの目を見て笑いかけます。おじさんは相好を崩し、オーよしよし。

さて、同じおじさんに対して、「目を見る」という動作にたいして、お兄さんと赤ちゃんで全く異なる結果を招いていますが、このような動作を従来のクラスベースのオブジェクト指向プログラミングでは自然に記述できません。

継承?

例えば以下の様に継承したとしても、

お父さんオブジェクトと、やくざオブジェクトは別につくれるでしょうが、あるおじさんが、ある時点ではお父さんで,ある時点ではやくざである、というように変化することができません。*1

インターフェース?

インターフェースを使ってみましょう。

この場合、ojisanクラスはヤクザのメソッドとお父さんのメソッドの両方を実装する必要があります。さらに、「目を合わせる」メソッドがどちらの動作をするかを切り替える事ができません。さらに、おじさんが将来「会社員」などの別の立場になったときおじさんクラスをその都度修正したり、追記していく必要があります。継承しているのでおじさんからやくざ・お父さんへの依存性が生じているのもたいへん嫌です。

実装の多重継承?

インターフェースの場合と同様に、やくざ・お父さんへの依存性があります。さらにもちろん、言語によっては実装の多重継承はサポートしていません。

コンポジション?

コンポジションではどうでしょうか?

そんなには悪くないですね。ただ、

  1. おじさんとやくざ/お父さんは異なるインスタンスとなる。
  2. おじさんのメソッドをやくざ/おとうさんは持っていない。明示的にたどるかおじさんへdelegateしてやる必要がある。
  3. 上にも関連するが「やくざ instanceof おじさん」ではないので扱いにくい。そうしたいならインターフェースをimplementsする必要がある。

という問題があります。まあマイナーな問題ではありますが、クラスと手間が増えます。UMLを書くのは面倒なんで、略。

ミックスイン!

インスタンスへのミックスインもしくはそれに相当する機能を言語が持っていれば、この状況はきれいに解決できます。
「おじさん」(データ)インスタンスに対して「やくざ」(ロール、役割り)をミックスインしてやるのです。
たとえば、Scalaのtraitsとwith、Groovyのインスタンスに対するmixin(.metaClass.mixin)、RubyincludeObject.html#extendなどを用いて実現できます。JavaだとQi4jとかでアノテーションとかを使うことになるようです。C++だとtemplateをつかうらしいです。

コンテキスト

ここまでは、1つのデザインパターン、もしくはとある言語機能の便利な使い方に過ぎないと言えましょう。あるいはすでにロールモデリングでつち使われてきたものみたいですね(ミックスインするかどうかは別として)。

さて、DCIアーキテクチャの最後のピースはコンテキストです。

お兄さんが町を歩いていて、おじさんに出会う、という「コンテキスト」において、おじさんに「やくざロール」がミックスインされるのです。

でも仮に、赤ちゃんが同じ新宿歌舞伎町でおじさんに出会ったら、そこでおじさんにミックスインされるのは、「お父さんロール」でしょう。

データにどんなロールがミックスインされるかは、コンテキストが定めるということです。「誰が(主体)」「何をしようとして(目的)」おじさんに出会うかによって、おじさんにそれぞれ適切なロールが注入され、その瞬間からおじさんはそのように振る舞い始めます。コンテキストはロールをデータに結びつけたうえで、相互作用を記述する場です。

利点

DCIアーキテクチャの利点を以下に示します。

メンタルモデルとの一致

DCIアーキテクチャではこのようにデータ、それに付与しうるロール、そしてその結びつける場の記述としてのコンテキストでシステムの振る舞いを記述します。

このような記述が重要なのは、人間が、世界を認識している、そのメンタルモデルと近いからです。
ありとあらゆる属性を含めて「おじさん」をモデリングなんかできっこないし、さらにそれが機能追加によって変化していくと、手に負えません。あるコンテキストである側面(ロール)に着目して記述するだけでよいのです。それが十分かどうかは、わかりませんが、人間ですら、現実世界において、そうせざるを得ないので、そうしているのです。

ぼくらは、とあるやくざとの示談金の交渉とか、事務所に連れ込まれたりした場合のやり取りを想起したり記述したりする際には、そのおじさんが「良いお父さん」であることに着目したりはしません。なぜならそれは想定/記述を簡潔に保つために、そして、よりよく理解するためには、不要で害があるからです。

主観的プログラミング

これは余談。誤解を恐れずに言うなら,特定の立ち位置(多くの場合ソフトウェアの利用者の視点)からの「主観」として現実を切り取る、という人なら誰もがやっている行為をプログラミングに持ち込むということです。考えてみると、主観無しで、客観的に世界を認識できると思うのは、人間としては傲慢な行為でありました。また、わかりやすさという観点から言うと、全てが客観的に書かれた小説や論文というものが有ったら、そんなものは実にわかりにくいでしょう。DCIアーキテクチャのコンテキストの概念は、プログラミング言語自然言語の記述運用法に近づけるものだ、ともいえるかもしれません。

アジャイル

このようなアーキテクチャが変化に強い事はそうなのかなと思います。データは基本的な属性とメソッドだけをもっていて(たとえば「おじさん」なら「年齢」と「名前」ぐらいを持っておく)。おじさんが銀行窓口に来たら「顧客」や「購入者」のロールが割り当てられるでしょう。

凝集性

伝統的なOOPでは、各オブジェクトにフラットに散らばっているしかなかったメソッドが、DCIアーキテクチャにおいては、データの保持のためなのか(モデルのメソッド)、特定コンテキストにおけるロールなのか(ロールのメソッド)、コンテキストにおける処理なのか(コンテキストのメソッド)、によって分別され、コードの配置が必然性を持って確定します。(多分)

まとめ

短い記事ではとても説明しきれる事ではございませんが、卑近な例を挙げて書いてみました。興味を持たれた方は前述の論文や、それに続く幾つかの記事サンプルコードを読んでみてください。

DCIアーキテクチャは、今後設計方法論やツールやフレームワークのサポートなどを含め、ソフトウェア開発における一角を占めていく考え方になるのではないかと思います。(ひかえめ)

なお、私はDCIアーキテクチャでちゃんとしたコードを書いた事が有るわけではないので、本記事は推測や予想が多分に含まれています。誤解や間違い、ウソなどございましたらご指摘いただけますと幸いです。

*1:図を書くのに使った[http://www.yuml.me/:title=yUMLサービス]のバグか仕様かわかりませんが「おじさん」だと意図する図にならなかったのでojisanと表記しいてます。

scalaはGroovyと全く競合しない

このところ思ってたのですが、ScalaとGroovyは競合しませんね。まったくしない。

適用領域に関して、GroovyのスイートスポットとScalaのスイートスポットに共通部分は、ほぼ全く全然無いと思います。

Scalaは、JVM上で動作しはしますが、Javaと完全なる別言語で、Java資産はライブラリとしてのみ使います。Scalaでのシステム開発では、Javaによる新規開発は無ければ無いほど良い開発となります。この点では、Scala者からのJavaの見方は、JRuby者から見たJavaと似ている。

Groovyは、これとは全く異なり、Groovy開発でJavaを除去はできません。GroovyはJavaの周辺ツール・ライブラリの一つに近い。Groovyが一番近いのは、あれですよ、JSPですよ。JSPJavaの関係に近い。組み合わせて使うということです。

プログラミングにおける意識についても大きく違います。Groovyプログラミングは、常にJava API層の上で行います。その差異と使い分け、ラッピングを意識します。Scalaではそれはない。Scalaの単一プログラミングモデルだけを意識すれば良い(本当か?)。

つうことで、結論は凡庸ですが、使い分けが大事ということです。今後の開発の世界はいずれにせよマルチ言語になるとして、手駒言語を何個まで増やせばいいかとか、教育方法とか、開発要員確保とかの現実世界の様々な側面でそれぞれの言語のトレードオフを評価しつつ、しなやかに選択していけば良いことでしょう。

GroovyでMaybe Monadを書いてみた。

モナドなんて難解なものを含んだ言語は業務で使えねー!!」とScalaの悪口を言おうとして調べてたらモナドが何となく分かった気がしたのでMaybe Monadをgroovyで書いてみました。面白いわこれはw。

ただ、HaskellScalaもちょっと読んだぐらいの経験しかないし、「JavascriptでMaybe」とか「RubyでMaybe」とかPerlでとかも参考にしてませんので、間違ってる可能性も大*1

例としては「モナドの全て」のクローン羊の話を実行しています。

なお、Maybeモナドモナドの中でも簡単なものだそうなので、わかったというのはおこがましい,ってのはその通りです。

さらに、Monadは、純関数型言語で使って最も効果を発揮するものだし、ライブラリや言語仕様やイディオムを含む、総体的な「モナディックプログラミング文化」としてでも多分捉えるべきなので、1個Maybeを定義したからと言って、利点や意義がピンとくる、というものではないですね。以下の例ならセーフナビゲーション(?.)で書くのと比べて何がうれしいの、とかいわれても、Groovyの範囲内で比較してもしょうがないというしかない。でも前に作ったGroovyの遅延評価と組み合わせたり、MonadPlusを定義したらもうちょっとおもしろいかも。

今回関数型言語のパワーの片鱗を見た気がしました。結局Scalaの悪口はやめておきますが、改めて思ったことはあるのでまた別途。

以下、とても参考になったページ:

class Monad<T> {
  T value;
  Monad() {}
  Monad(T a) {
    value = a
  }
  Monad<T> rightShiftUnsigned(Closure c) { // Haskell's >>=
    return c(this.value)
  }
  static Monad<T> Return(x) { new Monad<T>(x) }
}

abstract class Maybe<T> extends Monad<T> {
  abstract boolean isNothing();
  Maybe() {}
  Maybe(T a) {
    super(a)
  }
}

class Just<T> extends Maybe<T> {
  Just(T a) {
    super(a)
  }
  boolean isNothing() { false }
}

class Nothing<T> extends Maybe<T> {
  Nothing() {}
  boolean isNothing() { true }
}

class Sheep {
  Maybe<Sheep> mom
  Maybe<Sheep> dad
  String name
}

def Nothing = new Nothing<Sheep>();

father = { Sheep s ->
  if (s.dad.isNothing()) return Nothing;
  return new Just<Sheep>(s.dad.value);
}

mother = { Sheep s ->
  if (s.mom.isNothing()) return Nothing;
  return new Just<Sheep>(s.mom.value);
}

maternalGrandfather = { Sheep s ->
  if (mother(s).isNothing()) {
    return Nothing
  }
  Maybe<Sheep> m = mother(s)
  if (m.isNothing()) {
    return Nothing
  }
  return father(m.value)
}

mothersPaternalGrandfather = { Sheep s ->
  if (mother(s).isNothing()) {
    return Nothing
  }
  Maybe<Sheep> m = mother(s)
  if (m.isNothing()) {
    return Nothing
  }
  Maybe<Sheep> f = father(m.value)
  if (f.isNothing()) {
    return Nothing
  }
  return father(f.value)
}

Sheep s0 = new Sheep(name:'s0', dad:Nothing, mom:Nothing)
Sheep s1 = new Sheep(name:'s1', dad:new Just<Sheep>(s0), mom:Nothing)
Sheep s2 = new Sheep(name:'s2', dad:new Just<Sheep>(s1), mom:Nothing)
Sheep s3 = new Sheep(name:'s3', dad:Nothing, mom:new Just<Sheep>(s2))

assert s0.name == 's0'
assert s0.dad.isNothing()
assert s0.mom.isNothing()
assert s1.name == 's1'
assert s1.dad.value == s0


assert s1.mom.isNothing()
assert s2.name == 's2'
assert s2.dad.value == s1
assert s2.mom.isNothing()
assert s3.name == 's3'
assert s3.dad.isNothing()
assert s3.mom.value == s2

assert mother(s0).isNothing()
assert father(s0).isNothing()
assert mother(s1).isNothing()
assert father(s1).value.name == 's0'
assert father(s2).value.name == 's1'
assert mother(s2).isNothing()
assert maternalGrandfather(s2).isNothing()
assert maternalGrandfather(s3).value.name == 's1'
assert mothersPaternalGrandfather(s3).value.name == 's0'

assert (Monad.Return(s3) >>> mother).value.name == 's2'
assert (Monad.Return(s3) >>> father).isNothing()
assert (Monad.Return(s3) >>> mother >>> father).value.name == 's1'
assert (Monad.Return(s3) >>> mother >>> father >>> father).value.name == 's0'

*1:vlaueはMaybeの方に持つべきなんですかね。bind(Haskellの>>=,以下では>>>)もあえてMonadの方のデフォルト実装として書いてます。