Skip to content

Utilizing composition over inheritance in ExecutionStrategy #523

@tklovett

Description

@tklovett

I've run into a situation where I've had to duplicate code to achieve the same ExecutionStrategy#handleDataFetchingException behavior in multiple ExecutionStrategy implementations. Right now I can only reduce this duplication by pulling the code out into a static method on a utility class.

During local development, we use a simple extension of SimpleExecutionStrategy which overrides handleDataFetchingException in order to delegate to our custom handler (which maps regular and auth exceptions to different GraphQLErrors). During local development we want our data fetching to occur serially and in a single thread for ease of debugging.

In production, we use an extension of ExecutionStrategy, which is essentially a copy of ExecutorServiceExecutionStrategy, but ensures that the Spring Request context is passed to each child thread. It also overrides handleDataFetchingException with the same exact code as above.

If graphql-java were to utilize the "composition over inheritance" pattern for this ExecutionStrategy behavior (and elsewhere?), than end-user implementations can be DRY'd up without static method calls. I think @bbakerman was bringing up a similar use case for composition in this comment on PR #481.

More concretely, this could look something like this, where we set up the wiring in our code:

    @Bean
    public ExecutionStrategy devExecutionStrategy() {
        return new SimpleExecutionStrategy(executionExceptionHandler());
    }

    @Bean
    public ExecutionStrategy prodExecutionStrategy() {
        return new RequestContextAwareExecutionStrategy(executionExceptionHandler());
    }

    @Bean
    public ExecutionExceptionHandler executionExceptionHandler() {
        return (executionContext, fieldDef, argumentValues, path, e) -> {
            if (e instanceof AuthorizationException) {
                executionContext.addError(new AuthorizationError((AuthorizationException) e));
            } else {
                executionContext.addError(new ExceptionWhileDataFetching(e));
            }
        };
    }

And graphql-java does the following:

@PublicSpi
public abstract class ExecutionStrategy {
    // ...
    public ExecutionStrategy() {
        this.exceptionHandler = this::handleDataFetchingException; // for backwards compatibility, if necessary
    }

    public ExecutionStrategy(ExecutionExceptionHandler exceptionHandler) {
        this.exceptionHandler = exceptionHandler;
    }

    @Deprecated
    protected void handleDataFetchingException(
            ExecutionContext executionContext,
            GraphQLFieldDefinition fieldDef,
            Map<String, Object> argumentValues,
            ExecutionPath path,
            Exception e) {
        ExceptionWhileDataFetching error = new ExceptionWhileDataFetching(path, e);
        executionContext.addError(error);
        log.warn(error.getMessage(), e);

    }

    // ...
    protected ExecutionResult resolveField(ExecutionContext executionContext, ExecutionParameters parameters, List<Field> fields) {
        try {
            DataFetcher dataFetcher = fieldDef.getDataFetcher();
            resolvedValue = dataFetcher.get(environment);

            fetchCtx.onEnd(resolvedValue);
        } catch (Exception e) {
            fetchCtx.onEnd(e);
            exceptionHandler.handle(executionContext, fieldDef, argumentValues, parameters.path(), e);
        }
    // ...
    }
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions