この記事は ZOZO #3 Advent Calendar 2021 2日目の記事になります。
バリデーションについて。普段使っているSpringFrameworkを例にとりますが、SpringFrameworkではBeanValidationの機能があり、フィールドにアノテーションを付与するだけでバリデーションが実現するというものです。
たとえばこんな感じ。
public class CustomerName {
@NotEmpty
final String firstName;
@NotEmpty
final String lastName;
}
SpringMVCであれば、リクエストで@Valueをつけることで自動でバリデーションを行ってくれます。
@PostMapping(
value = "/customers/cancel",
produces = { "application/json; charset=utf-8" },
consumes = { "application/json" }
)
public ResponseEntity<Response> cancel(@Valid CustomerName customerName) {
・・・
}
アノテーションを付与するだけでバリデーションしてくれるこの機能はすごい便利です。
ただ、このケースですとモデルをControllerから使う前提になり、Viewとモデルが密となる状態はいかがなものかというのもあります。
その場合、Viewのリクエストをモデルに変換し、Application層から明示的にバリデーションを呼び出す必要があります。
そのような使われ方だけならまだこれでいいのですが、このモデルが他にバッチとか、gRPCのサービスからとかからも使われるということになると、バリデーションの呼び忘れなんてこともあるかもしれません。
もっと強力に強制力をもって制御したいとなれば、BeanValidationだけだと心もとない。ということで、「契約による設計」を参考に事前条件を設定するという考えに至りました。
たとえばこんな感じです。
public class CustomerName {
final String firstName;
final String lastName;
public CustomerName(String firstName, String lastName){
this.setFirstName(firstName);
this.setLastName(lastName);
}
private void setFirstName(String firstName){
if(firstName.isEmpty()){
throw new IllegalArgumentException("firstName is required!");
}
this.firstName = firstName
}
private void setLastName(String lastName){
if(lastName.isEmpty()){
throw new IllegalArgumentException("lastName is required!");
}
this.lastName = lastName
}
}
コンストラクタから呼び忘れてれば同じじゃないかというのはありますが、このクラスに閉じているので最初に作ってしまえば、あとはどこから呼ばれても強制的にこの制約が働くことになります。APIのリクエストからも、DBからの復元も、Factory使うときでも必ず呼ばれます。
もちろんsetXXXはprivateなので他から呼ばれることはありません。
テストもしやすそうですね。
契約による設計については色々なところで述べられています。
DDDに絡めてであれば、実践ドメイン駆動設計の第5章エンティティの箇所を読むといいと思います。
もっと鈍器でよければオブジェクト指向入門の第11章が詳しいです。
アドベントカレンダー2日目は契約による設計お話でした。次の日も自分ですね。「schema_spyでER図を自動生成」というタイトルでお届けいたします。