鳩舎

レースしない

『DCI なんて面倒なだけで Service 使えばいい』への返答

NOTE: 最下部に追記があります。

よく言われる話として、 DCI なんて実装が面倒な上に夢の実装の話をしており、現実解としては Service クラスを用いて実装すればシンプルな実装になるのだから、そういったものは必要ないのだ、というご意見への返答です。

こういった批判の文脈の際、 Service クラスというのがどこの Service クラスを指しているのか、が問題なのですが、 DDD における Service ではないように思えるので、おそらく PofEAA などで語られる Service Layer などを指していると思われます(違うならそう言ってください)。

PofEAA における Service Layer(以後、 Service と呼ぶものはこの PofEAA における Service です)はドメインオブジェクトからアプリケーションロジックを切り離すことを主目的としています。これはコードサンプルを見る限り DCI における Context と非常に類似しており、 Context の説明をする際に『いわゆる Service みたいなものだよ』と説明することも多くあるかと思います。これは間違いであり、 Service と Context はスタート地点からして違うものなので、混同してはいけません。

ロミオとジュリエット

さて、今回の例題は分かった気になる DCI 、ロミオとジュリエット編を元に話をしていきます。タイトルが長すぎるのでここではロミジュリというアプリケーションとして話ましょう。

ロミジュリにおけるドメインオブジェクトは言わずもがな Actor です。役者というデータモデルであり、本来であれば Actor には年齢、性別などのデータが付与されているものかと思いますが、今回は割愛します。とにかく役者のモデルがあり、役者は喋らなくてはならないので、 #say というメソッドを持っています。

ロミジュリアプリケーションでは、ロミオとジュリエットの1シーンを再現するのがアプリケーションの仕事ですので、ロミオ役の Actor とジュリエット役の Actor が必要になります。

ここで問題になるのは、『なぜ Romeo モデルと Juliette モデルになっていないのか』です。ロミジュリアプリケーションにおける主目的はなんなのか、という根底の部分が問題になってきます。

僕がロミジュリサンプルを書いた時、これらのコードはあるアプリケーションの一部として書かれたものである、という想定で書き起こされました。それは、舞台再生アプリケーションです。舞台再生アプリケーションはシナリオをコードとして持ち、特定のシーンを役者を入れ替えて再生することが出来るというアプリケーションで、いわゆるノベルゲームの画面で、キャラを入れ替えながら特定のシーンを再生できる、といったようなアプリケーションを想定していました。

なので、役である Romeo 及び Juliette はモデルではなくあくまで振る舞いです。Romeo も Juliette もデータを持たず、アプリケーション上の都合によりどのような台詞を喋るかによって区別されます。Romeo も Juliette も、データを持ったモデルというよりは特定の台詞の集合であると捉えるべきです。

Service によるロミジュリの実装

では、実際に Service でロミジュリアプリケーションを実装するとなると、どのような実装になるのでしょうか。シナリオデータは外部からの入力などではなく、アプリケーション上にベタ書きで展開されていますので、ロミオとジュリエットが喋る台詞の内容というのはアプリケーションレイヤの話になります。

ドメインオブジェクトとしては Actor は今のところ過不足なく機能を持っているので、アプリケーションレイヤの振る舞いを抽出して Service 化するとなると、以下のようなコードになるでしょうか。

class Scene03Service
  def initialize(romeo, juliette)
    @romeo = romeo
    @juliette = juliette
  end

  def execute
    # ジュリエットによる問いかけ
    @juliette.say <<-EOS
O Romeo, Romeo! wherefore art thou Romeo?
Deny thy father and refuse thy name;
Or, if thou wilt not, be but sworn my love,
And I'll no longer be a Capulet.
    EOS
    # ロミオの当惑
    @romeo.say "Shall I hear more, or shall I speak at this?"
    # ジュリエットの懇願
    @juliette.say <<-EOS
