Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8899

Clean Architectureの実践から多くを学んだ記録

$
0
0

はじめに

素晴らしい記事を読む ⇒ カッコイイ! ⇒ 採り入れる ⇒ 実際にやってみたらナゾだらけだぞ? ⇒ 理解した

の過程を共有します。

原本読んでないので、実践ベースです。間違ってたら指摘ください。

採り入れた範囲

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)とか作りましょう。

IntimeTodoCompletedTodoって、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を作る。以上。

Todo.cs
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を理解させてくるあたり、さすがはボブおじさんです。


  1. IEunmerableとか.NETユーザー以外にはわかりづらそうなので、Listになってます。 

  2. SOLID原則は、セカンドボブの出した原則のサブセット(部分集合)らしいです。 


Viewing all articles
Browse latest Browse all 8899

Trending Articles