システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

Page/Formのvalidateを見直す

vermeer.hatenablog.jp

vermeer.hatenablog.jp

で考えたことに対して、実際の実装を経て 妥協(?)した

vermeer.hatenablog.jp

を踏まえて、改めて整理をした方が良さそうに思い 考えてみることしました。

はじめに

考え直してみようと思ったことを呟いたもの。

振り返り

  • Pageは、Formの集約ルート
  • Formは、DomainObjectをPresentation用に包含したクラス
  • ScopeはPage単位で制御
  • Pageは、Entityへの変換をする
  • FormとDomainObjectにValidationを定義してControllerで検証
  • Pageは、Formを管理するPOJOとしたい

ざっくりいうと、Viewのデータ管理と変換をするPOJO ということを役割にしようと思いました。 ですが「FormにValidationを定義してControllerで検証」が 想定通りには出来ませんでした。

CDIインスタンスから値を取得するためには、アクセッサ(getter)を使わないといけない」というところが理由です。

対処案としては

  • Pageクラスにvalidateメソッドを準備して それを使う
  • 検証用のアクセッサ(getter)を準備して@Validを設定する
  • 検証用に新たにインスタンスを生成する

いずれかで、できそうでした。

「Pageクラスにvalidateメソッドを準備して それを使う」は そもそもvalidateはControllerで行いたいということで採用しませんでした。
理由はValidatorをPageクラスにDIしないといけないから、私の想定しているPOJOの範疇を超えている気がしたというのも理由です。
「検証用のアクセッサ(getter)を準備して@Validを設定する」を採用しなかったのは、 検証用のアクセッサというのが、どうもしっくりこないというか アクセッサが多すぎるというか、それだったら「検証用に新たにインスタンスを生成する」で 役割を明確にしたインスタンスを返却するということで まとめた方が可読性が良いかな?ということで一旦 整理をしました。

考察

「検証用に新たにインスタンスを生成する」で、一旦 良し としたのですが、フィールドの並び順の指定など、Viewのデータ管理を Pageクラスで 実装していくことで「Pageクラスではなく 検証用のインスタンスに 関心事が集約されている」という状態になってしまっていると感じるようになりました。

となると、有力な候補となる実装は validate(this) を使った やり方です。

あとは、JSFらしく作って、インスタンス生成メソッドを準備するやり方でしょうか。
発想としては検証用としてインスタンス生成をするのであれば、いっそのことプロパティをStringで管理してしまおう、と。
どうせ割り切るのであれば、これくらい割り切ってしまえ という発想です。

再考察

validate(this) をPageクラスに含める(ValiatorをPageクラスへDI)ことになるわけですが、元々は Pageクラスは 限りなくPOJOにしたい、validateはControllerで行いたいと考えていました。
こじつけであれ、方式を変える際には 自分なりの論拠を整理しておきたいところです。

さて、そもそも 私の考えていたPOJOは 当初案の時点でも達成出来うる方式だったでしょうか?
改めて疑いの目でPageクラスの実装を見て、今更ながらに気が付いた事がありました。
JSFの実装に依存する記載があります。
これは、最終的には infrastructure層とDIで解決するつもりで 先送りにするつもりだった実装です。
そうなんです、DIしないようにしたい、という発想での設計は、そもそも実現できていなかったのです。。

他にも、設計として 中途半端というか 統一されていないと感じるところがありました。
Controllerで、Validateをするためだけに、Pageクラスの内部情報をわざわざ公開して、しかもやることは validateに渡すだけという DTOなんだか ViewのEntity的役割のものなんだか、どこを目指しているのか まとまりのない実装になっていることに気が付きました。 これは、動詞を現すクラスで 検証はした方が良い、という私の思い込みも原因の1つです。
Entityを生成(Page/Formから変換)をするのであれば、不変条件は満たしていることを保証するべきでしょう。
例えば、SpringBootだったらリクエストパラメータでValidateをするように。

つまり、PageクラスにEntity(DomainObject)への変換をする役割を与えるのであれば、その検証論理だけでなく、検証実行も 防御的にすれば良かったのです。。

結論としては、こじつけでもなんでもなく、Pageクラスにて Entity(DomainObject)への変換をするのであれば 不変条件検証も もれなく実施すべきであるので validateはPageクラスでする、というだけだったということです。

