ユースケース図とドメインモデル図を使ったモデリング手法

松岡幸一郎氏:モデリングのやり方は1つに決まったものはとくにないんですが、リレーションシップ駆動要件分析(RDRA)やユースケース駆動分析設計など、いろいろと紹介されているものがあります。

これらもけっこういろいろな要件定義から踏み込んでできるようなフレームワークになっていて、DDDにおける実績もあるんですが、出てくる図の種類がすごく多かったりして、この説明とかを理解するだけでも、かなり時間がかかって、なかなかハードルが高かったりします。

これは私が実際にやってみて「これぐらいの簡単さだったらけっこう初心者の人もやりやすいかな?」というようなものを、私がいろいろ試行錯誤した中で見つけたものの提案となります。その1つとしてユースケース図とドメインモデル図の2つを使うモデリング手法がありますので、ここではそれを紹介します。

お題として、タスク管理アプリケーションにおける事例で説明したいと思います。

ユースケース図

作る図の2つのうちの1つがユースケース図です。これは一般的なUMLのものと基本的には同じと思ってください。何かというと、ユーザーとアプリケーションの相互作用を定義するものと言われています。

左側の棒人間みたいなものが「アクター」と言われるものです。「どういう種類の人が操作するか?」というユーザーの種類みたいなものを棒人間の下に書くのですが、その作業者という種類のアクターが、このソフトウェアでどんなことができるかというものを書いていきます。

例えばタスク管理のシステムだと、タスクを登録したり延期したり完了したりとか、タスクの担当者を設定したりタスクからカレンダーを登録したりと、「こんなことができるよ」ということをみんなでホワイトボードに書きながら図を起こしていったりします。

さらに、その次のステップでドメインモデル図を作るのですが、このときに「今回のモデリングのスコープはどこだよ」というのを枠で囲って決めたりします。

なぜこんなことをしなければならないのかには、理由があります。1つはユースケースの具体化・言語化です。

実際に文章にして具体化してみないと、どのようなモデルを作ればいいのかがわからない。例えば先ほどの例だと、一作業者として自分のタスクを管理したいのか、管理者として複数作業者のタスク状況を管理したいのかによって、同じタスク管理と言ったとしても必要なタスクというものが変わってきてしまいます。

これはつまりモデルというのは、問題を解決するために作っているので、どのような問題かが具体的にきちんと定義されないと、それに対する解決策も決めらないのです。

もう1つ、ドメインモデル図作成の作業の範囲を定めるというところでいうと、ドメインモデル図を作成していると、「こんなこともあるよね」「こんなこともあるよね」って、やってみるとわかるんですが、けっこういろんな要素が思い浮かんでしまってキリがなくなってしまいます。そこで、ある程度先にここで範囲を限定してしまって、限られた時間でまとまった成果を出せるようにすることが狙いとしてあります。

ドメインモデル図

ドメインモデル図は、この図のようなイメージのものです。クラス図の簡易版のようなものです。

クラス図には、属性とメソッドを書くと思うのですが、メソッドは書かなくてOK。属性も代表的なものだけでOKというふうにしています。特徴的なのが、ドメイン知識と呼ばれる、業務のルール・制約といったものを吹き出しで記述することです。

例えばさきほどのタスクでいうと、「タスクはどんな状態で作られるか」というと、「未完了状態で作られますよ」と。「タスクの延期に関するルール」ってどうしようかとなったときに、「3回ずつ」「3回だけ1日ずつできますよ」というものとか。それから、タスクの担当者としてどういうものを決められるかなというルール・制約があるかを考えると、「今アクティブなユーザーの人にだけ設定できるようにしたいよね」とか。そういうようなことです。非活性化されていないユーザーに担当をつけられるというふうな……。

というようなことをみなさんで話し合いながらホワイトボードに書きながらこのルールを決めていきます。それを吹き出しに書いていきます。

集約について決める

そして集約というものをご存じない方は、今聞き飛ばしていただいていいんですが、ご存じの方は聞いてほしいです。集約はこのタイミングで決めてしまうと非常によいと思います。

なぜかというと、ドメインモデル図を作ったあとはもう、直接実装にすぐ落とすんです。そのときに集約が決まっていないと、リポジトリはどの単位で作るかとか、オブジェクト間の参照の方法とかが変わってくるので、集約をここで一緒に決めてしまうと、実は、実装におけるいろんな意思決定がここでできちゃうことになります。ですから集約は、ここで決めると非常によいです。

集約を知らない方のためにさらっとだけご紹介をすると、「必ず守りたい強い整合性を持ったオブジェクトのまとまり」を「集約」として定義します。必ずまとめて取得して、まとめて更新する単位です。

