2017年3月9日木曜日

ドメイン駆動設計について DroidKaigi 2017 で登壇しました。

スライドだけ公開しても伝わらないと思うので、スピーチ原稿と合わせて公開します。
すごく長いです。


本日はドメイン駆動設計というものについて、Androidアプリ開発と絡めてお話させていただきます。
私あんざいゆきと言います。Androidアプリの開発をなりわいとしております。
長らく Y.A.Mの雑記帳というブログでAndroidの技術情報を発信しています。最近はなかなか投稿できなくなってしまいましたが、それも仕事としてAndroidに関われているためです。Androidを触り始めたころはまだ学生だったので時間があったんでしょうね。
はじめて Android に関するエントリを投稿したのは 2009 年 5 月 24 日です。当時はJavaFXを触っていたので、NetbeansでAndroidをやろうとしていたようです。
次の日にはEclipseに乗り換えるんですけどね。やっぱりAndroidアプリを開発するならEclipseですよねー。
当時のAndroidのバージョンは1.5、Fragment もなく、Support Library もなく、マルチタッチすらなく、ストアは Google Play ではなく Android Market という名前でした。
ここから2、3年くらいは、仕事でAndroid アプリを開発している人はもっぱらメーカーのプリインアプリを作っている方たちで、多くの人は趣味でAndroidアプリを開発して、マーケットに公開していました。
彼ら、彼女らが作るアプリはほとんどが無料で、ユーザーは開発者自身、自分が欲しいもの、自分にとって便利なものをみな作っていました。
こういったものは大抵がシンプルな単機能のもので、継続的にメンテナンスすることを考えずに作られています。
当時趣味でアプリを作っていた人で、半年後や1年後に自分のコードをみて、何をしているのかわからない、という状況になった方はいらっしゃるのではないでしょうか。私もその一人です。
さらに趣味アプリの場合、コードをいじるのは自分だけです。今ほど github を使っている人もいませんでしたし、趣味アプリの場合、OSS として github 上に公開してもコミットするのは自分だけという状況は多いのではないでしょうか。
このように規模が小さくシンプルなアプリで、作って公開したら終わり、しかも作り手は自分だけ、という状況では設計手法なんて考えなくてよかったのです。設計にそれほど注意を払わなくても作成できますから。
さて、その後 Android は破竹の勢いで世界を席巻しました。スマートフォンも普及し、今ではあるのが当たり前、対応するのが当たり前のプラットフォームです。
スマートフォンを対象に新たにサービスを立ち上げたり、すでに展開しているビジネスのクライアントとしてアプリを開発するというのは、趣味アプリとは前提が異なります。
まず、サービスとしてさまざまな機能を提供するため、アプリが複雑になります。画面数も多く、さまざまなことに注意を払う必要があります。
次に作って終わりではありません。継続的にメンテナンスし、ビジネス要件の変化に対応し、機能の追加や変更が必要です。ランタイムパーミッションに対応するなど、プラットフォームの進化にも追随しなくてはいけません。
ライバルのアプリに新しい機能が追加されたら、うちのアプリにも入れろと言われ、あれやこれやの施策をやりたいと言われ、しかも早くやれと言われる。開発するほうは大変です。
さらに、作り手は自分だけではありません。複数の開発者がアプリのさまざまな部分に手を入れます。人が入れ替わることもあるでしょう。
他の人が書いたところをすべて読まなくては機能変更ができない、という状況では困ります。
我々のアプリは危機に瀕している。
機能を満たすだけのコードが無秩序に積み上がった状態はまるでハウルの動く城。
あやまってネジを抜いてしまったら、全てが瓦解しかねない。
デグレの嵐、燃えるユーザーレビュー。
おそろしい。
よくわからないコードをいじる時はおそるおそる。すごく時間がかかる...
このままではいけない。

どうすれば、複数人で複雑なアプリを継続的に、安定して素早く開発していけるのか。

なんとかアーキテクチャや、なんたらパターンがあふれているけれど、どれもしっくりこない。 うまくいっているという話もあまり聞こえてこない。 もし技術的なパターンを適用するだけでうまくいくなら、今頃みんなそれをやっているはず。でも、そんなふうにはなっていない。 表面的な技術的パターンではなく、もっと本質的なことを考えるべきではないのか。