今 思えば、なんで考えが行き届かなかったのだろう?ということのような気もしますが、実際のところ Serviceクラスの実装において事前条件・事後条件の検証実行ロジックの置き場所を考えたから、今 改めて考えてみて気が付いたという気もしています。

実装

対応前

Page

@View
public class UserRegistrationPage implements Serializable {

    private static final long serialVersionUID = 1L;

    private UserId userId;

    private EmailForm userEmail;

    private NameForm name;

    private DateOfBirthForm dateOfBirth;

    private PhoneNumberForm phoneNumber;

    private GenderForm gender;

    public UserRegistrationPage() {
    }

    public UserRegistrationPage(UserRegistrationPage me) {
        this.userId = me.userId;
        this.userEmail = me.userEmail;
        this.name = me.name;
        this.dateOfBirth = me.dateOfBirth;
        this.phoneNumber = me.phoneNumber;
        this.gender = me.gender;
    }

    public void init() {
        this.userId = new UserId();
        this.userEmail = new EmailForm();
        this.name = new NameForm();
        this.dateOfBirth = new DateOfBirthForm();
        this.phoneNumber = new PhoneNumberForm();
        this.gender = new GenderForm();
    }

    public void update(User user) {
        this.userId = user.getUserId();
        this.userEmail = new EmailForm(user.getUserEmail().getValue());
        this.name = new NameForm(user.getName().getValue());
        this.dateOfBirth = new DateOfBirthForm(user.getDateOfBirth().getValue());
        this.phoneNumber = new PhoneNumberForm(user.getPhoneNumber().getValue());
        this.gender = new GenderForm(user.getGender().getValue());
    }

    public Map<String, String> checked(Integer index) {
        GenderType genderType = GenderType.find(index);
        Map<String, String> map = new HashMap<>();
        if (this.gender.isSameType(genderType)) {
            map.put("checked", "checked");
        }
        return map;
    }

    public String targetFor(UIComponent component, String targetName) {
        return component.getClientId() + "-" + targetName;
    }

    public void setGender(Integer index) {
        this.gender = new GenderForm(GenderType.find(index));
    }

    public User toUser() {
        return new User(this.userId, userEmail.getValue(), name.getValue(), dateOfBirth.getValue(), phoneNumber.getValue(), gender.getValue());
    }

   //検証用のインスタンスを生成するメソッド
    public Object getValidationForm() {
        ValidationForm obj = new ValidationForm();
        obj.userEmail = userEmail;
        obj.name = name;
        obj.dateOfBirth = dateOfBirth;
        obj.phoneNumber = phoneNumber;
        return obj;
    }

   //検証用のクラス定義。順序やValidを指定している。
    @SuppressFBWarnings("URF_UNREAD_FIELD")
    private static class ValidationForm {

        @Valid
        @FieldOrder(1)
        private EmailForm userEmail;

        @Valid
        @FieldOrder(2)
        private NameForm name;

        @Valid
        @FieldOrder(3)
        private DateOfBirthForm dateOfBirth;

        @Valid
        @FieldOrder(4)
        private PhoneNumberForm phoneNumber;

    }

//setter getter省略

Controller

@Controller
public class UserRegistrationAction {

    private UserRegistrationPage registrationPage;

    private UserService userService;

    private RegisterUser registerUser;

    private Validator validator;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, RegisterUser registerUser, Validator validator) {
        this.registrationPage = registrationForm;
        this.userService = userService;
        this.registerUser = registerUser;
        this.validator = validator;
    }

    public String fwPersist() {
        this.registrationPage.init();
        return "persistedit.xhtml";
    }

    public String confirm() {
       // 検証用インスタンスを生成・取得して、検証を実施。
        validator.validate(registrationPage.getValidationForm());
       // 検証不正が無いということでEntity(DomainObject)を生成
        User requestUser = this.registrationPage.toUser();
        registerUser.validatePreCondition(requestUser);
        return "persistconfirm.xhtml";
    }

    public String modify() {
        return "persistedit.xhtml";
    }