また、別の機会で記事とか書けたらいいかなと思っているのですが、いったんはこれくらいの理解で大丈夫です。ちなみに本の3章か4章ぐらいで解説しているので、お手元にお持ちの方はそちらを見ていただければと思います。

実際は、最初からこんなにきれいに整った図を作るわけではなく、本当にホワイトボードで殴り書きでやることをおすすめしています。

なぜかというと、最初はけっこうもう本当に第1版の下書きようなものなので、いきなりきれいなものになることはなくて、いろんな人がいろんなアイデアを出しながらオブジェクトの切り方とか吹き出しのルールとかを、参加者の頭の中にある違うそれぞれのものを一緒に表していくフェーズがあるので、そこはもう書いたり消したりをスピーディにやったほうが非常にいいのです。最初はとにかく殴り書きでOKというか、殴り書きから進めることをおすすめしています。

ちなみにですが、手書きしたものは、PlantUMLというテキストでクラス図というかこのような図を作るツールがあるので、そこに清書してGit管理していくことをおすすめしています。

どうやってコードに落とすか

では、次にいきましょう。実際に、どうやってコードに落とすか、やっていこうと思います。

少しシミュレーションしてみたのですが、いきなりコードからいってしまうと、オンラインだと画面の移り変わりとかで置いてけぼりになってしまうことがありそうだったので、このスライドのほうで一度やったやつをあとでおさらい的にライブコーディングするというかたちで進めたいと思います。よろしくお願いします。

アーキテクチャ設計になるのですが、ここをいろいろ考えてしまうとなかなか次に進めないので、こういうようなアーキテクチャで進めましょうというのをまず決めてから進めていくことをおすすめしています。

ここで紹介しているのは「オニオンアーキテクチャ」というものです。今回フォーカスしていただきたいのは、真ん中のアプリケーション層というところと下にあるドメイン層というところです。このクラスを実装していきます。真ん中は、今回の本の中ではユースケース層と言っていますが、「アプリケーション層」と「ユースケース層」は同じものとして受け取ってもらえると。

流れとしては、改善余地のあるコードから入ってリファクタリングしていくという進め方でいきます。なので最初のコードは、「ん?」って思われるかもしれませんが、それはそういうつくりになっていると思ってください。

各層のクラス

最初は、タスク管理のTaskクラスです。ちなみにサンプルコードはJavaで進めます。Javaのこのアノテーションの「@Setter」「@Getter」というのは、Lombokというライブラリを前提としたものです。このアノテーションをつけると、各項目にgetterとsetterが生えるようになっています。

このドメイン層のクラスがタスクを表すクラスで、タスクのIDとタスクのステータス。完了・未完了とかですかね。nameがタスク名で、dueDateがタスクの期日。postponeCountが延期した回数ですね。この段階では属性の定義のみをしていて、ドメイン知識、ルール・制約はまったく持っていないようなコードになっています。

今度はこれがアプリケーション層。ユースケース層にあるTaskApplicationServiceというクラスの中に、タスクを作成するメソッドを書いています。引数に名前と期日。一応name(名前)とdueDate(期日)はnullチェックし、問題があれば例外を投げるようにしています。雑にIllegalArgumentExceptionを投げていますが、実際はもうちょっと決めた例外を投げることになると思います。

最初にデフォルトコンストラクタでnew Task()でインスタンスを作って、そこに今回の処理用の属性を1個1個入れていきます。これはActive Recordとかを使っているとよくあるコードだと思います。はじめにTaskStatusで未完了の設定をして、タスク名を引数のものを設定して、期日も引数の期日を設定して、postponeCountは最初なので0。それでsaveしますというような実装になっています。

お気づきの方はいるかと思いますが、あまりよくないコードなので、ここから変えていくことになります。「@Setter……ん?」って思う方。きっと感度のいい方は、「@Setterもつけちゃうの?」というコメントがYouTubeにあったりします。

ちなみに、@AutowiredというのはJavaのSpringの技法で、ここにDependency Injectionで関係したインスタンスがインジェクトされることになっています。

延期時の処理もこのようなかたちで似たようなものです。postponeTaskというタスクを延期するようなメソッドがあって、taskIdだけ引数にするようなものになっています。

1回リポジトリから取ってくるというのも、普通の実装とは少し違うところがあるかもしれません。ここでは正解のかたちに一歩踏み出してはいるのですが、リポジトリからID指定でTaskインスタンスを取ってきています。ここでは今までの延期回数を取得して、定数になっている延期回数の最大数を超過していたら例外を投げるようにしています。

