Application層の検証結果をPageに関連付ける方法を考える
Application層の検証結果をControllerで どうやってPageクラスに関連付けるか。。
— Yamashita (@_vermeer_) 2018年10月17日
やり方に加えて 実装表現も含めて どうしたものかなぁ。
アノテーションによる情報指定だとマッピング対象が大量にあった場合、どうにも不細工な気もするし。。
しっくりくるイメージが まだ降ってこないなぁ。
試行錯誤の 垂れ流しです。
やりたいこと
- BeanValidationの結果や実行時例外を Pageクラスの特定のフィールドに関連付ける
- 関連付ける対象フィールドは複数のケースもある
フィールドに直接関連付けたいのはメッセージ順序の情報を保持している Pageクラスのフィールドに付与した Annotation(@FieldOrder
)から情報を取得したいからです。
なお、BeanValidationの結果は 特定のフィールドに関連付けて明示するべき要件になると思いますが、検証不正以外の実行時例外において 特定のフィールドに関連付ける必然は無いようにも思います。ということで、実行時例外とフィールドとの関連付けは後回し、もしくはしなくても良いかもしれないとして一旦は 考慮から外しておきます。
必要な要素
BeanValidationのメッセージキー(message template)と Pageクラスのフィールドのペアとなる情報。
例えば、Factoryクラスで表現するとしたら
ErrorMappingField.of("message.key",Hoge.class.fieldA)
複数だったら
ErrorMappingField.of("message.key",{Hoge.class.fieldA,Hoge.class.fieldB})
みたいな感じ。
なお、例としてFactoryで表現はしたけど、Factoryにするという訳ではありません。
関連付け処理は、BeanValidationをクライアント用のメッセージ変換をするのと同様に ControllerのAction用のInterceptorで行います。 要素としては、マッピング処理用のInterceptorを準備するということになります。
レイヤー
処理はInterceptorで行うとして、定義は どのレイヤーに配置するのが良いでしょうか?
発生源から遡る形で考察します。
Application
Application層の検証不正は、主にServiceから発行されるので、Serviceをベースに考えます。 ここで出来ることは、BeanValidationで検証不正理由となる情報を message で表現することだけです。
次に考えるとしたら、message に指定する情報を String(文字列)とするのか、EnumクラスのValueを使用するようにするか、です。
@AssertTrue
のメッセージを見れば、要件を把握することは出来ますし、幸い Annotationで情報を付与しているので 実行時であれ ツールであれ Serviceに どのような message が指定されているのか抽出することも可能です。
その上で Enumクラスでわざわざクラス実装までする必要があるか?というところです。
ここで、検証不正を発行する側ではなく、発行された検証不正情報を扱う 利用側の立場で考えてみます。
変換するためには、元の検証不正の情報(message)を漏れなく、誤記なく把握したいです。
となると、
- ビルド時にAnnotationから情報を取得してチェックする
- Enumで定数型クラスとして定義する
のどちらかのやり方が想定されます。
前者は そのために仕組みを構築しないといけないし、回りくどさのあるアプローチだと思います。
(ほぼ必然的ですが)Enumクラスを作って Serviceにおける message の設定でも、その value を使用するというのが良さそうです。
Enumクラスにするとして、それは どのレイヤーで管理するのが良いでしょう?
Serviceクラスから発行されるものだから、Serviceクラスで定義して、可視性をpublicにしておいて 利用側でも参照できるようにするというアプローチが1つ。
利用側がPresentation層のクラスであるということは明確で、Interceptorで変換する際にも利用するので 全レイヤーから横断的に使用するということで Domainレイヤーで扱うというのが、もう1つ。
直感的には、Serviceクラス内で定義しておくのが良さそうに思います(つまり1つ目のアプローチ)。
その上で全レイヤーから扱えるような設計をカバーしたいところです。
全レイヤーから操作をするために、Enumクラスに message と 関連するターゲット を取得するインターフェースを設けて、それを経由して操作するようにすれば 良いかな。
もう1つのDomainレイヤーで扱うというのも悪くは無いと思っていて、雑に言うと message.propertiesで検証不正の情報をメッセージに変換するのだから、逆に それを使うだけという考え方。
message.properties の情報から Enumクラスを同期を取って作成して それを使えば 検証不正の結果出力において 表示が出来なかったということにはならないので 表示安全と言えます。
欠点は、Serviceが発行する検証不正の種類がServiceクラス内で定義している場合と違って、実際のServiceの実装を見ないと分からないというところ。
つまり連携対象とすべき情報が漏れてしまう可能性があるということです。
ただ、情報が欠落するというのではなく、あくまで並び順が制御できないだけなので致命的な障害という訳でも無いという考え方もあります。
なお、補足的な話としてmessage.properties の情報から Enumクラスを同期を取って作成については、以前作成したaptライブラリを使用することを想定しています。*1
ただ、今考えている構想だと Keyの扱いへの考慮が足りないですし 、BeanValidation用の messageTemplate として使う場合、{ }
で囲っておかないといけないなど 多少手直しというか機能追加は必要です。
ちなみに、上述だと message.properties としていますが、このライブラリは Service単位(もしくはユースケース単位)で プロパティファイルを作成しておいて、Enumクラスに変換することも可能なので 2つの要件を満たすことも出来るのではないかな?と思っています。
なんとなく、方向性は見えてきた気がします。
- Serviecの検証不正のメッセージはEnumクラスで実装
- システム全体 もしくは サービス単位で Enumクラスを作成
- 当該Serviceが どのような検証不正を発行するか、については 出来そうだったら追加考察するけど、出来なかったとしても致命的な障害を引き起こすわけではないとして割り切ることも視野に入れる
一旦このくらいで次のことを考えます。
Presentation
Serviceからの検証不正をマッピングする先が、Pageクラスのフィールドなので 連携マッピングは Presentation層のパッケージで扱います。
とりあえずマッピングにおけるKey情報は、Application側で考えた Enum値だとして、ここで考えるのは Valueとなる画面側です。
なお、フィールドの背景色を変える場合は画面IDとなる文字列を 何かしらの形で関連付ける必要がありますが、とりあえず今回は メッセージ表示順なので Pageクラスのフィールドと関連付けさえできれば 一旦良しとします。
では、それを実装する場所は Controllerか Pageか?
Controller
以前、検証不正のあったフィールドの背景色を変更する時にやったのは、検証前に 実行時例外で保持させているメッセージ と 画面フィールドのIDをマッピングしておいてから検証およびServiceの実行をするという やり方をしていました。
ということで、Controllerから考えてみたいと思います。
Controllerで「BeanValidationのメッセージキー(message template)と Pageクラスのフィールドのペアとなる情報」と類似なものを 設定しておいて Serviceを実行する感じになると思います。
以前は エラー用のScopeを持ったBeanを作っておいて、DIをして実現しました。Controllerのメソッド内で設定をしていましたが、今 思えば Annotationで宣言しておいて Interceptorで処理するときに 情報を取得して 裏側で処理しても良かったかもしれません。
ということで、もし 今回 Controllerでマッピングするなら、Anntaionでクラス全体、またはメソッド単位に Annotationで宣言的に実装することにするとして、
これでも良いかな、という印象はあるのですが 検証不正とPageのフィールドとのマッピングをしようとすると、Pageクラスのフィールドの可視性を privateよりも広く公開しないといけません。そうしないと Controllerから指定できないからです。
その上で気になるというか、どうしたものか、というところ
- 多量のマッピングがあった時には ControllerがAnnotationまみれになってしまう
- ネストや繰り返しになっている場合の指定方法は どうする?
正直、ノーアイディアなので、以前の検証不正の背景色変更の時に どうやって実装的にやったのかな?と思って確認してみると。。
メッセージ と 画面フィールドのIDをマッピングした情報を Scope管理下のBeanに保持しておいて、Faceletsの 色を意味するclass値を メソッド経由でDIしたBeanから取得するということをしていました。*2
単一フィールドは
class="#{errCssForm.find().cd}"
テーブルのフィールドは
class="#{errCssForm.find(row.id).familyName}"
こんな感じです。
JSF(Facelets)だと、参照する Map型のインスタンスのキー値の指定ができるというだけです。
errCssForm.find()
の戻り値として、画面IDとclass値のMapが返却されます。そのMapにキー値cd
があった場合は、そのvalueとしてエラーを表現するclass値(背景色)が返却されるという仕掛けです。
.find
以降のフィールド風の値は 型安全なものではありませんし入力補完もありません*3。あくまで キー値(String)というだけです。
もしこれを Classファイルのフィールドにやろうとしたら、、んー、やっぱり いいアイディアが浮かばない。。
ただ、ここまで考えたり思い出したりする中で分かったことはマッピングする情報はPageクラスのフィールドだから、Controllerで指定するのではなくて Pageクラス側で宣言的に定義しておいた方が良いんじゃないか?ということです。
ということで、Controllerで考えることは ここまでとします。
Page
宣言的に指定するとした場合、イメージとしては
@InvalidMessageMapping(value=RegisterUser.Error.EXISTS_USER) @FieldOrder(1) private UserName uesName;
みたいな感じ。
特定のフィールドに関連付けるものだけにしか付与しないとはいえ、1つのフィールドに大量の検証不正を関連付けると Annotationまみれになってしまうのは避けようがないかもしれません。
@InvalidMessageMapping(value=RegisterUser.Error.EXISTS_USER) @InvalidMessageMapping(value=RegisterUser.Error.ALREADY_CHANGE) @FieldOrder(1) private UserName uesName;
引数を配列にしたら、どうでしょう?
@InvalidMessageMapping(value={RegisterUser.Error.EXISTS_USER,RegisterUser.Error.ALREADY_CHANGE}) @FieldOrder(1) private UserName uesName;
これは これでアリな気もしますが、手間と可読性自体は あまり変わりが無いように思います。 Annotationに第二引数を設けてグループ化する必要があれば、配列にする意味もありますが、そうでないから そこまででも無いかなぁという感じです。
次に、Pageクラスがネストした場合はどうしましょう?
全てのフィールドの配下全てのクラスに当該Annotationが存在しないか走査するという やり方もあると思いますが、さすがに性能面でやりたくありません。
とするとBeanValidationの@Valid
でマーカーするのと同じような仕組みで親クラス側で指定すれば良いかなと考えています。
例えば @MessageMapping
が付いていれば、そのクラスのフィールドでも @InvalidMessageMapping
があれば 連携情報として使用する感じです。
ところで、このようなネスト構造は 今後も色々とありそうです。
例えば
で扱った@GroupLabel
も似たような発想のものです。
もっと広く見ると、@FieldOrder
が付与されているフィールドも、同じかもしれません。
細かくAnnotationを設けておくことで、細かい制御ができるメリットはありますが、細かすぎるとAnnotationまみれになってしまいます。
多少、冗長であることは覚悟の上で、マーカーとなるAnnotationについては、もっとザックリと 一纏めにした方が良いような気もします。
たとえば、Pageクラスに対してはスコープを管理するAnnotationとして@Stereotype
で集約した@View
というものを作成しました。それと同じようにフィールド用に@ViewItem
というものを作成しても良いかもしれません。
イメージとしては、基本仕様は@FieldOrder
と同じで、任意項目はパラメータで指定する感じ。
悩ましいのは、Annotationまみれから、パラメータ過多のAnnotaionになっただけ、となりそうなところ。
逆の発想としては@ViewItem
を作るよりも、配下の情報を参照するマーカーとして@FieldOrder
や@GroupLabel
の いずれかがあった場合は対象とするという やり方の方が良いかもしれません。
これなら、任意のAnnotationを後から追加できる仕組みだから柔軟かもしれません。
ネスト構造については、このくらいかな?
さて、ネストに似ているけど 異なるテーブル構造(繰り返し)は どうでしょう?
これまでの整理では、行情報よりも列情報の指定が優先されることになることになりそうです。
対処案としては Annotation、@Valid が付与できるコレクション、Annotationという順番で 順序制御をすれば、目的は満たせそうです。
@Valid が付与できるコレクションとは、具体的には「配列、 Iterable を実装したコレクション、 Map」を考えています。
発想の元がBeanValidationだから、まぁこれで良いかな?というくらいの雑な整理です。
ただ、現状準備している実験場は、テーブル形式の入力サンプルは無いので、とりあえず保留。
段々と 整理出来つつあるように思いますがServiceで検証不正が発生した結果をマッピングするPageクラスと そもそも どうやって関連付けしましょう?
Actionで指定
Actionのクラスまたはメソッドに、レスポンスに使用するコンテキストとして指定するイメージです。
@ResponseContext(HogePage.class) public String hoge(){ return "index.xhtml" }
とか
@ResponseContext(HogePage.class) public class UserRegistrationAction { public String hoge(){ return "index.xhtml" } }
クラスに指定したら、配下のAction全てに適用するイメージです。
この場合の発想としては、ServiceのCallerであるControllerのAction単位で、そのレスポンスとなるPageクラスを指定するという考え方です。
PageクラスはAggregate Rootとして扱っていますが、例えば 複数のコンポーネントで構築されているという実装をした場合なら、パラメータに複数のクラス指定をするイメージです。
フィールドに指定
PageクラスのFieldにAnnotationを付与して宣言的に表現する。
public class HogeAction { @ViewContext private FugaPage; public String hoge(){ return "index.xhtml" } }
もし、複数のコンテキストで実装されている場合は、それぞれのFieldにAnnotationを付与します。
で、どっち?
コードを見つつ思った印象としては、後者の方が良い気がします。
理由は、Annotationのパラメータとしてクラスを指定するやり方だと、実際にActionで使用しているPageクラスであるかどうか保証がない気がするからです。単純なタイプミスも含めて 実際に使用しているFieldの型と目視なりコンパイラなりで確認をしないといけません。
また基本的にServiceから発行された検証不正のマッピング用途が現状の想定ですが、それ以外の「Service - Controller - Form」を関連付けるマーカーという意味で考えても 意図が伝わりやすい方式のように思います。
参考情報
JavaEE使い方メモ(Bean Validation) - Qiita
さいごに
考え始めた時の構想とは随分と異なる結論になった気がしますが、なんとなく自分としては 腑に落ちる整理が出来た気がします。
あとは実際に実装してみて、出来る出来ない 実際に出来たコードを見て考え直す、というプロセスに入ろうと思います。
追記
2018/10/26
Annotationのパラメータとしてインターフェースを指定できませんでした。
Enumは指定できますが、そうするとEnum毎にAnnotationを準備しないといけません。
Service毎にEnumを作るのではなく、システム全体で1つのEnumを作って 全てのServiceで参照をするというやり方ならできるとは思います。
出来れば Service単位で管理したいのだけれど最悪 そうするしかないのかなぁ。。
でも、そうすると@InvalidMessageMapping
が構造(仕様)を現すAnnotionではなくで具体的な実装と密になってしまうのでNGです。
となると、Stringで指定する方法で考えるしかないかな?
考えられる やり方としては Serviceクラスに 昔懐かしい 定数クラス方式。
少なくとも 構造的な記述を静的に実装はできます。
型安全な実装指針ではなく、実装作法を示す必要があるので、出来れば 基本型を使うような方式は取りたくなかったけど 仕方ないかなぁ。。
あっ、また あのライブラリが日の目を見るタイミングを逸してしまったのでは・・・。
なお、配列指定については、@InvalidMessageMapping側で 配列指定しておけば、上述の両方の要件は満たせるようです。