バリデーション解体新書
Always-Valid Domain Model
考え方は昔からあるが、この言葉の出自はVladimir Khorikovさんだと思われる。
要は
> ドメインオブジェクトは、Validな状態でしか生成・存在できないようにしよう
というもの。
Validな状態でしか生成できないようにする
会員のドメインモデルを考える。属性としてemailAddressを持っていて、これは通知メールを送るのに使われる。
public class Member { String emailAddress;}public class RegisterMemberUseCase { private SaveMemberPort saverMemberPort; void reister(RegisterMemberCommand command) { // これを呼び忘れると大問題 if (!command.getEmailAddress().matches("^\\S+@\\S+\\.\\S+$")) { throw new IllegalArgumentException(); } Member member = new Member(command.getEmailAddress()); saveMemberPort.save(member); }}ドメインモデルを生成するのに不正なメールアドレスを渡さないようにする責務が利用側にある。
こうなると、バリデーションを忘れると不正なメールアドレスを持ったMemberオブジェクトが生成されてしまう。
class Member { String emailAddress; public boolean isValid() { return emailAddress.matches("^\\S+@\\S+\\.\\S+$"); } }バリデーションをドメインオブジェクトに移したところで、isValidを呼ぶ責務は依然として呼び出し側にある。
そこで、そもそも不正な状態でオブジェクトを生成できないようにする。
public class Member { String emailAddress; public Member(String emailAddress) { if (!emailAddress.matches("^\\S+@\\S+\\.\\S+$")) { throws new IllegalArgumentException(); } this.emailAddress = emailAddress; }}属性がアトミックでないとAlways-Validにできないこともある
Eメールアドレスが検証済みでないと通知が送れないとする。これを実現するために先のモデルに、Eメールアドレスが検証済みかどうかのemailVerified属性を追加する。そして、検証用のサービスVerifyEmailServiceを用いて、検証がOKであれば、
public class Member { String emailAddress; boolean emailVerified;} public interface VerifyEmailService { boolean verify(String emailAddress);} public class VerifyEmailUseCase { private VerifyEmailService verifyEmailService; private SaveMemberPort saveMemberPort; public void verify(VerifyEmailCommand command) { if (verifyEmailService.verify(command.getEmailAddress())) { Member member = new Member(command.getEmailAddress(), true); saveMemberPort.save(member); } else { throw new FailToVerifyEmailException(); } }} こうなるとEメール自体と、EメールをVerifyするアクションとその結果がバラバラになって、ビジネス的整合性を保つ責務が、またもや呼び出し側に行ってしまう。すなわちVerifyを呼び忘れると、Eメールアドレスが未検証なのに、検証済みフラグがONのMemberオブジェクトが作れてしまう。
これを防ぐには、未検証のEメールアドレス(UnverifiedEmailAddress)と検証済みEメールアドレス(VerifiedEmailAddress)の2つの型を定義する。検証済みEメールアドレスの型は、EメールのVerifyアクションを通じてしか生成できないようにし、メール送信機能においても検証済みEメールアドレスのみを受け付けるように書いておけば、誤って未検証Eメールアドレスにメール送信してしまう事故を防げる。
public abstract class EmailAddress { String value;} public class VerifiedEmailAddress extends EmailAddress { VerifiedEmailAddress(String value) { super(value); }} public class UnverifiedEmailAddress extends EmailAddress { public UnverifiedEmailAddress(String value) { super(value); }} public class Member { EmailAddress emailAddress;} public interface VerifyEmailService { VerifiedEmailAddress verify(UnverifiedEmailAddress emailAddress);} public interface SendNotificationService { void send(VerifiedEmailAddress emailAddress);}というように、常にValidな状態を保つには、
なんでも入る緩い型
暗黙的な複数の属性の関連
を排除し、型によって業務ルールを表していくこと(Design with types)に他ならない。