期日は、自分自身のgetDueDate()で取ってきて、1日足したものをまた改めてsetDueDateで足しています。setPostponeCountも、自分自身のPostponeCountをgetして1足して、改めてsetPostponeCountする。そうして保存するという仕組みになっています。Active Recordを使っていると、こんな実装になるのではないでしょうか。

これでも普通にシンプルだったらなんとかなるのですが、不整合なデータをいくらでも作れてしまうというのが問題です。

例えば、createDoneTaskという上のほうのメソッド。引数に名前とdueDateを受けて新しくタスクを作るメソッドなのですが、赤く囲われている1つ目です。setTaskStatusにTaskStatus.DONE。完了状態のタスクを作れてしまう。なぜ作ったばかりなのに完了状態なのかって、意味がなかなか取れないですよね。setPostponeCount(-1)というなんだか裏技みたいな、延期した回数が-1ってなんか意味が取れなかったり、そういうような変なインスタンスが……。

また、下のほうのchangeTaskというメソッドを見てもらうと、元々はタスクのIDを指定して、1日延期をして、延期回数もインクリメントするようなメソッドだったのですが、1日ずつというルールを破って、引数に来たdueDateで設定してしまっています。さらに延期回数のpostponeCountもアップデートして増やしていないので、無限に延期できてしまいます。

ドメインモデル図に書いた吹き出しを活用する

それではこうしたコードを、どう改善していけばよいかということなんですが、ここで、ドメインモデル図に書いた吹き出しを機械的に使えます。

この図に書いた吹き出しの知識がどこに書かれているかを見ていきます。そうすると、「このタスクが未完了状態で作成される」とか「タスクは3回だけ1日ずつ延期できる」という、これはドメインモデル図にあるので、ドメインの知識、ルール・制約になるのですが、そこが先ほどのアーキテクチャでいうどこの層に書かれているかというのを見てみようと思います。

そうすると、ここですね。この吹き出しに書かれている新規作成時のルールとして、タスクは未完了状態で作成されているもの。これはまさに先ほどのドメインモデル図に書いてあったものなのですが、ここがどこのレイヤー(層)に書かれているかというと、やはりドメインモデルの知識なのでドメイン層に書きたいのですが、こうしてみるとアプリケーション層に書かれていることがわかると思います。

ここでアプリケーション層の右下を見てもらうと、このTaskApplicationServiceというのはアプリケーション層のメソッドなんですよね。つまりアプリケーション層のところにドメインモデル図の知識が書かれていることがわかります。

もう1つのPostponeTaskというところもApplicationServiceの中でやっているので、「タスクは3回だけ1日ずつ延期できる」というドメインのルールもアプリケーション層に書かれていることがわかります。

図にしてみると、このようなことになっています。先ほどのドメインモデル図の吹き出しに書いてあったものがアプリケーション層に書いてあります。

これをどうするかというと、こうです。ドメインモデルの知識はドメイン層に移すことが基本的な方針になります。

Taskクラスのbeforeとして修正前のクラスを再度見てみると、このクラスは、先ほどのgetter/setterと属性しかないメソッドなのですが、ここのクラスに吹き出しの知識を移譲していきたいと思います。

ドメイン層に移譲すると抽象度の高い実装だけが残る

まずコンストラクタです。コンストラクタの中にタスク、さきほどはデフォルトコンストラクタだけだったのですが、Stringの名前と期日を受けるコンストラクタを作りました。この赤字のところでnullチェックなどをして、インスタンスの初期状態の値を設定しています。

そうすると、名前と期日は引数で来たものなのですが、未完了という初期状態になります。それに加えて、延期回数も0という初期の設定を、このコンストラクタの中で定義できました。

もう1つ、postponeTaskという先ほどアプリケーション層にあったメソッドをドメイン層のクラス、Taskクラスのpostpone()メソッドの中に入れてやります。postpone()は引数なしで呼び出すメソッドになっていて、ここで自分自身のpostponeCount、これまでの延期回数を延期最大回数と比較し、ダメだったら例外を投げ、大丈夫であったら1日を足して処理を進めます。こうしてpostponeCountも間違いなく増やして処理を終えられます。

ここでsetterを消すのも、非常に大きなポイントです。ここではわかりやすいように@Setterをコメントアウトしているのですが、setterを消します。setterを消すと公開しているメソッド以外操作ができなくなるという非常に強い制約をかけられるわけです。