そのヒントを探してドメイン駆動設計の本を手に取りました。
最初に読んだのが「エリック・エヴァンスのドメイン駆動設計」です。
ドメイン駆動設計の本といえばこれですね。
すごく分厚いです。3cmあります。
一人で読み切るのは大変だと思います。私は友人と読書会をすることで通読できました。

チームでDDDに取り組もうとするときには、少なくとも一人はこの本を読み切った人がいるべきだと思っています。 実は、この本は読みながら出てきた内容を順番にチームで取り組んでいけば良い、という構成ではありません。最後の方、第4節、14章に大事なことが書いてあるんです。
次に読んだのが「実践ドメイン駆動設計」です。
こちらのほうが説明の日本語がわかりやすいかと思います。

この本は実践とついているだけあって、チームでDDDに取り組むにはどうやっていけばよいかという視点から書かれています。

先ほど言及したエリック・エヴァンスの本の14章で扱っている内容は、2章、3章で取り上げられています。

「実践ドメイン駆動設計」のほうが、チームでDDDを学びながら順次取り組んでいきたい、という要求に的しています。
ただし、この本ではエリック・エヴァンスの本を読んでいる前提で詳細な解説がされていないところもありますから、結局は2冊とも読むべきだと思います。

「ドメイン駆動設計をどうAndroidアプリ開発に取り入れるか」が、このセッションの主題ですが、その話をするにはまずドメイン駆動設計とはなんなのかの話をしなければいけません。
ドメイン駆動設計、Domain Driven Design、という言葉を聞いて最初に思ったことは「ドメイン」とはいったいなんだろう?でした。 インターネット上の住所などと言われるドメイン名のドメインのことではありません。

本には次のように書かれています。
私たちが作るアプリにもユーザーがそれを適用する対象領域があります。
例えば料理のレシピを検索するアプリはどうでしょうか。
ユーザーはこのアプリをどのような領域に適用するのか。つまりこのアプリで何をするのか。アパートを検索するわけではありませんね。料理のレシピを検索します。つまりこのアプリのドメインは料理のレシピ、または料理そのものと言えます。
アパートを検索するなら、別のアプリを使いますね。ユーザーは建物が欲しいわけではなく、住むところを探したいわけですから、このようなアプリのドメインはアパートではなく、住まいでしょう。住むということとも言えそうです。
UberやLyftはどうでしょうか。適用する対象領域は「移動」と言えるでしょう。海を渡るためには使いませんからドメインは「近距離移動」でしょうか。

自分のアプリやサービスのドメインについて考えてみてください。

ドメインについて考えると、そこには実際に存在するものから概念的なものまでさまざまなものが含まれます。例えば、食材や調理器具や間取りや車など。
githubのようなソースコードを管理するサービスのドメインにはソフトウェアという概念が含まれるでしょう。

ドメイン駆動設計でのドメインとは何かがわかってきました。
すると次の疑問は「ドメイン駆動で設計するとはどういうことか?何をするのか?」です。
まずドメインがあります。
次にドメインエキスパートという人が登場します。
また知らない単語が出てきました。ドメインエキスパートってなんでしょう?
端的にいうとドメインに詳しい人です。ドメインが何かは先ほど話しましたね。アプリによってドメインは異なりますから、アプリによってドメインエキスパートも異なります。
エキスパートとついているので専門家じゃないといけないと思うかもしれませんが、そうではありません。ドメインについて自分より詳しい人はみなドメインエキスパートです。
ユーザーだったり、同僚だったり、さまざまです。
ドメインエキスパートが何かわかったので、ドメイン駆動設計で何をするのかに戻りましょう。
ドメインエキスパートの頭の中には、ドメインを構成する物や振る舞い、関係性などの概念的な何かがあります。

例えば料理をするとはどういうことですかと聞くと、「材料を用意して、それぞれの分量を計り、手順に沿って調理していく」ことです。のような答えが出てくるでしょう。

なにやら「材料」というものがあって、それが複数必要なようだ。材料には「分量」というものがあって、「分量」というのは測るものらしい。さらに「手順」というものがあって「調理」というものをするらしい、と。
頭の中は直接見えませんから、ドメインエキスパートに質問したり話を聞いたり互いに協力して、ドメインエキスパートの頭の中にある概念的な何かをうまく取り出し、解釈し、蒸留し、我々のソフトウェアに役立つモデルとして作り上げます。
そうして作り上げたものがドメインを反映したモデル、ドメインモデルと呼んでいるものです。

