こんにちは。ナビタイムジャパンの地図フレームワークエンジニアときどきAndroidエンジニア、3代目ゆうです。
これは NAVITIME JAPAN Advent Calendar 2019 9日目の記事です。
この記事ではナビタイム社員旅行用のしおりアプリをDDD(ドメイン駆動設計)で作ってみた軌跡についてお話したいと思います。
はじめに
ナビタイムジャパンでは毎年秋に社員旅行を開催しています。最近では社員旅行のある会社は珍しいみたいですが、ふだん仕事で関わることが殆どの同僚と非日常な雰囲気で交流できるのは中々に楽しく、私は気に入っています。
それになんといっても会社のお金で美味い飯、美味い酒、いい温泉である。
しかして我らエンジニア、ただ消費するだけの旅行じゃ面白くない。せっかくならば社員旅行にかこつけて何か楽しい開発をしようじゃないかと、有志のアプリエンジニア数人が集まり勝手に始めた企画が今回お話する社員旅行アプリです。
旅のしおり(行程表)や各自の新幹線座席表、旅館の部屋番号などなど、必要な情報をまとめた一つのアプリを作っちゃおうという企画でした。
社員旅行アプリの目的、要件
そんな些細なきっかけから始まった社員旅行アプリプロジェクトですが、大まかな目的として下記を設定しました。
- 開発の目的
- 部署を超えて沢山の人達とアプリを作りたい、交流したい
- 新しい技術を使いたい、挑戦したい
- 今後の業務で参考になる設計・実装のアプリにしたい
- アプリの要件
- 社員旅行での”困った”を解決するアプリにする
- Android/iOSのネイティブアプリにする
本記事では「今後の業務で参考になる設計・実装のアプリにしたい」という目的からDDDを採用した経緯および実践してみたドメインモデル化の過程について書いていきたいと思います。
実際はよりアプリ実装に近い設計(アーキテクチャ)の話や、それに合わせたフレームワークライブラリの選定なども同時に行いましたが、話が大きくなりすぎてしまうため割愛します。
ドメイン駆動設計の採用
今回、有志メンバー同士で話し合い、以下の理由からDDDを導入することにしました。
チーム単位でのドメイン駆動設計を経験したい
一つ目の理由にして、最大の理由がこちらです。
今回の参加メンバーの習熟度としては、いわゆるDDD本1やIDDD本2の読了者から、「DDD?なにそれ?」な人まで幅広くいました(ちなみに、私は「DDD?なにそれ?」側の人間です)。中には個人のアプリ開発でDDDを実践しているという人や、普段の業務でドメインを意識した設計をしているという人もいましたが、チームとして導入した経験のある人はいませんでした。
新規サービスの立ち上げなどでもない限り、業務で突然導入するにはハードルが高いのが実情だと思います。しかし今回の社員旅行アプリは、「新規開発」で「規模もそこまで大きくなく」、「趣味半分」なので練習台として扱うには丁度いいのでは?という意見から実現することとなりました。
Android/iOSエンジニア間で用語の認識齟齬などを防ぎたい
前項のとおり、DDDの浸透が十分とは言えない開発現場において、身近な課題としてよく挙がるのがこちらです。
ナビタイムのアプリはその多くがAndroid/iOSの両OSについてネイティブアプリとして開発し、基本的には同じ機能を持つように作ります。
しかし、多くの場合それぞれ別の開発者が担当することになるため、機能の仕様検討までは一緒にやることが多くとも、コードレベルまで同調して開発ということはあまりやっていません。
その結果、同じ機能、同じ概念を指した言葉であってもコード上の名前が異なっているという事例は珍しくありませんでした。
今回、社員旅行アプリはAndroid/iOSの両OSについてネイティブアプリとして開発するという要件がありました。
また、「沢山の人達で作りたい、交流の場にしたい」という想いから、社内に広くコントリビュータを募り、不特定多数の開発者で作ることも予定していました。その結果として懸念される前述のようなドメインモデルの認識齟齬を防ぐためにも、より厳密な形のDDDが効果的なのではという狙いも導入を後押ししました。
ドメイン駆動設計の実践
DDDを実践していくにあたり、お手本としてDroidKaigi2019のカンファレンスアプリおよびその設計について解説したあんざいゆき氏の講演を参考にしました。
対象ドメインの確認
まず、ドメインモデルを考える上で、対象となるドメインそのものを決定しなくてはなりません。今回は特に大きな議論もなく、対象ドメインは以下の通りとして決定しました。
- 2019年度ナビタイム社員旅行
ユビキタス言語の洗い出し
例年、社員旅行が近づくと総務の方から旅のしおり(PDFファイル)や各個人の旅行コース、新幹線座席などが一覧になったスプレッドシートが展開されます3。そのため、まずはそれらの資料からひたすらにユビキタス言語となりうる単語についてざっくり書き出していくという作業をはじめました。この時点では、あいまいな表現の重複や実際にアプリとして使うかどうかといった事は一旦置いておいて、読み進めながら目についた単語を書き出していきました。
書き出した一例を以下に引用します。
部署 氏名 性別 往路乗車駅 往路新幹線座席番号 コース別観光 1日目バス号車 2日目バス号車 部屋割り ...
さて、一先ずの書き出しが終わったところで、次はそれらの単語をユビキタス言語として整理することとしました。
書き出した一覧から、あいまいな表現や意味合いが重複する単語を統一し、共通言語としての日本語とコードに落とし込むための英語の2言語で定義していくこととしました。
ここまでで、ユビキタス言語として定義した一例を以下に引用します。
個人情報【user】 氏名【name】 性別【gender】 部署【division】 1日目【day1】 2日目【day2】 乗車新幹線【shinkansen】 バス【bus】 観光コース【tripCourse】 部屋番号【roomNumber】 ...
ドメインモデル化
必要な項目名、単語名がユビキタス言語として出揃ったため、いよいよドメインモデル化を行います。
まず、より大きなくくりとなる概念から、それを構成する要素が何か整理することとしました。
ここでは、「個人情報」を例にとって解説していきます。
まず、「個人情報」には「氏名」、「性別」、「部署」が含まれるだろうという事が直感的に分かります。また、今回の対象ドメインは「2019年度ナビタイム社員旅行」であるため、その人が乗車する「新幹線」や「バス」、旅行中の「観光コース」や「部屋番号」も紐付いていいはずです。
実際には、この結論まで紆余曲折経つつ議論を行ったのですが、最終的に以下のような「個人情報」モデルが出来上がりました。
- 個人情報【user】
- 部署【division】
- 氏名【name】
- 性別【gender】
- 社員属性【employeeAttribute】
- 1日目【day1】
- 乗車新幹線【shinkansen】
- バス【bus】
- 配布ドリンク【drink】
- お茶【tea】
- ビール【beer】
- 観光コース【tripCourse】
- 2日目【day2】
- 乗車新幹線【shinkansen】
- バス【bus】
- 観光コース【tripCourse】
- 部屋番号【roomNumber】
さらに、ドメインモデルを考える上で重要な要素として、そのモデルが値オブジェクトなのかエンティティなのかを判断します。
改めて上記「個人情報」モデルを見返してみると、仮にこれらの値が一致していたとしても必ずしも同一の個人であるとは限らない事が分かります。逆に、「部署」や「観光コース」といった属性値が変わったとしても、その個人であることは変わらないため、「個人情報」は同一性によって区別されるもの、すなわちエンティティであると言えます。
エンティティを表現するためには、同一性を一意に識別できるIDが必要になってきます。
IDを付与するといっても、実態として何を使うかは考える必要があります。今回は対象ドメインである「2019年度ナビタイム社員旅行」における「個人情報」を一意に識別するためには社用メールアドレスの@以前が利用できるため、その文字列を利用することとしました。
同様に、以降も全ての概念についてモデル化を行っていきました。
ドメインモデル実装
最後に実装の話に少しだけ触れていきたいと思います。
前項までで洗い出せたドメインモデルを実際のコードに落とし込む作業となります。
なお、実際はAndroid/iOSそれぞれをkotlin/Swiftで実装したわけですが、大まかな考え方は共通な箇所となっているためAndroid(kotlin)を例にとって解説します4。
まず、実際に出来上がった「個人情報」のモデルクラスを御覧ください。
data class User(
val id: UserId,
val division: Division,
val name: String,
val gender: Gender,
val employeeAttribute: EmployeeAttribute,
val day1: Day1?,
val day2: Day2?,
val roomNumber: String?
) : Serializable
inline class UserId(val value: String) : Serializable
先程記載したドメインモデルそのままになっていることがわかると思います。
ドメインモデルは基本的に data class
または inline class
として定義し、選択肢が限られている場合( Gender
や Division
など)は enum class
として定義する形としました。
また、クラス名やプロパティ名にはユビキタス言語を使うようにしました。
実際には、ここからドメインモデルレイヤー以外のUIや各種レイヤーの設計、実装があるのですが、DDDの実践としてはここまでとしたいと思います。
大変だったところ、議論の白熱したところ
前項までのように実践してみて、難しさを感じたポイントや議論の白熱したところがありましたので、その一部を紹介させていただきます。
表記ゆれ多い問題
これはユビキタス言語の洗い出し過程で発生しました。
今回は主に旅のしおりを使って用語の抽出を行ったわけですが、それ自体はあくまでヒト向けの読み物であるため表記ゆれや曖昧な語が多数出現しました(e.g. 旅館, 宿舎, ホテル)。
こういった表記ゆれや曖昧な語を直感的にまとめてしまう事は簡単ですが、今回は一つ一つ愚直に「どういう意味合いを含むのか」「どちらがより適切か」といった事を話し合うようにしました。
思っていた以上に時間はかかってしまいましたが、その分メンバー間での用語に対する認識齟齬は殆ど無くなったため、やはり大事なフェーズだったと思います。
UIとモデルの関係性
ドメインモデルを考える上で意外と大変だったことが「UIの事を考えがち」になってしまったことです。今回集まったメンバーは全員アプリエンジニアだったのですが、弊社のアプリ開発手法としては画面ベースのプロトタイピングから各画面の仕様や機能を考える事が多いため、ふだんの慣習からドメインモデルもUI基準になってしまうという問題がありました。
すなわち、「この画面でこの情報を一緒に出したいから、同じモデルに入るべきではないか?」といった発想です。ドメインモデルがUIの事を意識するべきでないという認識は持っていましたが、分かっていてもついつい画面ファーストに考えてしまうことは少なからずありました。
そこで、今回は議論が行き詰まってきた場合には敢えていろんなパターンの画面を仮想的に考えて、その中で共通のモデルを使う場合にどこでも違和感がないようにすることで普遍的なドメインモデルを探るという方針を取りました。
結果として、実装フェーズに入ってから画面デザインの変更が起きてもドメインモデルに対する修正は発生せずに済んだ事もあったので有効な手段かもしれません。
新幹線は物か概念か
今回の例の中で最もDDDらしく、最も難しかった議論がこちらです。
旅行の行き帰りに乗る新幹線に関する情報について、当初は物理的な新幹線の車両としてモデル化しようと試みました。つまり新幹線とは、複数の車両を持ち、車両の中には無数の座席を持ち、各座席に着座する個人が紐づくものであると考えたのです。
しかし、この考え方だと実際にデータクラスに落とし込んだ時に大量の座席プロパティを持つなどあまり現実的なデータ構造ではなくなってしまいます。さらに、その新幹線の停車駅や発着時刻といった情報はまた別のモデルとして切り出すことになり、必要以上に複雑になってしまう事が分かりました。
その後の議論で、今回必要な新幹線の情報とは、結局のところ「個人がどの新幹線のどの座席に乗るのか、何時に出発し何時に到着するのか」だけだということに気付きました。最終的に、それらの情報を持った値オブジェクト「乗車新幹線」という概念としてモデル化することで解決しました。
ドメインモデルの決定には、対象とするドメインにおいてその用語がどのような文脈で語られているのかを考慮しないと、適切なモデリングにはならないという良い例になったと思います。
まとめ
社員旅行アプリという題材を元にDDDを実践してみた軌跡についてお話させていただきました。
私自身、「DDD?なにそれ?」な人だったわけですが、いざ勉強しつつ実践してみて本記事が書ける程度の理解を得ることが出来ました。
言うまでもなく分かっていたつもりではありましたが、改めてちゃんと設計することの難しさと大切さに気付けるいい機会になりました。
きっかけこそ軽い気持ちで始まった社員旅行アプリプロジェクトでしたが、最終的に新卒1年目も含む30名(!)近くものエンジニア、デザイナが関わってくれました。
それだけの人数で一つのプロダクトを作れたこと、さらにそこでドメイン駆動設計を体系的に実践出来た事は大きな意味を持つのでは無いかなと思っています!
参考
- ドメイン駆動設計/エリック・エヴァンス
- 実践ドメイン駆動設計/ヴァーン・ヴァーノン
- DroidKaigi 2019 で「LiveData と Coroutines で 実装する DDD の戦術的設計」について話してきました。- Y.A.M の 雑記帳