これを呼び出しているもとの先ほどのアプリケーション層のクラスはどうなるかというと、これは作成と延期を合わせてもこれだけのメソッドになって、非常に抽象度の高いコードになりました。 createTaskだと、「タスクを作ります」「タスクを保存します」という実装だけ。延期のほうでいうと、「タスクを取得します」「タスクを延期します」「タスクを保存します」。ユースケース記述でいう自然言語で書くような粒度と同じような、非常に抽象度の高い実装だけが残ります。

つまり、「やることのWhatだけ」をこのユースケース、Applicationのクラスで実装するのですが、「Howの部分」はドメイン層のほうに全部移譲していることになります。

setterを非公開にしたことによって、先ほどのchangeTaskという何でも引数に入れた期日とtaskStatusで更新するようなメソッドが使えなくなります。ここで言うと、setDueDate、setTaskStatusという、このようなメソッドがもう存在しないことになるので、これがコンパイルエラーになって、もうそもそも実行ができないということで、まず書いた段階で気づけます。setDueDateとしようとしたら「そんなことはできないよ」と気づけるわけです。

動作の挙動、仕様がひとめでわかるようになる

少し小さくて見づらいかもしれませんけれども、Taskクラスを1個見ただけでこのようなクラスになります。ほかのルールで生成・更新されないことが確実になっています。また、この1クラスだけで、吹き出しの知識が凝縮されています。

先ほどの「問題がある」というところで説明し忘れてしまったので戻しますが……。ここですね。この上から2番目の問題点のところです。

上から2番目に書かれている、「仕様を追いかけるのに、複数クラスをコード参照から追っていくしかない」というところです。

前のクラスでsetなんとかが全部公開されている状態だと、「setするパターンって何パターンあって、それで全部正しいんだっけ?」と確認するときに、setterの参照を全部辿っていくしかなくなってしまうんです。

setterを全部辿っていくのが3個5個とかだったらまだ大丈夫だと思うんですけど、それが10個20個あったときに、「その全体どういうパターンで全部正しいんだっけ?」というところを追うのは、やはりなかなか工数もかかるし難しくなってきますし、そうするとバグりやすくなってくるというのは、みなさん経験がおありかなと思います。

それがこのリファクタ後のコードだとどうなるかというと、この1クラス見るだけですべての遷移パターンが把握できるんです。先ほどだとsetterを全部1個1個追っていかないといけなかったものが、1つのクラスを見るだけで生成と内部状態の変更というパターンがすべてわかります。

ここで見てみると、Taskというコンストラクタで引数と期日を受けて、先ほどお見せした正しい初期状態をセットしたコンストラクタがあります。

Javaの場合、自前のコンストラクタを書いた場合はデフォルトコンストラクタがなくなるので、先ほどのデフォルトコンストラクタを使った初期化ができないことがわかります。なので、コンストラクタが1つしかないことが、このクラスを見ると1クラスを見るだけで確実に保証されていることになります。

また、延期処理のメソッドもpostponeメソッドが1つですが、上の@Setterがないので、コンストラクタで作った値を変えられるパターンが1個しかない、もう確実に1つしかないのがこのクラスを見るだけでわかる。判断できるわけです。

つまり、「このクラスを生成するパターンはいくつで、状態を変えるパターンはいくつですか?」と言われたときに、この1クラスを見るだけで「1つです」と確実に言い切れます。これはコードを追っていくときには、めちゃめちゃ役に立ちます。このクラスを見ればよいという大きな安心感を得られます。

また、先ほどの吹き出しに書かれていたものがこのコードにまさに書かれて、コードで表現されているので、あの吹き出しの知識をここから再現することも非常に簡単にできます。そういう大きなメリットがあります。

層を決めることは、コード規約としても使える

この設計自体のメリットとして、レイヤー(層)によって書くべきことが決まっているため、コード規約のように定めて実装やレビューのときに使えるという点が挙げられます。

例えば、レビュアーとレビュイーの間で「この設計がいいと思うんですけど」「いや、これのほうがいい」みたいな感じで、「俺の考えた最強の設計」同士が戦ってしまうみたいな場面があります。プルリクのコメントがめちゃめちゃ伸びるみたいなこともあるかと思うんですけれども……。

「このレイヤーにドメインモデル図に書いた知識を書く」というルールにしてしまえば、さきほどのドメインモデル図を機械的に使えるんです。

「この図に書かれている知識って、今どの層に書かれているのだっけ?」と見たときに、「アプリケーション層/ユースケース層だね。それならドメイン層に移さなきゃいけないよね」というのは、もう意見のぶつかり合いというか、客観的にコード規約のように使えるので、非常に全体的に……自分で設計・実装するときにもレビューするときにも客観的に使える指標になるので、非常に使いやすくて、全体の設計としての規約・規律を整えられるようになります。