ドメインモデルは図を書いて説明したり、文章で説明したりできますが、図それ自体がドメインモデルである、というわけではありません。 あくまで頭の中の共通概念としてモデル化したものです。
ドメインエキスパートと話をしていると、ドメインを構成する言葉が見つかります。
先ほどの例だと「材料」や「分量」や「調理」という単語や「材料を用意する」「分量を計る」などのフレーズです。ドメイン駆動設計ではこれらドメインを構成する言葉をユビキタス言語といいます。
ユビキタスは、日本語で「どこにでも存在する」というような意味ですが、ドメイン駆動設計ではユビキタス言語として見つけ出した単語やフレーズをあらゆる場所で使います。ドキュメントはもちろん、会議での会話、ドメインモデル、そしてそれを実装するコードにも使います。

ドメインエキスパートを含めたチーム全員が同じユビキタス言語を使います。

そのため、何をユビキタス言語とするかはドメインエキスパートを含めたチーム全員で議論して合意の上で決めます。
ユビキタス言語はドメインエキスパートと開発者、そしてチームの共通言語です。単なる用語集ではありません。言葉は進化し、育っていくものです。


「開発者とドメインエキスパートが協力して、ドメインを構成するユビキタス言語を確立し、それを使ってドメインを反映したモデルを作り上げる」というところまできました。
ドメインモデルを作ったら、それを正確にコードで表現するように実装します。

ドメイン駆動設計の利点としてよく、コードが設計であり、設計がコードであるという点が挙げられますが、コードがドメインモデルを正確に表現していれば、それはすなわち設計である、ということです。
共通の概念モデルとしてドメインモデルを作り、それをコードで正確に表現するよう実装するので、他の人にとっても誤解しにくいコードになります。

じゃあ最初にドメインモデルを全部作り上げ、それから一気に実装すればいいのかというと、そうではありません。
最初から完璧なドメインモデルを作ることは不可能です。実装してみると、このモデルだとうまく実装できない、ということが判明したり、実装の途中でもっとよいモデルを思いつくことはよくあります。


そのためドメイン駆動設計では、ドメインモデルを作り、実装してみて、その結果をフィードバックし、ドメインモデルを修正したり変更したり、まったく新しいモデルを作ったり、そしてそれをまた実装する。これを繰り返してドメインモデルとコードの両方を洗練させていきます。
つまり、アジャイル的なプロセスを前提としています。
まとめると、

まず、ドメインエキスパートの言葉を観察し、ドメインを構成するユビキタス言語を見つけます。
次にユビキタス言語を使ってドメインを適切に反映した、我々のソフトウェアに役立つドメインモデルを作ります。
そして、作ったドメインモデルを正確に表現するようコードを実装し、これを繰り返します。

ドメインモデルを作ってから実装です。いきなり実装ではありません。
やることはわかった。でも実際やるのは難しい。

そこで、ドメイン駆動設計では、実践するために役立つさまざまな手法が出てきます。これらは主に2つに分けることができます。
戦略的設計と戦術的設計です。
ドメインモデルを作り上げるために役立つ手法が戦略的設計
ドメインモデルからそれを表現した実装を行っていくのに役立つ手法が戦術的設計です。
今までの話にでてきたユビキタス言語は戦略的設計です。他にも境界づけられたコンテキストやコンテキストマップがあります。

一方の戦術的設計には値オブジェクトやエンティティ、サービスなどがあります。名前を聞いたことがあるという方もいるでしょう。他にも集約やドメインイベントやリポジトリなどもあります。

このようなコーディングにおける技術的な手法は理解しやすいため、ここだけを取り入れてみました、という話がよくありますが、これは完全なDDDではありません。
なぜか。ドメインモデルがないからです。
さて、ドメイン駆動設計が何であるかの話をしてきました。

ここでちょっとドメイン駆動設計が何でないかの話をしたいと思います。
「DDDってClean Architecture のことでしょ?」

