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

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

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

さいごに

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