Tis but thy name that is my enemy;
Thou art thyself, though not a Montague.
What's Montague? it is nor hand, nor foot,
Nor arm, nor face, nor any other part
Belonging to a man. O, be some other name!
What's in a name? that which we call a rose
By any other name would smell as sweet.
    EOS
  end
end

喋る内容についてはアプリケーション側の都合ですので、ドメインオブジェクトにはなりえません。ですので、愚直に実装するとこのようなコードになるかと思われます(もし、この認識が間違っている場合はご指摘いただけるとありがたいです)。

今はあえて読みにくくなるような例題を出していますので、かなり恣意的な結果になっているとは思いますが、 Service による実装はアプリケーションコードを手続き型的なコードに変容させていくのではないか、という危惧だけ伝われば幸いです。手続き型的であれ、アプリケーションが動けばよいのである、というお話でしたら、設計手法など議論する余地がないので、ここでおしまいです。

この例では、かけあいとはいえほぼお互いに言いっぱなしの状況ですから、オブジェクト間で何らかのやりとりがある、といった状況にはなっていませんが、 Actor に例えば #left_hand などの Attribute があり、手に持ったオブジェクトをやりとりする、などのシーンが実装されたとしたら、その時はより手続き型らしいコードになるかと思われます。

# ジュリエットが手に持った毒をロミオに渡す
@romeo.left_hand = @juliette.left_hand
@juliette.left_hand = nil

コード上では正しく目的どおりにジュリエットの手元の毒がロミオの手元にわたっているのですが、これは劇中進行を一切反映していません。『ジュリエットが毒をロミオに渡す』と言っているのですから、主語はジュリエットであるべきなのに1行目の主語はロミオですし、コメントがなければアプリケーションとしてこの参照の移動がどういった意味あいを持っているのか理解するのは困難でしょう。

もちろん、Service が悪い訳ではなく、今回の例は恣意的に Service で表現しにくいコードを選んで、かつ手抜きに表現しているのでわかりにくくなっています。PofEAA に造詣が深いわけでもないので、僕が勘違いしているかもしれませんがこういう時は ItemPassingService でも作って、渡す側と受け取る側に分かれてオブジェクトをやりとりするような処理が入ることになるんでしょうか。

DCI による毒の受け渡し

一方 DCI において毒を受け渡すようなシーンが必要な場合は、おそらく以下のような書き方になります。

module Juliette
  def pass(target)
    target.left_hand = self.left_hand
    self.left_hand = nil
  end
end

class PassingPoisonContext
  def execute
    @juliette.pass(@romeo)
  end
end

先ほどの例にあった処理が Juliette Role のメソッドになり、Context ではそのメソッドを呼び出すだけになりました。コード量としては増えたのですが、アプリケーションに期待するメンタルモデルがコード中に投影される量が増えたことはご理解いただけるでしょうか。

DCI は別に Role を extend して unextend する、というような部分が本質ではありません。開発として楽になるから DCI を採用する、というのも間違っているのではないかと思っています。

DCI によってコーディングが簡略化されるかというと、それはおそらくあまりないでしょうし、コードの見通し、という点にしても実際に僕自身 DCI で業務で携わる規模のアプリケーションを実装したことがありませんからわかりませんが、おそらく増えるロールとコンテキストによって、1つのコードファイルを見て把握出来ることは減るのではないかと思っています。また、現実的な利用を考えた時、一時的な振る舞いをオブジェクトに注入するのはどうするのか、という部分についてまず検討する必要があり、現在のプログラミング環境において、採用しえないパラダイムだろうなという気はしています。

それでも僕は、コード中に投影されるメンタルモデルが多くなればなるほどコードの保守性は上がっていくはずであると信じていますし、最悪、コードだけでメンタルモデルが理解出来るような日が来るのであれば、それはシナリオを書けばアプリケーションが出来上がっていくのと同義であるのではないかとすら思っています。

メンタルモデルを増やす