違います。Clean Architecture を知らないという人は気にしなくてよいです。
もちろんDDDとClean Architectureを組み合わせて使うということは可能ですし、Clean Architecture のコンセプトはDDDの影響を受けている、参考にしているということはあるでしょう。しかし、ここで言いたいのは DDD = Clearn Architecture ではない、ということです。
「DDDって MVC とか MVP とかの仲間でしょ?」

これも違います。今までの話の中でビューはでてきましたか?出てきてませんね。我々が話してきたのはドメインモデルについてだけです。
「レイヤ化アーキテクチャにすればDDDだよね?」

エリック・エヴァンスの本でレイヤ化アーキテクチャが紹介されているからか、DDD=レイヤ化アーキテクチャにすること、のように勘違いしているのを見かけることがあります。ドメイン駆動設計とアーキテクチャの関係はこのあと取り上げますが、ドメイン駆動設計は特定のアーキテクチャに依存しているものではありません。
「ドメインモデルは作ってないけど技術的なパターンを真似したからDDDだよね?」

ドメイン駆動設計ではドメインモデルを作ってそれを正確に表現するようにコードを実装することであって、特定の技術的パターンをとることではありません。
「ドメインモデルを作ってそれを正確に表現するように実装したからDDDだよね?」

これが正解です。

簡単に言うけど、やるのは難しいんだよね。
そろそろAndroidの話をしましょうか。

Androidアプリ開発でドメイン駆動設計に取り組む場合も同じです。ドメインエキスパートの話を聞いて、概念を適切に反映するドメインモデルを作って、それを正確に表現するよう実装する、これを繰り返します。

そうは言われてもどこから手をつけたらいいかわからない。
アプリにはいろいろ機能があるし、どれをやるべきなの?
どこから手をつけるべきか、それを知るにはアプリの全体像、地勢を把握する必要があります。
そのためにドメイン駆動設計で登場するのが境界づけられたコンテキストとコンテキストマップです。

また知らない単語がでてきました。
境界づけられたコンテキストとコンテキストマップ。

先ほど戦略的設計で名前が出てきました。この2つもドメイン駆動設計の重要な要素です。
境界づけられたコンテキスト。

境界はわかります。コンテキストとは何でしょうか。日本語ではよく文脈などと訳されますね。Androidでよく出てくるあのコンテキストではありません。

ユビキタス言語の言葉が特定の意味を持つ領域がコンテキストです。
例えばAccountという言葉があります。
この言葉がユーザー認証の文脈で語られていた場合、その意味はサービスを利用する際の利用単位のことだとわかります。
一方、銀行の文脈で語られていた場合、その意味は口座になります。
さらに、文学のもとではAccountの意味は報告書になります。
コンテキストが異なると、同じ言葉でも意味が変わります。
ユビキタス言語を構成する言葉が特定の意味を持つ領域がコンテキストであり、
境界づけられたコンテキストの内部では、ユビキタス言語を構成する言葉は特定の意味を持ちます。
ドメインモデルはユビキタス言語で構成されますから、ドメインモデルはそれを構成するユビキタス言語の境界づけられたコンテキストに属します。
では、自分のアプリの境界づけられたコンテキストをどう見つければいいのでしょうか
言語の境界がコンテキストの境界ですから、言葉の境界を探せばよさそうです。
どういうところが言葉の境界になるのでしょうか。
例えばチームが異なると、単一のユビキタス言語を維持するのは難しいでしょう。コミュニケーションにコストがかかることから、チーム内だけの言葉が発展していき、お互いの言葉が徐々に解離していきます。
いずれ、ドメインモデルを境界内で厳密に一貫性のあるものに保つことができなくなります。
特定の機能が外部のライブラリやSDKとして提供されている場合、その部分は別のコンテキストになっていることが多いです。ドメインモデルが一貫性をもつ範囲を考えると理解しやすいでしょう。
同じようにコードベースが異なる場合もヒントになります。わかりやすくいうとgithubのリポジトリが異なるのなら別のコンテキストではないか、ということです。
見つかったコンテキストには名前をつけます。
そして、その名前をユビキタス言語の一部にします。
境界づけられたコンテキストがわかってきたら、次にコンテキストマップを描きます。
コンテキストマップは、現時点の境界づけられたコンテキストと、それらがどのようにやりとりしているのかを示すものです。
大事なのは理想の姿ではなく現状の状態を描くことです。

