はじめに
素晴らしい記事を読む ⇒ カッコイイ! ⇒ 採り入れる ⇒ 実際にやってみたらナゾだらけだぞ? ⇒ 理解した
の過程を共有します。
原本読んでないので、実践ベースです。間違ってたら指摘ください。
採り入れた範囲
Webアプリケーションのバックエンドに取り入れました。
図だとこんな感じです。
HTTP 呼び出し 呼び出し
フロント <----> コントローラー --> ユースケース --> リポジトリ
|
| 呼び出し
|
└> プレゼンター
- コントローラー -> WebAPIのルーター
- ユースケース -> ドメインロジック
- リポジトリ -> 外部へのアクセス
- プレゼンター -> PDF出したり
という感じで、ノリで作成しましたが
深まるナゾ
- プレゼンターって「表示」に使うなら、ここでの「フロント」ってどういう立ち位置なんだ?
- まあPDF作成はそれっぽいからいいとして。。。
- と、とりあえずコントローラーから帰ってきたデータを、Reactでパリッと表示しときゃええやろ。。。
- リポジトリからDBアクセスしてデータ返す時点で、どこまで絞リ込みしていいんだ?
- データ絞り込むしかないか。。。
- ドメインロジック入っちゃうやん。。。
- でも全件取得はパフォーマンスが。。。
- ここまでで搾り取って最後に残った「ドメインロジック」とは一体なんだったのか?ただの残りかすなのか。。。?
⇒ 深まるナゾ。。。となり、
ここで自分のやってきたことに不安を覚え始める。。。そして
DDDの記事に出会う
皆さんはソフトウェア設計をする際に、なにをしますか?
僕にとっての正解は、ドメインモデリングです。
というのは、この素晴らしい記事を読んで、そう思ったからです。
https://little-hands.hatenablog.com/entry/2018/10/08/goal-of-ddd
この時点で、モデリングした結果できたものは、Clean Architectureにおける「エンティティ + ユースケース」にほぼ同義なんじゃないか?とか考えだす。
そして、モデリングしたドメインを継続的に、ソフトウェアに反映し続ける。。。
この響き。。。ムム!?アジャイル!?お前アジャイルじゃないか!?
アジャイルと絡めて、考えだす
アジャイルな開発をやっているとよく出てくるのが、ユーザーストーリーとかペルソナとかのツールが出てきます。(実際はWFとかでも使えます)
ここで詳しく書くつもりはないんですが、あれらは要求分析ツールのようなもので、
ユーザー自身・顧客の中で、まとまっていない抽象的な要求、というか「想い」みたいな、ざっくりしたものを具体的にしていくときに使ったりします。
こうしてできたものが、ドメインにおいてソフトウェア化によって解決すべき問題になります。
要求からブレークダウンしよう
今まで、さっき述べたような「問題」が発生するたびに、僕はこう考えてきたわけです。
- ええと、その項目ってDBにあったっけっか
- どうやって取得しよう
- パフォーマンスは…
いわばリポジトリから作ってきたようなものです。
でもそうすると、リポジトリにGetAllData()
とかいう良くわからんメソッドを作ってしまうわけです。
ユーザーの「僕は、期限内のTODOをみたいんだけどなぁ」という要求を考えてみましょう。
もうちょっと突き詰めておきましょう。
僕「TODOの何を見たいですか?」
ユーザー「名前と発行日とあとそれから~~~」
僕「とりあえず最低限でお願いします」
ユーザー「名前と発行日は必須です!」
僕「名前だけじゃだめですよね?」
ユーザー「発行日がないと、さすがに使い物にならんわ。。。」
僕「そうっすね。。。今後さらに項目が必要なら、ユーザーストーリー考えましょう。」
ユーザー「はい。」
以上のやりとりは、ユーザーストーリーを分割したかったりするときにします。
分割すると、範囲が狭まる分、目的が明確になります。
目的が明確 = 「ドメインロジックの特化」
なので、ユーザーストーリーの分割とは実はドメインロジックの特化そのものなのです。
この部分が特化されていない=目的があいまいだと、ドメインロジックが複雑になってしまうので気を付けましょう。
コントローラーをつくる
ええと、僕=tanakaだとしたら、/tanaka/todo
とかで、tanakaの期限内のTODOが取れて、画面にリストで出るところまで考えたとしましょう。
コントローラーでは、HTTP通信のエンドポイントの定義と、パラメータのフォーマットチェックとかをして、問題なさそうならユースケースを呼び出します。
この時、複数のユースケースを使っていいかというと、たぶんいいんだと思います。ファサードっていうんですかね。
あと、ユースケースとかリポジトリでエラーをthrow
して、コントローラーでcatch
して、HTTPステータス400番代とかで返したりします。
これで、Input/Output/Error
というインターフェース3大要素をケアできました。
ユースケースをつくる
「期限内のTODOを取得する」ので、それをそのままメソッドにします。
IntimeTodo GetIntimeTodo(string userId)
を作ります。この時、Todo GetTodo(string userId, bool intime)
とか作ると失敗します。
intime=false
とかで「期限外」が取れるケースなんて考えるのはやめましょう。
たぶん欲しくなるのは「期限外」じゃなくて「完了済み」とか「期限切れ(完了は含めない)」とかですよね?
そのたびに、CompletedTodo GetCompletedTodo(string userId)
とかExpiredTodo GetExpiredTodo(string userId)
とか作りましょう。
IntimeTodo
とCompletedTodo
って、Todo
じゃだめなの?って思うかもしれませんが、恐らくIntimeTodo
には「完了日」が存在しないので、別々にしといてあげた方が余計な属性値の容量でネットワークを圧迫するリスクを回避できるかもしれません。
「Todo名・発行日」が必要なので、UseCase Output Portはこんな感じ。
namespaceTodoApp.UseCase.Todo{internalclassIntimeTodo{internalstringName{get;set;}internalDateTimeIssuedDate{get;set;}}}
YAGNI然り、「必要そうだから」という理由でドメインロジックのバリエーションを増やすと失敗します。
要らなかったものを作る=コード・テストパターンが増える=コストになってしまいます。
オーバーロードを活用したい人は、IntimeTodo GetTodo(Intime input)
とかにして、
namespaceTodoApp.UseCases.Todo{internalclassIntime{stringuserId{get;set;}}}
としてあげましょう。
言い過ぎかもしれませんが、ドメインロジックの汎用性のことは考えるのをやめてもいいくらいです。目的に特化しましょう。
目的が同じで使いまわせたら「ラッキ~!」くらいで十分です。たぶん。
リポジトリをつくる
Todo
クラスとList<Todo> GetTodo()
1を作る。以上。
namespaceTodoApp.Gateways.Todo{internalclassTodo{internalstringName{get;set;}internalDateTimeIssuedDate{get;set;}}}
でもそうすると、リポジトリに
GetAllData()
とかいう良くわからんメソッドを作ってしまうわけです。
とか言ってたくせに、全件取得して満足してみます。
だって今のところ、tanakaのTODOリスト全件取得するだけなんですから。
tanakaのTODOリストが全部で、1万件あるでしょうか?
もしそうなら、tanakaさんのペルソナに「俺は、多忙すぎてTODOが日に1000個追加される」と書き忘れたのは誰だ!とキレるところです。
あ、でもtanakaの分取りたいのに、suzukiのリストはさすがにいらないですね ⇒ GetTodo(string userId)
これなら、今後GetCompleted
とかGetExpired
とかユースケースが増えても、Todo
クラスを拡張して自動テスト流せば、GetIntimeTodo
は動作が保証されますね。
このようにリポジトリは、ユースケースとは逆に「汎化」を意識すべきだと僕は思います。
ですが、画面には1000件分のデータの統計情報しか要らないのに、全件取得だと10万件取ってくるような場合、パフォーマンスがままならないときはあります。
たとえば、全ユーザーの登録しているデータの中から、「指定した日付で発行されたTodoの情報を見たい」場合です。
「日付での絞り込み」は当然ユースケースの責務なのですが、もしかしたら1万人 × 平均50件=50万件のデータを全部とってきて、メモリ上で日付の絞り込みをかけるのは、パフォーマンス的によろしくないかもしれません。
そういった場合は、List<Todo> GetTodo(string userId)
とは別に、List<Todo> GetTodo(string userId, DateTime issuedDate)
という風に、引数で取れるデータをフィルターできるようなメソッドを追加してあげましょう。
ただ「完了日で絞り込み」メソッドを追加するとき、List<Todo> GetTodo(string userId, DateTime completedDate)
になり、オーバロードができないので、
List<Todo> GetTodo(string userId, IssuedDateFilter filter)
としてあげればよいかと思います。
namespaceTodoApp.Gateways.Todo{internalclassIssuedDateFilter{DateTimeissuedDate{get;set;}}}
また、上のような例の場合は、テーブルの「発行日」列にインデックスを張るのを忘れずにしましょう。
どうでしょうか。これで後から、
「なんで発行日の絞り込みのリポジトリ作ってあるの?」 → パフォーマンス劣化を防ぐためですよ~というリポジトリの責務が果たすべきDBへのアプローチを表現できています。
「発行日で絞り込みたいな~」→ これ使いな!つ List<Todo> GetTodo(string userId, IssuedDateFilter filter)
という風にできます。
え?間違えて全件取得の方を使ってパフォーマンスが劣化したらどうするか?って?
パフォーマンステストしろよ
ドメインの変化に強いアプリにするのだから、ドメインの変化を大事にしよう
大事にしましょう。
さいごに
最初に言った通り「Clean Architecture」の本は、読んでないのだけれども調べてみたら著者がすごい人でした。
Robert C. Martinさん
「セカンドボブ」という、某スポンジのようなあだ名を付けられた神です。
アジャイル宣言まで宣言しているうえにあの「SOLID原則」のネタ元となる原則をぶちかましまくった超人らしいです。2
僕なんて、この5つのうちDを理解するだけでも結構かかったのに。。。
きっと僕みたいな奴に対して、SOLIDの概念が伝わらなくて伝わらなくて悔しくてClean Architecture書いたのでしょう。ごめんなさい。最初疑っちゃいました。そしてありがとうございます。
よくよく考えると、SOLIDのDとかってきっと、リポジトリから作ってた今までの制御を逆転させて、上位モジュールから作っても、下位に依存せずにテストできるようにするテクニックですよね。
そう考えると、要求をブレークダウンするClean Architectureに対するアプローチは正しいんじゃないかなと思ってます。
こうしてClean Architectureを実践することで、SOLIDを理解させてくるあたり、さすがはボブおじさんです。
IEunmerableとか.NETユーザー以外にはわかりづらそうなので、Listになってます。 ↩