で考えたことに対して、実際の実装を経て 妥協(?)した
を踏まえて、改めて整理をした方が良さそうに思い 考えてみることしました。
はじめに
考え直してみようと思ったことを呟いたもの。
返信ありがとうございます。
— Yamashita (@_vermeer_) 2018年7月11日
javax.inject.Providerは初見です。
調べてみたいと思います。
Pageクラスの中でvalidate(this)するということは、クラス内にvalidatorがあると思うのですが、個人的な考えにより実行行為はAction側で行いたいと考えておりまして。。
>自己レス
— Yamashita (@_vermeer_) 2018年10月8日
Pageクラスでvalidate(this)したくない(Action側でしたい)と思っていた理由ってなんでだろ?
不変条件の検証なんだから、自身に検証メソッドを持たせて何が悪いんだろう?
個々のDomainObjectではAnnotationで検証論理を定義して、validateは 実際に使用する際に実行する、という発想(続
ということで、Actionでvalidateをしたいと思っていたとは思う。
— Yamashita (@_vermeer_) 2018年10月8日
でもPageは関心事の主体というよりも集約管理が主体としたら、集約全体の不変条件を確認する、というメソッドを持っていても良い気がしてきた。
実際、検証用のフィールドだけを持ったクラスインスタンスを生成したけど(続
Pageクラスに目的別のvalidateメソッドを準備しておけば、内部情報を暴露する必要も無いし、Controller側もシンプルな実装になるんじゃないかな?
— Yamashita (@_vermeer_) 2018年10月8日
動詞を主体とするクラスでしか validateをしない方が良さそう、という思い込みが何故生まれたのか分からないけど もうちょっと考えてみた方が良さそう。
振り返り
- 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
さいごに
「じぶんのかんがえるさいきょうのふれーむわーく」の道のりは険しいですねぇ。
なにせ方式を考えている過去の自分が怪しいのですから。。