[追記] 考えてみたら、既存メソッドはいじらないからDecoratorじゃなかった。パターン名がパッと出てこない、なんて名前だこれ。ただのFacadeか。
TL;DR
Validation
用のラッパークラスとCreation
用のラッパークラスを別々に実装することで、それぞれの責務におけるインタフェースを明示する。
どんなときに有効か
フレームワークの機能を利用したバリデーションとは別にバリデーションロジックが必要となる場合。
具体的には、OpenID Connectのようにエラーレスポンスをリダイレクトで返さなければいけないような、フレームワークが生成するエラーレスポンスでは対応できないようなシステムを実装する場合。
ここでは便宜上、OpenID Connectのエラーレスポンスを生成するロジックを例にするが、複雑なバリデーションを実装しなければならないシステムではどんなシステムでも当てはまる。
何がしたいか
リクエストパラメータをドメインオブジェクトに変換する際に、1つのリクエストオブジェクトをもとにバリデーションとドメインオブジェクト生成を行うと、コードの可読性が悪くなっていくのでそれを避けたい。
例えばOpenID Connectでは認可リクエストのバリデーションだけで結構なボリュームのコード量になり、かつ、パフォーマンスのためにはバリデーション途中で生成したオブジェクトをコンテキストに保持しておきたいケースがよくある。
また、実はOpenID ConnectじゃなくてOAuth2のリクエストでした
というパターンもケアしなければならないため、バリデーションロジック内の分岐はエゲツない数になる。
できればテストケース爆発を起こさないよう、分岐フローごとにそのフロー用のリクエストオブジェクトクラスを生成してポリモーフィズムでバリデーションロジックの分岐を実現したい。
でも実際にはフローによらず共通な処理が山程あり、request.isImplicitFlow()
みたいなメソッドは至るところから呼ばれるため、リクエストオブジェクトに実装することになる。
上述のようなメソッドは、バリデーション時にしか利用しない。なぜなら、ドメインオブジェクトに変換された後は、Flowは静的であり、おそらくImplicitFlowHandler
みたいなクラスが処理を行うはずだからである。
念押しですが、OpenID Connectに限った話ではないです
なのでバリデーション用のラッパークラスを用意して、委譲メソッドを実装してあげれば、リクエストオブジェクト自体にはメソッドが実装されないため、コードの見通しがよくなる。
サンプルコード
Controller層ではHTTPリクエストパラメータを受け取るので、フィールドは必ずString
となる。
package jp.hoge.openidconnect.authorization;
import lombok.Getter;
// リクエストオブジェクト
// (HTTPリクエストパラメータをbindするオブジェクト)
@Getter // Getterが定義されるものとする
public class AuthorizationRequest {
private String responseType;
private String clientId;
private String scope;
}
package jp.hoge.openidconnect.authorization.validation;
import jp.hoge.openidconnect.core.ResponseType;
// Validation用のラッパークラス. ラップしているクラスを継承する.
// 可視性は必ずPackageデフォルトとする.
class RequestWrapperForValidation extends AuthorizationRequest {
private final AuthorizationRequest rawRequest;
private ResponseType responseType = null;
// コンストラクタも可視性はpackage
RequestWrapperForValidation(final AuthorizationRequest rawRequest) {
this.rawRequest = rawRequest;
}
// コレがValidation層でのみ利用するメソッド.
// 実際のOpenID Connectはもっと複雑だけど、煩雑なので大体のイメージ
boolean isImplicitFlow() {
if (rawRequest.getScope().contains("openid")) {
return _getResponseType().contains(ResponseType.ID_TOKEN);
} else {
return _getResponseType().contains(ResponseType.TOKEN);
}
}
// ドメインオブジェクトへの変換を1回だけにするためのメソッド
// ここで生成したオブジェクトをバリデーションプロセス内で何度も参照できる.
// ただし、Thread-Safeでないことは明示すること.
private ResponseType _getResponseType() {
if (this.responseType == null) {
this.responseType = ResponseType.parse(rawRequest.getResponseType());
}
return this.responseType;
}
// ラップしているクラスに対する委譲メソッド
public String getResponseType() {
return rawRequest.getResponseType();
}
// ... 以降、委譲メソッドを実装していく
}
(コードの善し悪しはともかく)こんな感じで実装すれば、同じパッケージ(バリデーション層)からはバリデーション用のメソッドが利用できるが、他の層からはもとのオブジェクトと同じに見える。
OpenID ConnectのようなWebの仕様の場合、リクエストオブジェクトがドメイン層にいても問題ないけど、カジュアルに使う場合はリクエストオブジェクトではなく、リクエストオブジェクトのインタフェースをラップする。そうしないとドメイン層がController層のクラスを参照することになるのでこれはNG。
バリデーション同様、クリエイション時もラッパークラスを用意してあげると、腐敗防止層の肥大化対策にもなる。でもその場合はただのBuilderパターンで実装するほうがよい。
public class AuthorizationModelBuilder {
private final AuthorizationRequest rawRequest;
public AuthorizationModel toDomainModel() {
// ... ドメインモデルへの変換処理
}
}
OpenID Connectを引き合いに出したのは失敗だった、ドメイン知識が特殊すぎたw
更に追記
別にバリデーションに限った話ではないです。責務が異なるレイヤやパッケージではあえてラップしたオブジェクトを使ってインタフェースや用途を明確にしてあげると可読性が向上します。
当然、実装コストはかさむのでトレードオフは意識します。