人はアプリケーションに何かを期待して操作を入力します。ATM であれば『自分の口座からお金が手元に出てくる』というメンタルモデルですが、実際のアプリケーション内での実装では、『操作者の口座の現金残高が入力値分減少する』『ATM 金庫内の現金を入力値分送出する』という実装がなされており、コード上の表現とユーザーのメンタルモデルは一致しません。

メンタルモデルにコードを近づけていくという作業は、自身のアプリケーションへの理解を向上させるとともに、ドメインエキスパートから得た知識をよりコード上に投影しやすくなることだと思っており、これこそが DCI の利点であり採用すべき理由だと思っています。

ということで

返答は以上です。

DCI 派からも、 Service 派からもツッコミがあると思うので、突っ込まれ次第修正していく所存です。

よりよい Service の例

monzou さんにより現実的な Service のコードの例が来た。帰省中にありがとうございます。

「『DCI なんて面倒なだけで Service 使えばいい』への返答」を読んだ感想とポエム

まずはじめに言っておくと、僕は Service より DCI の方が優れてるとは思っておらず、単に僕は DCI が好きです、ぐらいでしかないです。ただ、あまりにも DCI に対して Service でいいし DCI は要らない、と言われることが(個人的経験上)多かったのでいやもう Service の話はいいんですよ。という感じで書き始めたのでした。そういうエントリで Service をより良く知ることになるというのは面白い感じです。

さて、本題の monzou さんにもらった例を見ると、Scene がドメインオブジェクトになっており、僕のコード例は Service のコード例として実に不適切だったことがわかります。

ドメインオブジェクトとして Scene03 を実装して、 Service からはその Scene オブジェクトを実行するだけ、という形になっており、かつ、メンタルモデルの投影についても僕の書いた DCI の例と一切遜色ありません。

ていうか大体やってること一緒なんじゃないの?みたいな気持ちになりますが、そりゃそうだ、という感じです。この時の Scene の実装の違いは、 Delegation によって実装されているか、それとも extend による拡張かの違いぐらいです。

Jim Copelien 氏によれば、DCI において Role を Delegation によって実現すべきでないのだそうです。理由としては、 Context の中に入ったとしてもとある Data は変わったわけではないので、同一のオブジェクトであるべきであり、何かで Wrap された別のオブジェクトであるべきではない、という思想からくる理由だそうです。

逆を言えばその程度しか違いがないので、『DCI でも Service でも、どっちでもいいんじゃない?』となると返答は『はい』になります。とはいえ、上記のブログ記事だけだと僕も Service より DCI の方がいい!と言っているようにしか読めないのですが……単に好きなものへのバイアスがかかっているだけです。 Service 使えばいいといった人たちも Service が好きなだけで DCI はまったく意味がないと思っている人ばっかりじゃないのかもしれません。

また、手続き的になるのはトランザクションスクリプトだよ、というのもまったくその通りでした。完全に勘違いですね。

そういう意味で言うと、変な話ですが DCI における Context はトランザクションスクリプト的ではあります。ユーザーのユースケースのストーリーを順に書いていくことになるので、大きな Context になればそれだけ長いストーリーが並ぶことになります。ロミジュリの例で言えば、1シーンから1幕になったらどれだけの量の台詞の羅列が並ぶんだ……という感じですね。そういう時は Context そのものを分割して子 Context を作っていくことになると思いますが、ぱっと見のコードで手続き的に映るのは DCI なんじゃないかな、という気でいます。ユーザーの想像するメンタルモデルを羅列していくことになるので、まぁそうなるよな、という感じですが。

とまぁ話もそれてきたので戻しておくと、よりよい Service の実装を読む限り、DCI で実現しているそれは Service の層ではなくドメインオブジェクトの層なのでは?(いや、実際そうかというと多分ちょっと違うのですが)みたいな気になってきたので、Service 全然悪くない、むしろ僕はなんで DCI と比較して Service を勧められていたのだろうか……というのが今のところの心中です。

まだ俺に喋らせろ!とか『いやこういうのがあってだな』などの場合はご連絡ください。monzou さん、本当にありがとうございます。