前回に引き続き、スピーチ原稿と合わせて公開します。
(講演ではアドリブもあるので原稿とは微妙に異なることをご了承ください)
追記 :
前回の内容を読んでいない方は、先にそちらを読んでください。
前回の Droid Kaigi で私は「ドメイン駆動設計とは何か」という話をしました。
本当は前回のCfPを出す時点でAndroidアプリ開発での実装の話もいれようかなと思っていたのですが、ドメイン駆動設計が何かをきちんと説明するだけでいっぱいいっぱいでした。
今回は前回の続きなので、簡単に前回の復習からはじめます。前回の話の完全版は私のブログに書いてありますので、ぜひ読んでください。
前回の説明を復習すると、ドメイン駆動設計とは
ドメインエキスパートの言葉を観察し、ドメインを構成するユビキタス言語を見つけ、
ユビキタス言語を使ってドメインを適切に反映した我々のソフトウェアに役立つドメインモデルを作り、
作ったドメインモデルを正確に表現するようコードを実装し、
これを繰り返してドメインモデルと実装の両方を洗練させていく設計手法です。
ドメイン駆動設計では、実践するために役立つさまざまな手法が出てきます。これらは主に戦略的設計と戦術的設計の2つに分けることができます。
ドメインモデルを作り上げるために役立つ手法が戦略的設計、ドメインモデルからそれを表現した実装を行っていくのに役立つ手法が戦術的設計です。
戦略的設計のユビキタス言語、境界づけられたコンテキスト、コンテキストマップについては前回お話しました。
今回は戦術的設計についてお話します。
ドメイン駆動設計では、ドメインモデルをそのまま表現するように実装します。
究極的にはコードを読めばそのドメインモデルがわかるし、そのドメインモデルを理解しているならエンジニアじゃなくてもなんとなくテストコードが読めるという状態です。
じゃあ具体的にどうすれば、ドメインモデルをそのまま表現した実装にできるのかなって思いますよね。
そこで、こういうふうに実装するとうまくドメインモデルを表現できたよ、という実装パターンが紹介されています。
それが戦術的設計です。
戦術的設計としてたくさんパターンが紹介されているのですが、これらを取り入れなくても、ドメインモデルをうまく表現した実装ができるなら、それはちゃんとドメイン駆動設計です。
例えばドメインイベントは、エリック・エヴァンスの本が出版された後に新しく付け加えられています。
戦術的設計のパターンはたくさんありますから、すべてがAndroidアプリの実装に役立つとは限りません。
例えば、実践ドメイン駆動設計にはMongoDBやSpringでの例が出てくるパターンがあるのですが、Androidアプリの開発に置き換えて考えるのはなかなか難しいです。
よって、この講演ではこれらを網羅的に紹介したりはしません。
この講演では、
長年の開発で大きく、複雑になってきたAndroidアプリにドメイン駆動設計のエッセンスを取り入れてみたいけど、どこからはじめればいいかさっぱりわからない。
という方に向けて
この一年半、私がドメイン駆動設計を念頭に、既存のAndroidアプリの実装をリファクタリングして、これはよかったなという戦術的設計のパターンと、どう取り入れたかの例を紹介します。
作り直すときの話ではありません。
いままで5つ以上のアプリで、ドメイン駆動設計をどう取り入れるか試行錯誤してきました。
複雑に絡み合ったアプリのコードを少しづつ紐解いて、UIに隠されていたドメインモデルを実装としてどう表現してきたかという話です。
もう少し前提条件というか状況をはっきりさせておきます。
こういう方法よかったよと言う場合、無条件にすべてに適用できるものではありません。どういう状況のときによかったのかをはっきりさせておかないと、お互いに不幸になります。
特にドメイン駆動設計はさまざまなソフトウェアの開発で取り組まれていますから、同じ戦術的設計のパターンであっても、対象とするドメインだけでなくプラットフォームやフレームワークによっても最適な実装が異なるでしょう。
この講演で紹介する実装例は、以下の状況を前提としています。
まず、Androidアプリの開発での話です。iOSアプリやサーバーサイドの開発には参考にならない可能性があります。
次に、少なくとも1年以上継続的に開発しているアプリを前提としています。
これは「ある程度モデルらしきものがメンバーの共通認識としてあり、コードにもそれっぽいものがあるが、ドメインモデルとしてコードでうまく表現されていない」という状況になるには1年ぐらい必要かなと思ったからです。実際にはもっと年数の経っているアプリで取り組んでいます。
そして重要な前提条件が、サーバーとAPIを介してやりとりする、よくあるクライアントアプリであるという点です。ゲームアプリなどにはあまり参考にならないかもしれません。
サーバー側が別チームというのは、前回お話した境界づけられたコンテキストに関係する部分です。サーバーとアプリで別の境界づけられたコンテキストになっているという状況です。
では、さっそく本題に入りましょう。
最初の戦術的設計パターンは「ドメインを隔離する」です。
前回の話で、「ドメインを隔離する」というのは
「ビジネスロジックをドメインモデルとして隔離する」ということだとお話しました。
なぜ隔離するのか
本では次のように書かれています。
「ドメインロジックがプログラムの他の関心事と混ざり合っていたら、設計と実装をこのように一致させることは現実的ではない」
ここでのドメインロジックは、ドメインモデルに属するロジックのことです。ドメインモデルに関連するロジック、ドメインが持っているロジックとも言えます。
例えば、ドメインモデルに属するロジックが UI に書かれていると、ドメインモデルをそのまま表現した実装にできません。
設計と実装を一致させるために隔離する、ということがわかりました。
そうすると次は、どうやって隔離するのか、です。
本では次のように書かれています。
「ドメイン層を隔離した状態に保つことさえできれば、どのアプローチでもかまわない。」
「DDDの大きな利点のひとつが、特定のアーキテクチャに依存しないということだ。」
この文章は前回も紹介しました。
つまりドメインを隔離する方法は問わないということなんです。
好きな方法でやればよいのです。なので前回は具体的な方法は紹介しませんでした。
エリック・エヴァンスのドメイン駆動設計を読んだ方なら、じゃあアレはなんなんだ、と思うかもしれません。
そう、レイヤ化アーキテクチャです。
なぜエリック・エヴァンスのドメイン駆動設計では、レイヤ化アーキテクチャが紹介されているのか。
方法は問わないと言っているのに紹介しているなら、その理由は簡単です。
ただの例です。
いきなりドメインを隔離しろと言われても、具体例がないとなかなか理解できないものです。
つまり、ドメインを隔離できそうな設計としてこういうのがあるよ、という例としてレイヤ化アーキテクチャが紹介されている、ということなんです。
あくまで目的はドメインを隔離することです。それができればどのアプローチでもかまわない。
ただし、方法については次のようにも書かれています。
「アーキテクチャがドメインに関連するコードを隔離して、凝縮度が高いドメインの設計が、システムの他の部分と疎結合できるようにしているなら、そのアーキテクチャはおそらくドメイン駆動設計を支えられるだろう」
単に置き場所を分ければいいというわけではなく、他の部分と疎結合できるようになっている必要がある、というわけです。
まとめると、システムの他の部分と疎結合できるような方法でドメインを隔離する、ということです。
Androidアプリでもドメインを隔離したくなってきましたね。
前回お話しましたが、我々はUIにドメインにある概念や知識、ビジネスロジックを詰め込みがちです。
利口な UI から脱却するにはぜひともドメインを隔離したいところ。
そうなると問題はどう隔離するか?です。
やりたいことは、
UIはドメインを知っているが、ドメインはUIを知らないようにしたいということ、
ドメインと他の部分を疎結合にしたいということ、
そして、それを強制させたいということです。
単純にパッケージを分けるだけだと、ドメインからUIのクラスを見ようと思ったら見えてしまいます。
見ないように気を付けようだと形骸化します。メンバーが変わった時だけでなく、リリースに間に合わせるために今回だけ特別、みたいなことが容易に想像できます。
そこで、ドメイン用に別モジュールを用意することにしました。
このモジュールは gradle のモジュールのことです。
このような構成にすると、ドメインがUIを知らないように強制できますし、純粋なロジック部分だけになるのでテストが書きやすいです。
また、UI を変更してもドメイン部分に変更がないのであれば、その部分の動作を担保できますし、ドメインモジュールのリビルドが走らないのでちょっとだけビルドが速くなります。
さぁ、ドメインモデルの置き場所が決まりました。
あとは、ここにドメインモデルをガンガン入れていくのですが、
何を入れたらいいかわからん...ってなりませんか?
いや、入れるのはドメインモデルってのはわかってる。
わからないのは、自分のコードの中でどれがドメインモデルであるべきなのか、どこから見つけていけばいいのか...
そこで役に立つのが次の戦術的設計のパターン、「値オブジェクト」です。
まずは、ドメイン駆動設計の本で値オブジェクトについてなんと言っているのか見てみましょう。
「あるオブジェクトが、ドメインにおける記述的な側面を表現し、概念的な同一性を持たない場合、そういうオブジェクトは、値オブジェクトと呼ばれる。」
概念的な同一性を持たない、というのは、一意に識別する必要がない、区別する必要がないということです。
例えば色は値オブジェクトの一例です。
同じ色を持つ Color オブジェクトを区別する必要はありませんよね。
ドメインモデルの中には、このような「何であるかだけが問題となり、誰であるかやどれであるかは問われないような要素」がでてきます。
これらを値オブジェクトとして実装することで、ドメインモデルを表現した実装になります。
値オブジェクトの他の面も見てみましょう。
ドメイン駆動設計では、値オブジェクトを不変にすることを勧めています。
不変であれば自由にコピーや共有ができ、複数のスレッドで安全に利用できます。また、引数や戻り値として別のオブジェクトに渡しても、その先で変更されないので設計の簡素化につながります。
例えば Color オブジェクトであれば、このように内部で保持する色の値を可変にするのはおすすめできません。
このように Color オブジェクトは不変にし、色を変更するときは Color オブジェクト自体を入れ替えるようにします。
この話を聞いて、なんか聞いたことあるなと思った方いらっしゃると思います。このイミュータビリティの話は Effective Java でも紹介されています。
さらに別の面も見てみましょう。
「値オブジェクトを構成する属性は、概念的な統一体を形成すべきである」
概念的な統一体...ちょっと難しいですね。
例えばユーザーに紐づく情報として、IDや名前の他に郵便番号、都道府県、残りのアドレスがあるとします。
このとき、郵便番号、都道府県、残りのアドレスは、ユーザーの別々の属性ではなく、住所という統一体を形成する属性です。
よって、この3つで形成される住所という値オブジェクトを用意し、ユーザーには住所を持たせるようにします。
値オブジェクトはドメイン駆動設計において、ドメインモデルを構成する基本的な要素です。エンティティやドメインサービスなど他の戦術的設計パターンではみな値オブジェクトを利用します。
つまり、値オブジェクトとなるドメインモデルを見つけるところから始めるのが最適ということです。
いやいや、そうは言っても、Color とか Android フレームワークにあるし、値オブジェクトにするもの見つかるかな?
そんなあなたのために、今回は Android アプリの開発で値オブジェクトとしてモデリングした例をたくさん用意しました。
例を紹介する前に、Android アプリで値オブジェクトをどう実装するかですが、
実装要件である
状態を不変に保つ、つまりイミュータブルにできる
値が等しいかどうか比較できる
全体を完全に置き換えられる
に注目すると、
Kotlin の data class が最適です。イミュータブルにしたいのでプロパティは val ですね。
Java ならフィールドを全て final にして equals() と hashCode() を override するか、AutoValue などのライブラリを使うという手もあります。ですが、これを機に Kotlin を導入してみてはいかがでしょうか。
では Android アプリで値オブジェクトを見つけていきましょう。
UserId, ItemId, ProductId, OrderId などなど、アプリの中で一つぐらい ID というものを扱っていると思います。
このID、文字列で取り回していませんか?
この ID を値オブジェクトにするとどうなるか、最初の例を見てみましょう。
ユーザーID からフォロワーの一覧を取得するAPIです。
フォロワー一覧画面を作るとき、このAPIを呼び出す側も呼び出される側も同じ人が実装すると、何を渡せばいいのかわかっているので String であっても特に困ることなく実装できます。
後になって、別の人が他の場所からこのAPIを呼ぶことになったり、機能を変更することになったら、次のような問題が出てきます。
userId ってどこにある id だっけ?
Profile クラスの持ってる id ってここに渡していいやつだっけ?
空文字ってだめだよね?呼び出す前にチェック必要?
userId が空文字になるわけないんだけど、でも Profile が持ってる id に空文字が入らないなんて他の部分も読まないと保証できないし...
そこで UserId を値オブジェクトにしましょう。
UserId のインスタンスがあるなら、それが保持している文字は絶対に空文字ではないと保証できる状態にします。
こうすれば、利用側は安心して getFollower() API を呼び出すことができます。
Profile クラスが持っている id が UserId 型なら、この API に渡してよいいIDであることが明白です。
さらに String から UserId 型にすることで、どこで利用されているかを静的に解析でき、リファクタリング時の影響範囲も調べやすくなります。
値オブジェクトとして ID を表現するということは、ユビキタス言語の成長にも繋がります。
例えばアプリの中に categoryId というのがあったので、値オブジェクトにしようと思って利用箇所などを調べていると
genreId というのもあることに気づいてしまいました。よくよく話を聞くと
この2つは同じものでした。
この ID 用の値オブジェクトを用意するにはクラスの名前が必要です。この名前はユビキタス言語になるものです。
そこで、どちらをユビキタス言語にするか話し合って GenreId に統一しようということになりました。
GenreId を値オブジェクトとして用意するとこうなります。
GenreId の変数が categoryId なのすごく違和感ありますよね。
ID を値オブジェクトにできないか考えたことで、言語の問題に気づけ、チームとして新しいユビキタス言語を見つけることができました。まぎらわしい変数名がつけられるのを防ぐこともできます。
次は ID が複数の情報を含んでいる場合の例です。
商品 ID が、ブランドの ID とブランド内でのコードから構成されています。
商品画面からブランド一覧画面に遷移するために、商品 ID の文字列を処理してブランド ID 部分を取り出すようになっていました。
id が空文字だったり : を含まない文字列だったら、意図しない値で BrandActivity を開くことになる、という問題もありますが、
ここでの一番の問題は、UI が知るべきではない商品 ID のフォーマットが漏れてしまっているということです。
もうわかりますね、
値オブジェクトとして ProductId を用意しましょう。
正しいフォーマットから ProductId が構成されることを保証できますし、UI 側は商品 ID の文字列表現がどうなっているかを知らなくてすみます。
値オブジェクトの見つけ方わかってきたでしょうか?
では次の例に行きます。
ユーザー登録画面の作成を依頼されました。
性別と生年月日を渡す必要があります。
作り始めて気がつきました。
性別ってどんな文字列送ればいいの?
というか、そもそも扱いたいのは性別そのものであって、性別を表現した文字列じゃないよね。ということは性別はモデルでは...
同じ性別なら、性別として区別する必要はないのでこれも値オブジェクトです。
このようなとりうる状態が限定されている値オブジェクトは enum で表現できますね。
性別の文字列について UI が知らなくて済むようになりました。
しかし、まだ問題があります。
誕生日の方もどういう文字列を送ったらいいかわかりませんね。
こちらも考えてみましょう。
同じ日を指している日付は区別する必要がないので、値オブジェクトとして表現できます。
誕生日など特定の意味をもった日付はドメインモデルです。
DateOfBirth クラスとして実装するとこのようになるでしょう。
どういうフォーマットで文字列にするのかを UI から隠蔽できます。
引数の型として DateOfBirth を使うことで、何を渡せばいいのか明白になりました。
次の例に行きましょう。
画像の縦横サイズとURLを持つ Image というオブジェクトがあります。画像のアクペクト比が 4 : 3 より横長、例えば 16 : 9 とかだったら、 wide 用の placeholder を使う、という仕様です。
このコードの問題は、wide かどうかの判定処理が UI に書かれているところですね。ImageView に画像をロードするあちこちの処理で同じコードが書かれていそうです。
UI に書くのはよくないよね、ということで次にありがちなのが、ユーティリティクラスです。
判定処理の重複はなくなりましたが、ドメイン駆動設計としては問題があります。
Image クラスがただのデータモデルになっているという点です。このような状態をドメインモデル貧血症といいます。
wide かどうかの判定はドメインモデル、つまり Image に属するロジックなので、Image に持たせましょう。
ロジックが誰の責務なのか考えるの難しいですよね。私も迷うことがよくあります。この例にしても、wide かどうかの情報は placeholder を使い分けるために使っているから、UI のロジックであってドメインのロジックではないのでは?と思うかもしれません。
この例では Image に置きましたが、表現するドメインモデルが異なれば異なる実装になるでしょう。画一的は判断基準はなく、頭の中のドメインモデルにとって自然かどうかだけです。
wide かどうかという属性は Image というドメインモデルにとって自然かどうか。
最初からしっくりくる実装ができないことはよくあります。大事なのは、その実装で固定化せず、よりよい方法が思いついたときにリファクタリングする、できるようにしておくことです。
次の例にいきましょう。
サーバーのAPIを叩いて JSON などのレスポンスをもらい、それをオブジェクトにマッピングするという処理は、多くの Android アプリで行われています。
このとき、JSONやXMLの構成をそのまま反映しただけのクラスにしていませんか?
例えば商品の情報を取得するAPIからこのようなレスポンスが返ってくるとします。
この images の配列には、同じ画像の異なるサイズへのURLが入っていて、アプリでは表示する領域の大きさに応じて、この中から適切なものを使う仕様になっています。
この JSON をそのままマッピングすると、このようなクラスになります。
アプリでは表示する領域の大きさに応じて、この中から適切なものを選んで使う仕様になっているので、どの Image を使うか判定するロジックが必要になります。
さぁ、このロジックをどこに置きましょうか?
複数箇所で同じことをやるからとか、UI にロジックを置くのはよくないからなどの理由で、このようなユーティリティクラスに置かれることがあります。
この場合、Item クラスは値を持っているだけで、そのデータを使った判断や加工処理は別のところで行われることになります。
このような状態は、先ほどと同じドメインモデル貧血症ですね。
Item が貧血になっているので、Item にこのロジックを置いてみました。
ところが問題が起こります。
Item の他に Category というのがあり、こちらでも Image を選択する処理が必要でした。
Item と Category に同じロジックが重複することになってしまい、よくありません。
じゃあどうするか。
JSON の形式を一旦忘れて、「表示する領域の大きさに応じて適切な Image を選択するロジック」は誰の責務なのかを考えましょう。
本当に Item の責務なのかな?
Item や Category の責務ではなく、Image の集合という別のドメインモデルの責務なんじゃないかな。
Image の集合を表現するドメインモデルとして Images クラスを導入してみたらどうだろう。
適切な Image を選択するロジックの置き場所として自然な感じがしませんか?
サーバーのレスポンス形式にとらわれず、別のドメインモデルがあるのではないか、ドメインモデルを適切に表現する値オブジェクトがあるのではないか考えてみてください。
次の例にいきましょう。
今度はお知らせの一覧を取得したときのレスポンスです。
各お知らせにはバナーが含まれている場合があり、バナーには表示期間の開始日時と終了日時があります。
これをそのままマッピングすると、このようなクラスになります。
これを使うとしたらどうでしょう。
bannerImageUrl は null じゃないけど、bannerStartDate が null のときはどうするの?って思いませんか?
この banner に関する3つの属性は、それぞれがお知らせに属するのではなく、この3つで概念的な統一体を構成しています。
つまり、この3つの属性をもった値オブジェクトが必要ということです。それを Banner クラスとして用意すると、このようになります。
banner のインスタンスがあれば url も日付も揃っているということを保証できます。
さらに、開始日時が終了日時より前であることを事前条件として保証できますし、今日が表示期間内か判定するロジックの置き場所としても自然です。
次の例に行きましょう。
このアプリにはユーザーが使える電子アイテムがあります。
アイテムにはログインしていなくても使えるものや、ログインしていれば使えるもの、有料会員になっていれば使えるものなどがあり、サーバーからは複数のフラグが返されます。
昔は2種類しかなかったなど、歴史的経緯とか拡張とかで、こういうレスポンスになっています。
今までの話の流れから想像つくと思いますが、こういうクラスにマッピングされていました。
UI 側で if 文を駆使した処理が行われていることは容易に想像できます。
ここでも一旦レスポンス形式のことを忘れましょう。
アイテムをドメインモデルとして考えると、アイテムの属性として自然なのは個々の Boolean よりも、どういうアイテムなのかという種類です。
そうなると次はどういう種類があるのか、ということになりますが、ここで何も考えずに Boolean 4つの組み合わせなので、2の4乗で16種類あるね、としてはいけません。
次にすることは、ドメインエキスパートに話を聞きに行くことです
どういう種類のアイテムがあると認識しているのか聞いてみると、結局は5種類しかないことがわかりました。
この5種類を enum にすればよいですね。
Boolean の組み合わせは16パターンあるのに、アイテムは5種類しかないということは、ありえない組み合わせや縮退している組み合わせがあるということです。
判明した種類を元に、サーバーチームにどういうときにどういう値が返ってくるのか確認しましょう。
例えば、
isMemberUsable は都度課金用のアイテムができたときに isPurchaseRequired と一緒に追加したので、isPurchaseRequired が true のときしか意味ないです。
とか、
isPurchaseRequired が true の時は isLoginNeeded と isMembershipRequired は意味ないので無視してください。
などの情報が得られました。
あとは得られた情報から enum に変換しましょう。
どこで変換するかって?
腐敗防止層ですよ。前回出てきましたね。
同じ属性値を持つなら区別が必要ない、というモデルがあり、それを値オブジェクトで表現する例をいろいろ紹介してきました。
しかし、モデルの中には同じ属性値であっても区別が必要なものがあります。例えば、AさんとBさんが同じ山田太郎という名前であっても、別の人として区別が必要です。
このようなモデルを表現するオブジェクトとしてドメイン駆動設計にはエンティティという戦術的設計パターンがあります。
本ではこのように書かれています。
「主として同一性によって定義されるオブジェクトはエンティティと呼ばれる。」
人間には同一性があります。例えば年齢や見た目が変わってもAさんが同じ人だと認識しますよね。
「ライフサイクルにおいて、エンティティの形態と内容は根本的に変わることがあるが、連続性のつながりは維持されなければならない。」
エンティティにはライフサイクルがあります。人がその一生の間に変化していくように、ライフサイクルの間にエンティティの内容は変わることがあります。そのとき、新しく作られるのではなく、以前の内容が変わったという連続性が維持される必要があります。
この連続性と同一性をソフトウェアで表現するときに問題になるのが、連続性を維持するためにどう永続化するかと、同一性を持たせるための識別子をどう発行するかです。
これらは技術的な制約と関係することもあり、エリック・エヴァンスのドメイン駆動設計でも実践ドメイン駆動設計でも具体的な実装を出して議論されています。
Android アプリでも、スタンドアローンで、マスターデータをアプリ内のデータベースに保存するような場合には、これらをどう実装するかが問題になります。
しかし、マスターデータがサーバーに保存され、識別子もサーバーで発行されるクライアントアプリでは、これらをアプリ側で行うことがありません。
現時点で Android アプリでのエンティティについて私が言えることは、識別子を使ってオブジェクトを比較するように実装すべきということだけです。
UserId で一意に識別すべき User なら、UserId で識別するよう equals() と hashCode() を override すべきでしょう。
Android アプリでのエンティティの扱いについてはまだまだ試行錯誤中で、例えば SharedPreferences に保存している、初回起動かどうかのフラグや通知を受け取るかどうかの設定はエンティティなのか、など悩みはつきません。
この話は機会があれば次回できるといいなと思っています。
では、まとめです。
ドメインを隔離するという目的に対し、gradle のモジュールで分ける方法を紹介しました。モジュールに分けることで依存方向を強制でき、ドメインが UI などのその他の部分を知らない状態に隔離できます。
同じ属性値をもっているなら区別する必要がないモデルは値オブジェクトとして表現できるということを紹介しました。
属性は不変にし、値オブジェクト自体を差し替えることで変更に対応しましょう。
モデルに属するべきロジックを持たせましょう。モデルの表現として、条件を満たしているときだけインスタンス化しましょう。
文字列で取り回している要素が値オブジェクトではないか考えましょう。ID、種類、日付、サイズなどは値オブジェクトを導入するよいスタート地点です。
サーバーのレスポンス形式は時としてモデルをうまく表現できていないことがあります。それに引きづられてドメインモデル貧血症になっていませんか?一旦レスポンス形式のことは忘れて、どういうモデルがあるべきかを考えましょう。
以上です。ご静聴ありがとうございました。