だからといって正確に把握したものでなければならないというわけでもありません。最初はわかっている範囲で十分です。状態が変わったり、新たな知見が見つかったらその都度マップを更新しましょう。

凝ったつくりにする必要もありません。ホワイトボードに書いたものを写真で撮れば十分です。

依存する他のプロジェクトに何があって、それとどのような関係なのかをチームで共有し、考えるきっかけにします。
作ったマップはいつでも見れるところに置いておく。Activityのライフサイクルポスターの横に貼り出すなんて最高ですね。

境界づけられたコンテキストの間の関係がどのようになるのか、この関係性についてDDDには組織的なパターンや統合のパターンがいくつか紹介されています。
自分たちのコンテキスト間の関係性がこれらのパターンのどれに一番近いか考えてみましょう。
そしてコンテキストマップのコンテキスト同士をつなぐ線にどのパターンなのかを書いてみましょう。

パターンに名前がついていることは、とても重要です。後からチームに参加したひとでもコンテキストマップをみることで、どの依存プロジェクトが協力的で、どこが融通がきかないのか把握することができます。

いくつかのパターンをAndroidアプリ開発でありそうな状況にあてはめてみます。
パートナーシップというのは、成功・失敗の運命を共にする関係です。例えばアプリの主要な機能がすべてそのアプリ用のSDKとして提供されている場合、これらの運命は一蓮托生であり、パートナーシップの関係が一番近いでしょう。
特定の機能を社内SDKとして提供している場合はどうでしょうか。同じ社内ですから、アプリ側のニーズに対応してくれるかもしれない。もし対応してくれるような関係性であるなら、それは顧客/供給者の関係が一番近いでしょう。
3rd party が提供しているSDKではどうでしょうか。例えば twitter や facebook SDK など。この場合我々は提供されているSDKをそのまま利用するしかありません。このような関係は順応者になります。 社内SDKであっても、アプリ側のニーズに対応してくれない関係性なら順応者になります。
公開ホストサービスについてドメイン駆動設計の本には次のように書かれています「サブシステムにアクセスできるようにするプロトコルを、サービスの集合として定義すること。そのプロトコルを公開し、サブシステムと統合する必要のある人が全員使用できるようにすること。」

よくわからないですね。ようはこういうことです。

あるサブシステムとやりとりしたい人が複数います。
それぞれに個別に対応するのは大変なので外部に方法を公開するよということです。方法はRESTかもしれないしRPCかもしれない。
公開ホストサービスと一緒に使われることが多いのが公表された言語です。
わかりやすく言うと、XMLとかJSONとかProtocol Buffer とか、ようは形式が公表されている言語です。
さらに一緒に取り入れることが多いのが腐敗防止層です。
別のコンテキストのモデルによって自分のドメインモデルが汚染されないように、必要に応じて自分のコンテキスト内のモデルに変換します。なので、この変換はコンテキストの外側にあることになります。
コンテキストマップに描くとこのようになります。
OHS は公開ホストサービス、PL は公表された言語、ACL は腐敗防止層です。

公開ホストサービスに限らないのですが、サーバー上のサービスが提供しているコンテキストとアプリのコンテキストが異なるのであれば、モデルの変換が必要になります。
実践ドメイン駆動設計では次のような例があります。本ではXMLですが、ここではJSONに置き換えました。

別のコンテキストの userInRole をそのまま利用側のコンテキスト内で使うのではなく、利用側のコンテキストに存在するドメインモデルである Moderator に変換して利用します。
さて、我々のアプリのドメインが何か考えました。
ユビキタス言語がDDDを行っていくうえで重要な要素であることを理解しました。
境界づけられたコンテキストを見つけ、コンテキストマップを描きました。

