[GO] Wrap and encapsulate the validation target to prevent complications

[Addition] When I think about it, it wasn't a Decorator because I didn't mess with existing methods. This is the name that the pattern name does not come out suddenly. Is it just a Facade?

TL;DR

By implementing the wrapper class for Validation and the wrapper class for Creation separately, the interface for each responsibility is specified.

When is it effective

When validation logic is required separately from validation using framework functions. Specifically, when implementing a system such as OpenID Connect that requires an error response to be returned by redirect, which cannot be handled by the error response generated by the framework.

For the sake of convenience, the logic that generates the OpenID Connect error response is taken as an example here, but it applies to any system that must implement complex validation.

What do you want to do

When converting request parameters to domain objects, if validation and domain object generation are performed based on one request object, the readability of the code will deteriorate, so I want to avoid that.

For example, in OpenID Connect, it is often the case that the amount of code in the volume is quite large just by validating the authorization request, and for performance, it is desirable to keep the object generated during validation in the context. You also have to take care of the pattern actually it was an OAuth2 request instead of OpenID Connect, so the number of branches in the validation logic is ridiculous.

If possible, I would like to generate a request object class for each branch flow and realize validation logic branching with polymorphism so as not to cause a test case explosion.

However, in reality, there are a lot of common processes regardless of the flow, and methods like request.isImplicitFlow () are called from everywhere, so they will be implemented in the request object.

The above methods are used only during validation. That's because Flow is static after being converted to a domain object, and probably a class like ʻImplicitFlowHandler` should do the work.

As a reminder, it's not just about OpenID Connect

So if you prepare a wrapper class for validation and implement the delegation method, the method will not be implemented in the request object itself, so the code will be better visible.

Sample code

Since the Controller layer receives HTTP request parameters, the field will always be String.

package jp.hoge.openidconnect.authorization;
import lombok.Getter;

//Request object
// (Object to bind HTTP request parameters)
@Getter //Getter shall be defined
public class AuthorizationRequest {
  private String responseType;
  private String clientId;
  private String scope;
}
package jp.hoge.openidconnect.authorization.validation;

import jp.hoge.openidconnect.core.ResponseType;

//Wrapper class for Validation.Inherit the wrapping class.
//Visibility is always the Package default. 
class RequestWrapperForValidation extends AuthorizationRequest {

  private final AuthorizationRequest rawRequest;
  
  private ResponseType responseType = null;

  //The visibility of the constructor is also package
  RequestWrapperForValidation(final AuthorizationRequest rawRequest) {
    this.rawRequest = rawRequest;
  }

  //Method that this uses only in the Validation layer.
  //The actual OpenID Connect is more complicated, but it's complicated, so it's a rough image
  boolean isImplicitFlow() {
    if (rawRequest.getScope().contains("openid")) {
      return _getResponseType().contains(ResponseType.ID_TOKEN);
    } else {
      return _getResponseType().contains(ResponseType.TOKEN);
    }
  }

  //Method for converting to domain object only once
  //You can refer to the object generated here many times in the validation process..
  //However, Thread-Make it clear that it is not Safe.
  private ResponseType _getResponseType() {
    if (this.responseType == null) {
      this.responseType = ResponseType.parse(rawRequest.getResponseType());
    }
    return this.responseType;
  }

  //Delegation method for the wrapping class
  public String getResponseType() {
    return rawRequest.getResponseType();
  }
  // ...After that, we will implement the delegation method
}

If you implement it like this (regardless of whether the code is good or bad), you can use the validation method from the same package (validation layer), but it looks the same as the original object from other layers.

In the case of Web specifications such as OpenID Connect, it doesn't matter if the request object is in the domain layer, but for casual use, it wraps the interface of the request object instead of the request object. Otherwise, the domain layer will refer to the class of the Controller layer, so this is NG.

As with validation, preparing a wrapper class during creation will also help prevent the anti-corruption layer from bloating. But in that case, it is better to implement it with just a Builder pattern.

public class AuthorizationModelBuilder {
  private final AuthorizationRequest rawRequest;

  public AuthorizationModel toDomainModel() {
    // ...Conversion process to domain model
  }
}

It was a failure to cite OpenID Connect, domain knowledge was too special w

Further notes

It's not just about validation. For layers and packages with different responsibilities, it is better to use wrapped objects to clarify the interface and usage to improve readability. Of course, the implementation cost is high, so be aware of the trade-offs.

Recommended Posts

Wrap and encapsulate the validation target to prevent complications
Python Memorandum: Refer to the text and edit the file name while copying the target file
Converting the coordinate system to ECEF and geodesy
Experiment and leave evidence to determine the specifications.