Clean Architectureにおいてバリデーションはどこでやるべきか
Tweetクリーンアーキテクチャで web アプリケーションを作る際に、バリデーションはどのレイヤの責務なのか?と悩むことが多いため、それについての考察を行ってみる。
あと、バリデーションについて書いてたはずがドメインロジックとアプリケーションロジックの違いについても結構言及せざるを得ない感じになったので、そのへんの話もしてみる。
結論から言うと
バリデーションはどのレイヤの責務なのか?という問い自体が間違いであり、レイヤごとにそのレイヤの責務となるバリデーションを行うべき、というのが今のところの結論。
バリデーションという単語は意味があまりに広い。「意図していないもの/ことを防ぐ」ことはすべてバリデーションと呼ばれている節がある。そのことにより、バリデーションというのはあたかも唯一つの責務であるかのように錯覚しがちだが、そうではない。クリーンアーキテクチャではレイヤによって責務を分担しているが、同様にバリデーションについても、
それぞれのレイヤごとに、それを受け付けてしまうと自分のレイヤの責務を果たすことができなってしまうような input がやってきた場合は弾くようにするというのがバリデーションである。 なので、バリデーションはレイヤごとに存在するし、バリデーションの責務や意味合いはレイヤごとに異なってくる。
そして逆に言うと、とあるレイヤで、そのレイヤの責務以上のバリデーションをしてしまうようなお節介をすべきではない。そのようなことをすると何らかの不都合が発生してしまうことが多い( 後述)。
バリデーションは単なる仕様
バリデーションという言葉に引っ張られて、クリーンアーキテクチャの枠から外れてバリデーションを特別扱いして考えてしまってはいけない。バリデーションも単なる仕様であり、仕様の部分集合のことをわざわざバリデーションと呼んでいるだけに過ぎない。「クライアントは JSON 形式でリクエストできる」「ユーザーは任意の文章を投稿できる」といった仕様があればまた、その裏返しとして「JSON 以外の形式は受け付けない」「本文が空白の場合は投稿できない」といったバリデーションが必要というだけの話である。 システムが、自身に言いつけられた仕様や責務を果たそうとした結果の副産物としてバリデーションが発生する。
ここまでで本質的な部分のお話は終わってしまった感があるが、ここから先は、具体的にクリーンアーキテクチャにおいてどの層でどういったバリデーションをすべきかについて考察していく。
Frameworks & Drivers 層におけるバリデーション
この層はクリーンアーキテクチャにおいて一番外側に位置する層であり、自分たちで実装する分量は少ないが、この層でもバリデーションが行われる場合がある。
たとえば nginx は、リクエストのデータが大きすぎる場合に HTTP Status Code として 413(Entity Too Large) を返す設定があるが、これも一種のバリデーションと言える。あとは、SSL/TLS におけるリクエストの改竄チェックとかもバリデーションと言っていいのかもしれない。このレイヤにおけるバリデーションは、OS やハードなど技術的詳細に起因する制限や、最低限のセキュリティの担保というのが主な役割になりそう。
Interface Adapter 層におけるバリデーション
Interface Adapter の責務は、アプリの内外のデータ形式の変換である。たとえば、外部から渡ってきた HTTP Request の Body に入っている JSON 文字列を、辞書型のオブジェクトなどにデシリアライズして適切な UseCase に渡す、といった処理である。
ここで、たとえば Request Body が壊れており JSON のフォーマットになっていない場合などは、内外のデータの変換が完遂できないのでエラーとするのが一般的であると思うが、これも一種のバリデーションエラーといえる。やりとりのためのプロトコルが守られていないことを検知して弾いていることになるので、立派なバリデーションエラーである。Interface Adapter 層におけるバリデーションは、「システム内外のやりとりのためのプロトコルが守られているかのチェック」であるといえる。
逆に、正しくオブジェクトにデシリアライズできたのであれば、Interface Adapter 層ではそれ以上のチェック、たとえば文字数や数値範囲のチェックは不要である。なぜならそれをやると、内外のデータ形式の変換という役割から逸脱してしまうからである。
Application 層と Domain 層それぞれにおけるバリデーション
Application 層1にはアプリケーションロジックが書かれ、Domain 層2にはドメインロジックが書かれる。
ところで、アプリケーションロジックとドメインロジックの違いは?という問いについて、なかなか直観的かつ解釈にブレが生じづらい表現が難しいという問題があるのだが、ここでは一旦以下のような定義として話を進めさせてもらう。
-
アプリケーションロジック: システム都合、アプリ都合のロジック。システムやアプリの形によりけりなロジック。システムやアプリという形になっているために存在しているロジック。ユースケース特有のロジック。例としては、送金サービスにおける、永続層( Repository)を介してのデータの保存/取得や「出金した際にはメール通知を飛ばす」といった仕様など。
-
ドメインロジック: ドメインを表現するロジック。システムやアプリの形によらず存在しうるロジック。扱いたいドメインがシステムやアプリという形になっていなかったとしても存在しうるロジック。ユースケースによらず成り立つロジック。例としては、送金サービスにおける、「残高以上の金額の送金はできない」といった仕様など。
※ なお、アプリケーションロジックなのかドメインロジックなのかは、「そのシステムにおいてそもそも何をドメインとするか」で変わってくる可能性があることは注意。このへんの性質が、両者の違いの言語化が難しい要因のような気がする。。
では、Application 層と Domain 層それぞれにおけるバリデーションについて、具体例で考えてみる。まずは以下のようなバリデーションについて考える。
ECサイトにおいて、サーバーの処理能力に限界があるため、一度に注文できる上限個数を99個に制限する。
先程の定義に照らし合わせると、これはアプリケーションロジックと言える。 ここでの 99 個という上限はシステム都合の仕様であり、ドメイン自体の表現に関わるロジックではない。なのでこれはアプリケーションロジックだし、Application 層に書くべきであるといえる。
では次はどうだろうか。
ECサイトにおいて、1人あたりの購入上限個数が決まっている限定セールを行う際、一人が注文できる上限個数を3個に制限する。
これはドメインロジックと言える。 このバリデーションがなかった場合には限定セールというドメインが成り立たなくなるためである。ドメインロジックなので、Domain 層に書くべきであると言える。
上記 2 つの例を見ると分かる通り、ややこしいのは、 入力数値のバリデーションひとつ取っても、アプリケーションロジックになる場合もあるしドメインロジックになる場合もあるという点である。 文面的には両者はほぼほぼ変わらないため、この事実は見落とされやすい。とあるバリデーションについて Application 層に書くべきなのか Domain 層に書くべきなのかの見極めは慎重に行う必要がある。
余談
twitter の 140 字制限は果たしてどっちなんだろうか。どっちかというとアプリケーションロジック寄りな気もするが、文字数制限が厳し目なのが twitter という概念の根幹な気もするので、ドメインロジックな気もする。やっぱりアプリケーションロジックとドメインロジックの明確な境目を一言でバシッと定めるのは難しい。。
ちょっと脱線したが話を戻していく。
バリデーションを書く場所を間違った場合にどうなるか
ここまで、各層においてどんなバリデーションをすべきかについて述べてきた。 では、とある層がお節介をしてしまい他の層でやるべきバリデーションまでやってしまうとどうなるのかについて考えてみる。
内側の層でやるべきバリデーションを外側の層でやった場合
このパターンは割とデメリットがわかりやすいというか自明かもしれない。本来もっと内側の層でやるべきバリデーションを外側の層でやってしまうと、複数箇所に同じようなロジックがコピペ的に増えたり、必須のはずの処理の抜け漏れが発生しやすくなるということが考えられる。
ではこの逆のパターンだとどうなるか。
外側の層でやるべきバリデーションを内側の層でやった場合
たとえば文章投稿 SNS において、「サーバーの処理能力の関係で、書き込みの際の文字数は 10000 文字までとしておく」というバリデーションについて考える。 ここで、10000 文字という制限はシステム都合の上限値であるため、アプリケーションロジックと言えそうである。
このようなアプリケーションロジックを、より内側のドメイン層に書くとどうなるか。 投稿文章エンティティのコンストラクタで文字数のバリデーションを行った場合に何が起きるか考える。
最初のうちは問題にならないかもしれないが、たとえばもしあとで、「サーバーの負荷がきつくなってきたので、5000 文字に制限を引き上げる」といった変更が入った場合について考える。そうなると、既存の投稿を永続層から取り出してエンティティとして復元しようとしたときに、5000 文字を超えている既存の投稿についてはコンストラクタのバリデーションに引っかかってエラーになり、復元できなくなってしまう。 また、この変更を決断した時点で、文章投稿 SNS のドメインに変化があったわけではない。変化したのはシステム(アプリケーション) の状況だけである。にもかかわらず、ドメイン層に書かれたロジックを変更しなくてはいけないという、不自然な状態に陥ってしまっている。
これは本来アプリケーション層にあるべきロジックをドメイン層に書いてしまったがゆえに起こってしまった事象であると言える。 アプリケーション層に書いておけば、バリデーションが走るのは新規投稿に関するユースケースのみに限るようにすることができ、既存の投稿には一切影響を与えずに新規の投稿についてのみ文字数制限を厳しくすることができたはずである。
このように、本来もっと外側の層でやるべきバリデーションを内側でやってしまうと、不要に強すぎる整合性に困らされる場面が出てくると考えられる。 また、一般に内側の層のクラスになればなるほど他のクラスから依存されることが増える傾向にあるため、内側にバリデーションを書きすぎるとその分依存する側でのエラーハンドリングのコードも増えてしまう。内側になればなるほど、バリデーションは本当に必要なもののみを最小限にとどめて書くことが重要であるといえる。
まとめ
こうしてみると、どの層も責務は違えど、自分の責務を果たそうとした結果としてバリデーションが発生していることがわかって、当たり前といえばそうではあるけど面白い。システム設計に関するプラクティスはどれも元を追えば、「各自自分の責務だけを忠実に果たすようにする」という単純な原則に帰結するのかもしれない。
意識的にバリデーションを書こうとするのではなく、各レイヤに適切な責務を持たせようとした結果として自然とバリデーションロジックが書かれるような設計実装の方針が理想と言えそうです。