mardi 13 septembre 2016

Java generic type bounds and runtime construction check

I am trying to create a builder which would help me to construct an object graph implemented using decorator pattern. I want the builder to be as reliable as possible, but it fails when generics are introduced with type bounds. That is it constructs an object graph which will fail when decorator "workflow" is called with "Cannot cast X to Y" error.

Builder usage:

    Processor<XyzContext> processor = Builder.create(new XyzProcessor())
        .persist()
        .handleRequest()
        .handleError()
        .build();

Builder class used to decorate the original processor:

class Builder<T> {

    Processor<T> processor;

    Builder(Processor<T> processor) {
        this.processor = processor;
    }

    static <T> Builder<T> create(Processor<T> processor) {
        return new Builder<T>(processor);
    }

    Builder<T> handleRequest() {
        processor = new RequestHandlingProcessor(processor);
        return this;
    }

    public Builder<T> processNonBarcode() {
        processor = new PersistingProcessor(processor);
        return this;
    }

    public Builder<T> handleError() {
        processor = new ErrorHandlingProcessor(processor);
        return this;
    }

    public Processor<T> build() {
        return processor;
    }

}

Persisting processor (being decorated)

public class PersistingProcessor<T> implements Processor<T> {
        private final Processor<T> delegate;

    public PersistingProcessor(Processor<T> delegate) {
        this.delegate = delegate;
    }

    @Override
    public void process(T request) {
        // save to db
    }
}

Request handling decorator

public class RequestHandlingProcessor<T extends RequestEntityHolder & RawRequestHolder> 
        implements Processor<T> {

    private final Processor<T> delegate;

    public RequestHandlingProcessor(Processor<T> delegate) {
        this.delegate = delegate;
    }

    @Override
    public void process(T context) {
        RequestEntity requestEntity = new RequestEntity();
        requestEntity.setRaw(context.getRawRequest());  <------- RawRequestHolder interface used
        context.setRequestEntity(requestEntity);  <------------- RequestEntityHolder interface used

        try {
            delegate.process(context);
        } catch (RuntimeException e) {
            requestEntity.setError(e.getMessage());
            throw e;
        }
    }

}

Error handling decorator

public class ErrorHandlingProcessor<T> implements Processor<T> {


    private final Processor<T> delegate;

    public ErrorHandlingProcessor(Processor<T> delegate) {
        this.delegate = delegate;
    }

    @Override
    public void process(T request) {
        try {
            delegate.process(request);
        } catch (RuntimeException e) {
            // do stuff
            throw e;
        }
    }

}

The context class

public class XyzContext implements RequestEntityHolder, RawRequestHolder {

    private String request;
    private RequestEntity requestEntity;

    public XyzContext(String request) {
        this.request = request;
    }

    @Override
    public String getRawRequest() {
        return request.toString();
    }

    @Override
    public RequestEntity getRequestEntity() {
        return requestEntity;
    }

    @Override
    public void setRequestEntity(RequestEntity requestEntity) {
        this.requestEntity = requestEntity;
    }
}

When I would use this builder for a different Context class, e.g. AbcContext class which does not implement RequestResponseHolder or RawRequestHolder, I would know about this unbound type error too late (when the decorated processor would be used for the first time to process something by throwing ClassCastException).

When I construct the graph using constructors only:

new ErrorHandlingProcessor<XyzContext>(
    new RequestHandlingProcessor<XyzContext>(
        new PersistingProcessor<XyzContext>(
            new AbcProcessor()
        )
    )
);

then I, of course, get a compilation error that AbcContext is out of type bounds.

Is there some pattern or a way that it throws error during construction using the "dynamic" builder too? The builder would be used to create a Spring bean, so it would be awesome if it could throw an error during construction (app context would not start).

I know about type erasure, but was curious if there is some better way then using ugly nested constructors.

Aucun commentaire:

Enregistrer un commentaire