次に考えることはなんでしょうか?
実践ドメイン駆動設計では、コンテキストマップの次の章はアーキテクチャです。
ドメイン駆動設計でアーキテクチャというと、レイヤ化アーキテクチャのことがよく出てきます。エリック・エヴァンスの本で紹介されているからか、ドメイン駆動設計ではレイヤ化アーキテクチャを使わなければならない、とか、ドメイン駆動設計はレイヤ化アーキテクチャにすることだ、のような誤った認識をときどき見かけます。
ドメイン駆動設計は特定のアーキテクチャに依存するものではありません。
ではレイヤ化アーキテクチャを取り上げた理由はなぜか、何をしたいのか、
それはドメイン層を隔離することです。
ここでいうドメイン層というのは、ドメインモデルの集まり。正確にいうとドメインモデルを表現したコード、実装の集まりです。
ドメインにある概念や知識、ビジネスロジックとも表現しますが、これをドメインモデルとしてその他から隔離するということです。
Androidアプリ開発において、本来ドメインモデルとして隔離すべき、ドメインにある概念や知識、ビジネスロジックが混入しがちなのが、ユーザーインタフェースです。

なぜ我々はユーザーインタフェースに、ドメインにある概念や知識、ビジネスロジックを詰め込んでしまうのか。
それはアプリの作り方に深く関わっています。
エリック・エヴァンスの本にも登場する利口なUI(スマートUI)は、ユーザーインタフェースにすべてのビジネスロジックを埋め込むパターンです。

その利点には次のように書かれています。
・単純なアプリケーションの場合、生産性が高く、すぐに作れる。
・それほど有能でない開発者でも、この方法なら、ほとんど訓練しないで仕事ができる。
・要求分析が不足していても、プロトタイプをユーザに公開し、その要望を満たすように製品を変更することで、問題を克服できる。
などなど

この利点が利点として生きる場所があります。モックやプロトタイピングです。
モックやプロトタイピングでは、画面のデザインを動くもので素早く確認するのが目的ですから、単純なビジネスロジックも含めすべてユーザーインタフェースに入れます。


つまり、動くアプリをすばやく作ってユーザーに見せて検証したい、という場合、利口なUIになりがち、ということです。


他にも、仕様が決まった後に画面デザインとちょっとした機能説明が書かれたドキュメントが来て、それをもとに実装する...
ありがちな状況ですが、画面デザインと単純な機能を短い期間で作ることを求められる状態でも利口なUIになりがちです。
そこから抜け出すにはどうしたらいいのか。

理想を言えば、機能について議論するところから開発者も参加して、みんなの頭のなかにある概念的なモデルについて観察し、ドメインモデルを作りたい。

でもいきなりそんなこと言われてもできないよ、ってなりますよね。


重要なのは利口なUIになりがちだと認識すること。そして、ドメインにある概念や知識、ビジネスロジックがUIに存在していないか観察し、ドメインモデルとしてUIから隔離できないか考えることです。

ただし注意してほしいのは、ユーザーインタフェースからビジネスロジックを単純に隠蔽することと、ドメインモデルとして隔離することは違います。
Activityとライフサイクルを同期するようにした、なんたらプレゼンターみたいな名前のクラスを作って、そこに処理を全部移譲することではありません。

大事なのはドメインモデルです。ドメインを反映したドメインモデルを作ることで、ユーザーインターフェースからビジネスロジックを引きはがせないか考えましょう。


ここまでの話を一旦まとめましょう。
まず、アプリのドメインとは何か考えました。
次にドメインエキスパートと会話をしてドメインを構成する言語を見つけ、ユビキタス言語として確立し、育てていく必要があることがわかりました。
そして、言語の境界がコンテキストの境界になり、アプリにはそれを構成する境界づけられたコンテキストが複数存在することがありえるということをみました。
コンテキスト同士の関係をコンテキストマップとして描くことで、どのような関係性がコンテキスト同士の間にあるのか把握できるようになりました。
これら戦略的設計と分類できる手法の目的は
ドメインを反映したモデル、ドメインモデルを作り上げることです。
作ったドメインモデルを正確に表現するコードとして実装し、これを繰り返してドメインモデルとコードの両方を洗練させていく。
ドメインモデルを正確にコードで表現する、言うのは簡単ですが実行するのは難しい。そこでドメイン駆動設計ではそのために役立つ技術的なパターンも紹介されていて、それが戦術的設計に分類される手法です。
アーキテクチャの話のところで出てきた「ドメインを隔離する」というのは、ドメインモデルを正確にコードで表現するために必要ですし、他にも値オブジェクトやエンティティ、サービス、リポジトリなどの手法があります。

