ユースケースシナリオを中心に据えたドメインモデルの育て方
この記事はドメイン駆動設計 Advent Calendar 2020 - Qiitaの19日目です。 昨日はたなかこういち (@Tanaka9230) | Twitterさんの『DDDに関する論の主戦軸を整理してみた(2020年版) - Qiita』でした。
今日はドメイン駆動設計における主要な関心事の一つである「いかにドメインモデルを作って、育てていくか」という話を書きたいと思います。
エリック・エヴァンスの『ドメイン駆動設計』やヴォーン・ヴァーノンの『実践ドメイン駆動設計』には、ソフトウェアが扱う問題領域に存在する知識やルールをソフトウェアの要素(クラスなど)で直接表現することの価値とその実践方法が書かれています。ドメインモデルは問題領域に対する理解を表現したものであり、まずこれがなければドメイン駆動設計のプラクティスは実践できません。では開発チームがドメインモデルに到達するためのプラクティスにはどのようなものがあるのでしょうか?
その答えは世の中にいくつもあると思いますが、今回紹介するのは「ユースケース」を中心に据えた方法論です。ユースケースと聞いて多くの人が思い浮かべるのはユースケース図かもしれませんが、ここで重要となるのはユースケースシナリオ(ユースケース記述)と呼ばれる文章表現のほうです。ドメイン駆動設計はユビキタス言語を認識して言葉を大事にする設計手法です。ユースケースシナリオは自然言語で書かれるので、ユビキタス言語を直接用いてシステムの仕様を表現することができます。
自然言語で書かれているので、エンジニア以外でもほとんど予備知識無しに読み解くことが可能です。ドメイン駆動設計が開発者の中で完結した手法ではなく、顧客やプロダクトオーナー、ビジネスメンバーを巻き込んで行う必要がある以上、設計ドキュメントが誰にでも理解できる形式になっていることが非常に大きな意味を持ちます。ユースケースシナリオを中心に据えた手法は、ソフトウェアに関わる全ての人を設計の深いところまで連れて行くことができるのです。この性質はビジネス領域とテクノロジー領域を行き来して進化していくドメイン駆動設計をスムーズに回していく助けとなるはずです。
それでは、筆者が自分の現場でどのように実践しているのかを順を追って説明していきます。
ステップ0. 要求を収集する
ソフトウェアを作るには、なにはなくともまずは「要求」が必要です。そのソフトウェアが何のために存在するのか、何を達成すべきなのかがわからなければ、どのような手法を用いても正しい道を歩むことはできません。要求の形式はExcelの文書からエクストリームプログラミングにおけるユーザーストーリー形式まで、各社様々だと思います。自分の今の現場では、PRD(Product Requirements Document)のフォーマットを作り、ビジネスメンバーに記入を依頼するという形で要求を収集しています。
ステップ1. とにかく図を描いてみる
要求を確認したら、次はいきなり概念図を描いていきます。要求文書の中に登場する概念(特に名詞や動詞)をグラフィカルに配置することで、現状の理解を整理する手助けになります。物理的にチームが集まれるなら、ホワイトボードと付箋を使うのもお勧めです。ただし、この段階では正しいドメインモデルを捉えることは不可能なので、一定時間で切り上げるのがよいでしょう。*1
たたき台だけでも概念図ができれば、そこに登場する概念から成る「用語集」が完成するはずです。これらの用語とその関係性を整理し、チーム内の認識を揃えておくのがこのステップの第一の目的です。
ステップ2. ドメインモデルの言葉でシナリオを記述する
用語集ができたら、いよいよユースケースシナリオを書いていきます。ユースケースシナリオはシステムとその外部に居る存在(アクター)とのインタラクション(相互作用)をステップ毎に記述したものです。システムを直接操作する人間や、システムを利用する他のシステム、システムからアクセスする外部システムなどがアクターになります。これらを認識するには、そもそもシナリオで言及している「システム」とはどこからどこまでを指すのかという設計スコープを定義しなければなりません。その辺りの話はいずれ別の記事で書きたいと思います。
シナリオの記述で最も重要になってくるのが、一般的な成功ルートと、エラーや任意の追加操作などの例外的なルートの両方を網羅的に記述することです。これが出来ていないと、プログラマがコードを書きながら例外ケースに気づく度にプロダクトオーナー*2の判断を仰いだり、もっと悪い場合はプログラマが勝手に例外ケースの対処方法を決めて実装してしまって、あとで問題になることもあります。ユースケースシナリオの記述とプロダクトオーナーによるレビューを通じて、外から見たシステムの挙動における論点や不確定要素を予め解消しておくことができます。完成したドキュメントそのものより、コーディングに先立って発生するコミュニケーションこそが真に価値があると言えるでしょう。
シナリオの網羅性を担保するには、書き方の形式を揃える必要があります。形式を揃えておけば、「このステップでこういうことが起きたらどうなるのか?」というツッコミをレビュアーが容易に行うことができるようになります。書き手は網羅的に書いているつもりでも、暗黙的な前提に立っていることがままあるため、未定義な部分は未定義であることが明らかになるような書式に揃えると良いでしょう。筆者の現場ではアリスター・コバーンの『ユースケース実践ガイド』を参考に、以下のようなフォーマットを使っています。
ユースケース名:領収書をダウンロードする 主アクター:ユーザー 支援アクター:なし 事前条件 - ユーザーがログイン済みである - ユーザーが「注文詳細」画面を閲覧中である 主シナリオ 1. ユーザーは「領収書作成」リンクをクリックする 2. システムは「領収書作成」画面を表示する 3. ユーザーは宛名を入力する 4. システムは入力を検証する 5. システムは注文から領収書を生成する 6. システムは領収書の内容を表示する 7. ユーザーは「ダウンロード」ボタンをクリックする 8. システムは領収書のPDFを出力する 9. ユーザーはPDFの保存先を選んでダウンロードする 副シナリオ 4a. 宛名が空文字の場合: システムはエラーメッセージを「領収書作成」画面に表示する 3に戻る 事後条件 - ユーザーのローカルマシン上に領収書のPDFがある - 注文の状態は変化しない
各項目の意味は以下の通りです。*3
項目 | 意味 |
---|---|
ユースケース名 | ユースケースを識別するための名前です。主アクターの目的を表す名前にすると良いでしょう。 |
主アクター | システムを扱う主体です。エンドユーザーやバッチを定期実行するスケジューラーなどです。 |
支援アクター | システムが依存する外部の存在です。連携している外部サービスなどです。 |
事前条件 | シナリオを開始する前の状態について記述します。ユーザーのログイン状態や開いているページ、事前に実行しておくべき他のシナリオなどです。 |
主シナリオ | シナリオのメインルートです。主アクターが最も一般的な操作を行い、それらが成功した場合のルートを記述します。 |
副シナリオ | シナリオの例外的なルートです。入力エラーや特殊な条件でのみ実行される追加処理、外部サービスへのリクエストの失敗時の動作などについて記述します。 |
事後条件 | シナリオを最後まで実行した場合、システムやアクターの環境がどう変化するかを記述します。データの挿入や更新、削除が主になるでしょう。 |
恐らくシナリオを書く過程で、用意した概念図の不備がいくつも見つかることでしょう。見えていなかった概念を追加したり、既存の概念を分割したり、実は不要だったものを削除したりしながらブラッシュアップしていきます。それと同時に用語集が更新され、ユースケースシナリオもより洗練されていきます。
ステップ3. 責務を割り当てる
シナリオを書いてシステムの挙動が明らかになったら、次はそれを実際のコードに落とし込むにはどうすればいいかを考えます。システムをオブジェクト指向言語で作っているならドメインモデルをそのままクラスにマッピングすればいいと思うかもしれませんが、手元にある概念図は抽象的すぎて、まだ現実のコードまではかなりの距離があります。特に概念図にはクラスの責務に当たるものが記載されていません。オブジェクト指向らしいコードを書くには、どのクラスが何を知っていて、何を行う責任があるのかという責務の配分を適切に行う必要があります。その検討のために役立つワークショップがCRC(Class Responsibility Collaborator)カードです。
このワークショップではシナリオを実行するために必要なクラスの候補と、そのクラスが持つ責務(プロパティとメソッドと言ってもいいでしょう)、そのクラスが依存するクラス(コラボレーター)をカードに1枚1枚記載してきます。可能なら物理的な情報カードを用いて行うのが理想ですが、Cacooなどのオンラインコラボレーションサービスを利用することもできます。
カードに収まりきらないほどの責務を書きたくなったら要注意です。そのクラスが責務を持ちすぎていないか、単一の概念をさらに切り出せないかを検討すると良いでしょう。また、ここで書くべきクラスは概念図に登場するものだけとは限りません。デザインパターンの適用を検討して、その実現に必要なクラスを導出することもあるでしょう。ドメイン駆動設計では、こうして生まれた新たなクラスをドメインモデルにフィードバックすることが特に重要になってきます。問題領域を表すモデルと実際に動いているコードを乖離させないためです。
ステップ4. シナリオのウォークスルー
さて、手元にはクラスの候補とその責務、コラボレーターが書かれたカードが揃いました。ここまで来ればコーディングまであと一息です。これらのクラスが本当にユースケースシナリオを実行する能力を持っているのかどうか、カードを使って実験してみましょう。自分がシステムになったつもりでステップを一つずつ実行していきます。アクターからの入力値を受取り、メモリ上にまずどのオブジェクトを生成するでしょうか? そしてデータベースにどういったパラメータで検索をかけ、どんなオブジェクトを保存するでしょうか? 順を追って考えていけば配分されていない責務や入力値の不足に気づくかもしれません。
シナリオを実行できるクラスと責務の組み合わせは一つとは限りません。深く議論すればするほど様々な候補が生まれてくることでしょう。よりよい設計に至るためには何パターンか試してみてから、どの組み合わせを採用するかをチームで話し合ってみましょう。「より良い設計」とは何かという疑問に対して明確な答えは誰も持ち合わせていないと思いますが、特に意識すべきなのは凝集度と結合度です。責務が多すぎるカードや責務が少なすぎるカードは凝集度の低さを、コラボレーターの多いカードは結合度の高さを表しているかもしれません。
ここまで来れば、ほとんど機械的にコードに落とし込むことができるようになっていると思います。ただし、全ての不確定要素を事前に議論するには多大なコストがかかります。どのステップもそうですが、完璧を目指すと分析麻痺に陥りかねません。最終的にはコードで語るというスタンスは常に持っておきたいですね。自分の場合は事前の設計で行き詰まったり、議論が空中戦になり始めたら、テスト駆動開発やモブプログラミングのアプローチでコードをこね回しながら、しっくり来るクラス設計を探索する方針に切り替えることがあります。
まとめ
この記事ではユースケースシナリオとCRCカードを用いてドメインモデルをブラッシュアップしつつ、コードにまで到達する方法を紹介しました。ドメイン駆動設計はビジネス領域の問題認識を抽出するだけでなく、設計と実装を通じて得られた知見を問題認識にまでフィードバックするのが面白いところです。そうしたポイントを実現する上で、自然言語を用いるユースケースシナリオは非常に相性が良い手法であると言えるでしょう。ビジネスメンバーを設計の深みにまで連れて行くことを意識し続けていると、やがてビジネスメンバーの口から詳細なアルゴリズムについての的確な指摘が出てきて驚くことになると思います。
実践する上で特別な知識や技術は不要ですが、もし理解を深めるために一冊だけ本を読むのであれば、レベッカ・ワーフスブラックの『オブジェクトデザイン』をお勧めします。絶版本なので入手が困難ですが、苦労するだけの価値はあると思います。
明日は今のところ空き枠で、明後日はabekoh (@abekoh_bcky) | Twitterさんです。 よろしくお願いします。