    public String register() {
        User requestUser = this.registrationPage.toUser();
        registerUser.with(requestUser);
        User responseUser = userService.persistedUser(requestUser);
        this.registrationPage.update(responseUser);
        return "persistcomplete.xhtml";
    }

    @EndConversation
    public String fwTop() {
        return "index.xhtml";
    }

}

対応後

Page

@View
public class UserRegistrationPage implements Serializable {

    private static final long serialVersionUID = 1L;

    private Validator validator;

    private UserId userId;

    @Valid
    @FieldOrder(1)
    private EmailForm userEmail;

    @Valid
    @FieldOrder(2)
    private NameForm name;

    @Valid
    @FieldOrder(3)
    private DateOfBirthForm dateOfBirth;

    @Valid
    @FieldOrder(4)
    private PhoneNumberForm phoneNumber;

    @Valid
    @FieldOrder(5)
    private GenderForm gender;

    public UserRegistrationPage() {
    }

    @Inject
    public UserRegistrationPage(Validator validator) {
        this.validator = validator;
    }

    public void init() {
        this.userId = new UserId();
        this.userEmail = new EmailForm();
        this.name = new NameForm();
        this.dateOfBirth = new DateOfBirthForm();
        this.phoneNumber = new PhoneNumberForm();
        this.gender = new GenderForm();
    }

    public void update(User user) {
        this.userId = user.getUserId();
        this.userEmail = new EmailForm(user.getUserEmail().getValue());
        this.name = new NameForm(user.getName().getValue());
        this.dateOfBirth = new DateOfBirthForm(user.getDateOfBirth().getValue());
        this.phoneNumber = new PhoneNumberForm(user.getPhoneNumber().getValue());
        this.gender = new GenderForm(user.getGender().getValue());
    }

    public User toUser() {
        this.validator.validate(this);
        return new User(this.userId, userEmail.getValue(), name.getValue(), dateOfBirth.getValue(), phoneNumber.getValue(), gender.getValue());
    }

    public Map<String, String> checked(Integer index) {
        GenderType genderType = GenderType.find(index);
        Map<String, String> map = new HashMap<>();
        if (this.gender.isSameType(genderType)) {
            map.put("checked", "checked");
        }
        return map;
    }

    public String targetFor(UIComponent component, String targetName) {
        return component.getClientId() + "-" + targetName;
    }

//setter getter省略

Controller

@Controller
public class UserRegistrationAction {

    private UserRegistrationPage registrationPage;

    private UserService userService;

    private RegisterUser registerUser;

    public UserRegistrationAction() {
    }

    @Inject
    public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, RegisterUser registerUser) {
        this.registrationPage = registrationForm;
        this.userService = userService;
        this.registerUser = registerUser;
    }

    public String fwPersist() {
        this.registrationPage.init();
        return "persistedit.xhtml";
    }

    public String confirm() {
        User requestUser = this.registrationPage.toUser();
        registerUser.validatePreCondition(requestUser);
        return "persistconfirm.xhtml";
    }

    public String modify() {
        return "persistedit.xhtml";
    }

    public String register() {
        User requestUser = this.registrationPage.toUser();
        registerUser.with(requestUser);
        User responseUser = userService.persistedUser(requestUser);
        this.registrationPage.update(responseUser);
        return "persistcomplete.xhtml";
    }

    @EndConversation
    public String fwTop() {
        return "index.xhtml";
    }

}

所感

Controllerがスッキリしました。
Pageクラスの変換にて、不変条件が充足されている事を保証することで、Controller側は 安全な実装が達成できているように思われます。
なにより「検証用に新たにインスタンスを生成する」実装が無くなったことで、分かりやすいコードになったと思います。

メモ

DIするValidatorについて、1つだけ想定外というか、言われてみればそうなのかもしれないけど… というようなことがありました。
それは、Validatorの具象クラスである BeanValidator が そのままでは PageクラスにDI出来なかったことです。

Pageクラスが ConversationScopedであるため、DIする具象クラスは Serializableじゃないということで実行時例外が発生しました。

BeanValidator は プロパティを持たないクラスなので Serializableは不要だろうと思っていましたが、そういう訳では無かったようです。

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

「じぶんのかんがえるさいきょうのふれーむわーく」の道のりは険しいですねぇ。
なにせ方式を考えている過去の自分が怪しいのですから。。