重要なのは、これらの技術的なパターン、手法は「ドメインモデルを正確に表現したコードを実装する」ためのものです。表現したいドメインモデルがないのに、技術的なパターンを真似してもドメイン駆動設計の恩恵は限定的です。
ドメイン駆動設計について、多少でもおわかりいただけたでしょうか。

なんとなく理解したけどコードが出てこないとやっぱりよくわからない。 私もそうです。言葉だけで理解するのは難しい。

というわけで、ここからはおまけです。
ドメイン、そしてドメインモデルについて考える練習として、すごく単純な例を用意しました。
動物の体重をTextViewに表示するコードです。 体重がわからない動物のときは空表示にしたいようです。

このコードには2つ問題があります。
1つ目が、-1という数字が特別な意味をもつということをユーザーインタフェースが知ってしまっているということ。
2つ目が、マイナスの体重というものが表現されてしまっていることです。

体重というものについて考えてみてください。体重の概念にマイナスというものはありますか?私の体重マイナスですっていうかたいますかね?いたとしたら多分その人は反物質でできているんだと思います。 ここで大事なのは科学的にありえるかどうかではなく、ドメインについて考えることです。動物の体重という概念を反映したドメインモデルを考えるとマイナスという状況はないですよね。

ではどうするか、体重を反映したドメインモデルを作るんです。そのモデルは浮動小数の値を持っていて、その値はマイナスではない。いや、マイナスではないというより 0 より大きいと考えるほうが適切ですね。

ドメインモデルを考えたので実装してみましょう。

Weight オブジェクトのインスタンスから取得した体重の値は、必ず 0 より大きい。つまり体重に対するドメインモデルを表現できています。
これを利用すると先ほどのコードはこうなります。

注意してほしいのが、未入力を -1 で表すか null で表すかという話ではない、ということです。
ドメインにおける体重というものについて考え、それを反映したドメインモデルを考え、Weight クラスというドメインオブジェクトとして表現する、という話です。
今度は性別を表示するようです。

このコードにも問題がありますね。
文字列が “M” の場合がオスで”F”の場合はメスだそうです。文字列の意味をUIが知っている。なんて賢いUIなんでしょう。

なんでこんな実装になっているのか聞いてみました。
「サーバーのレスポンスが文字列だったので...」
なるほどーーーー。

UIが文字列の意味を知ってるのは変だから直してみてくれる?
「わかりました。できました。ユーティリティクラスを作って、そっちの static メソッドで判定するようにしました!」

そういうことじゃないんだ...


UIがどのユーティリティメソッドを使って判定するか知っていなければならない、結局UIが賢い問題は解決していない。

必要なのはドメイン、つまり性別について考えることです。
我々の頭のなかには性別という概念的なモデルがあるのだから、それを表現するドメインモデルを用意しましょう。
動物の性別を表現するモデルで、オス(MALE)とメス(FEMALE) がある。

このドメインモデルをコードで表現するなら enum でよさそうです。
「サーバーのレスポンスは文字列ですよ。どこで enum に変換するんですか?」

そこで出てくるのがコンテキストの統合で紹介した腐敗防止層です。

ここで、サーバーのレスポンスをアプリのコンテキスト内のドメインモデルに変換します。
長い時間お疲れ様でした。

ドメイン駆動設計について多少なりとも理解いただけたでしょうか。
難しかったかもしれませんが、
まずはドメインについて考えるところから始めてみてください。
そして、大事なのはドメインモデルです。

技術的なパターンだけとりあげたブログなどがよくありますが、表面的なことを真似しただけでうまくいくようなそんな簡単なものではありません。

例であげたようなシンプルなドメインモデルからでいいのです。ドメインを反映したモデルを作るという本質に取り組んでほしいです。
DDDは概念的な話が多くて理解するのが難しいかもしれません。理解しても実行するのは一筋縄ではいかないです。でも怖くはありません。

行き当たりばったりの設計に、とりあえず動けばいいやで作られたアプリを長年メンテナンスし機能追加するほうがよっぽど恐ろしいです。

シンプルなところからぜひドメイン駆動設計の本質を取り入れてみてください。




0 件のコメント:

コメントを投稿

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,''):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

Blogger Syntax Highliter

Version: {V}

http://www.dreamprojections.com/syntaxhighlighter

©2004-2007 Alex Gorbatchev.

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,'\n'):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

ページビューの合計