Last active
February 10, 2026 16:04
-
-
Save stefanofago73/6916c3d430d20b226bf1c4610b342843 to your computer and use it in GitHub Desktop.
Handlers: Some design experiments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // Maybe we can use Operation instead of Handler? | |
| // | |
| @FunctionalInterface | |
| public interface Handler<I, O> { | |
| Result<O> handle(I request, ServiceContext context); | |
| } | |
| // | |
| // The classic view of a decoration, can be done in FP style and using only interface | |
| // | |
| public abstract class DelegatingHandler<I, O> implements Handler<I, O> { | |
| private final Handler<I, O> delegate; | |
| protected DelegatingHandler(Handler<I, O> delegate) { | |
| this.delegate = Objects.requireNonNull(delegate, "delegate"); | |
| } | |
| protected final Handler<I, O> delegate() { | |
| return this.delegate; | |
| } | |
| } | |
| // | |
| // Un internal DSL to guide Devs in the decoration of existing handlers | |
| // The final implementation need to be done as staged set of interfaces | |
| // | |
| public final class HandlerDsl { | |
| private HandlerDsl() { | |
| } | |
| @FunctionalInterface | |
| public interface Pre<I> { | |
| I apply(I request, ServiceContext ctx); | |
| } | |
| @FunctionalInterface | |
| public interface Post<O> { | |
| Result<O> apply(Result<O> result, ServiceContext ctx); | |
| } | |
| @FunctionalInterface | |
| public interface PreCondition<I> { | |
| boolean test(I request, ServiceContext ctx); | |
| } | |
| @FunctionalInterface | |
| public interface PostCondition<O> { | |
| boolean test(Result<O> result, ServiceContext ctx); | |
| } | |
| @FunctionalInterface | |
| public interface Around<I, O> { | |
| Result<O> apply(I request, ServiceContext ctx, Proceeding<I, O> next); | |
| } | |
| @FunctionalInterface | |
| public interface Proceeding<I, O> { | |
| Result<O> proceed(I request, ServiceContext ctx); | |
| } | |
| @FunctionalInterface | |
| public interface ExceptionHandler<O> { | |
| Result<O> handle(Throwable error, ServiceContext ctx); | |
| } | |
| @FunctionalInterface | |
| public interface FinallyHook<I, O> { | |
| void run(I originalRequest, I internalRequest, ServiceContext ctx, Result<O> result, Throwable error); | |
| } | |
| static <I, O> Handler<I, O> compose(Handler<I, O> h1, Handler<I, O> h2, | |
| BinaryOperator<Result<O>> mapper) { | |
| return new Handler<I, O>() { | |
| @Override | |
| public Result<O> handle(I request, ServiceContext context) { | |
| var r1 = h1.handle(request, context); | |
| var r2 = h2.handle(request, context); | |
| return mapper.apply(r1, r2); | |
| } | |
| }; | |
| } | |
| static <I, O> Handler<I, O> compose(List<Handler<I, O>> handlers, | |
| BinaryOperator<Result<O>> mapper) { | |
| return new Handler<I, O>() { | |
| @Override | |
| public Result<O> handle(I request, ServiceContext context) { | |
| return handlers | |
| .stream() | |
| .map(h->h.handle(request, context)) | |
| .reduce(mapper) | |
| .orElseThrow(); | |
| } | |
| }; | |
| } | |
| static <R, S, T> Handler<R, T> compose(Handler<R, S> h1, Handler<S, T> h2) { | |
| return compose(h1,h2,result->result.orElseThrow(()->new RuntimeException())); | |
| } | |
| static <R, S, T> Handler<R, T> compose(Handler<R, S> h1, Handler<S, T> h2, Function<Result<S>,S> fallbackOrThrows) { | |
| return new Handler<R, T>() { | |
| @Override | |
| public Result<T> handle(R request, ServiceContext context) { | |
| var r1 = h1.handle(request, context); | |
| var value = fallbackOrThrows.apply(r1); | |
| var r2 = h2.handle(value, context); | |
| return r2; | |
| } | |
| }; | |
| } | |
| public static <I, O> Builder<I, O> wrap(Handler<I, O> delegate) { | |
| return new Builder<>(delegate); | |
| } | |
| public static final class Builder<I, O> { | |
| private final Handler<I, O> delegate; | |
| private final List<Pre<I>> pres = new ArrayList<>(); | |
| private final List<Post<O>> posts = new ArrayList<>(); | |
| private final List<Around<I, O>> arounds = new ArrayList<>(); | |
| private final List<FinallyHook<I, O>> finals = new ArrayList<>(); | |
| private ExceptionHandler<O> exceptionHandler; | |
| private Builder(Handler<I, O> delegate) { | |
| this.delegate = Objects.requireNonNull(delegate); | |
| } | |
| // -------- PRE -------- | |
| public Builder<I, O> pre(Pre<I> pre) { | |
| pres.add(Objects.requireNonNull(pre)); | |
| return this; | |
| } | |
| public Builder<I, O> preIf(PreCondition<I> condition, Pre<I> pre) { | |
| Objects.requireNonNull(condition); | |
| Objects.requireNonNull(pre); | |
| pres.add((req, ctx) -> condition.test(req, ctx) ? pre.apply(req, ctx) : req); | |
| return this; | |
| } | |
| // -------- POST -------- | |
| public Builder<I, O> post(Post<O> post) { | |
| posts.add(Objects.requireNonNull(post)); | |
| return this; | |
| } | |
| public Builder<I, O> postIf(PostCondition<O> condition, Post<O> post) { | |
| Objects.requireNonNull(condition); | |
| Objects.requireNonNull(post); | |
| posts.add((res, ctx) -> condition.test(res, ctx) ? post.apply(res, ctx) : res); | |
| return this; | |
| } | |
| // -------- AROUND -------- | |
| public Builder<I, O> around(Around<I, O> around) { | |
| arounds.add(Objects.requireNonNull(around)); | |
| return this; | |
| } | |
| public Builder<I, O> aroundIf(PreCondition<I> condition, Around<I, O> around) { | |
| Objects.requireNonNull(condition); | |
| Objects.requireNonNull(around); | |
| arounds.add((req, ctx, next) -> condition.test(req, ctx) ? around.apply(req, ctx, next) | |
| : next.proceed(req, ctx)); | |
| return this; | |
| } | |
| // -------- ERROR / FINALLY -------- | |
| public Builder<I, O> onException(ExceptionHandler<O> handler) { | |
| this.exceptionHandler = Objects.requireNonNull(handler); | |
| return this; | |
| } | |
| public Builder<I, O> finallyDo(FinallyHook<I, O> hook) { | |
| finals.add(Objects.requireNonNull(hook)); | |
| return this; | |
| } | |
| public Handler<I, O> build() { | |
| // copia preventiva | |
| final var preChain = List.copyOf(pres); | |
| final var postChain = List.copyOf(posts); | |
| final var aroundChain = List.copyOf(arounds); | |
| final var finallyChain = List.copyOf(finals); | |
| final var onEx = this.exceptionHandler; // può essere null | |
| return new DelegatingHandler<I, O>(delegate) { | |
| @Override | |
| public Result<O> handle(I request, ServiceContext context) { | |
| final I originalReq = request; | |
| I internalReq = request; | |
| Result<O> result = null; | |
| Throwable error = null; | |
| try { | |
| // 1) PRE | |
| for (var pre : preChain) { | |
| internalReq = pre.apply(internalReq, context); | |
| } | |
| // 2) CORE + AROUND CHAIN | |
| Proceeding<I, O> core = (req, ctx) -> { | |
| // delegate | |
| Result<O> r = delegate.handle(req, ctx); | |
| // POST | |
| for (var post : postChain) { | |
| r = post.apply(r, ctx); | |
| } | |
| return r; | |
| }; | |
| Proceeding<I, O> pipeline = chainAround(aroundChain, core); | |
| result = pipeline.proceed(internalReq, context); | |
| return result; | |
| } catch (Throwable t) { | |
| // salvo l'errore causa delle stop | |
| error = t; | |
| if (onEx != null) { | |
| result = onEx.handle(t, context); | |
| return result; | |
| } | |
| // se Handler non dichiara throws, rilancio runtime | |
| if (t instanceof RuntimeException re) | |
| throw re; | |
| if (t instanceof Error err) | |
| throw err; | |
| // rilanciare con eccezione specifica e con informazioni a contorno | |
| throw new RuntimeException("Execuzion Problem",t); | |
| } finally { | |
| for (var fin : finallyChain) { | |
| try { | |
| fin.run(originalReq, internalReq, context, result, error); | |
| } catch (Throwable finErr) { | |
| // accumulare gli error e fuori dal ciclo loggare come warning! | |
| } | |
| } | |
| } | |
| } | |
| private Proceeding<I, O> chainAround(List<Around<I, O>> arounds, Proceeding<I, O> terminal) { | |
| Proceeding<I, O> next = terminal; | |
| // applica in ordine di registrazione: around1( around2( core ) ) | |
| for (int i = arounds.size() - 1; i >= 0; i--) { | |
| final Around<I, O> current = arounds.get(i); | |
| final Proceeding<I, O> capturedNext = next; | |
| next = (req, ctx) -> current.apply(req, ctx, capturedNext); | |
| } | |
| return next; | |
| } | |
| }; | |
| } | |
| } | |
| } | |
| // | |
| // Abstraction done to provide a Type Key for ServiceContext | |
| // The final implementation can be different if we need to | |
| // create custom Keys or other kind of neees | |
| // | |
| public record Key<T>(String name, Class<T> type) { | |
| public static <R> Key<R> of(String name, Class<R> type){ | |
| Objects.requireNonNull(name, "key name"); | |
| Objects.requireNonNull(type, "key type"); | |
| return new Key<R>(name,type); | |
| } | |
| } | |
| // | |
| // This is the main abstraction for ServiceContext for Handlers | |
| // The ServiceContext act as internal ServiceLocator that can be | |
| // inject or created locally on a Service. It can be enriched | |
| // thanks to a set of base methods and internal DSL to guide the Devs | |
| // The final implemantation need to be a staged DSL with different | |
| // interface to describe the different part of the final syntax | |
| // | |
| public interface ServiceContext { | |
| // Placeholder meaning "no context required". | |
| // Not a base class, just an empty implementation. | |
| ServiceContext VOID_CONTEXT = new ServiceContext() { | |
| }; | |
| default <T> Optional<T> get(Key<T> key) { | |
| return Optional.empty(); | |
| } | |
| default <T> void put(Key<T> key, T value) { | |
| } | |
| default <T> T require(Key<T> key) { | |
| throw new NoSuchElementException(key + " non available!"); | |
| } | |
| // ------------------------- | |
| // DSL entry point | |
| // ------------------------- | |
| static Builder wrap(ServiceContext base) { | |
| return new Builder(base); | |
| } | |
| // ------------------------- | |
| // DSL Builder | |
| // ------------------------- | |
| final class Builder { | |
| private final ServiceContext base; | |
| private final List<Enrichment> steps = new ArrayList<>(); | |
| private Builder(ServiceContext base) { | |
| // Normalize null to VOID_CONTEXT | |
| this.base = (base == null) ? VOID_CONTEXT : base; | |
| } | |
| // 1) Constant / primitive values (via boxing) | |
| public <T> Builder enrich(Key<T> key, T value) { | |
| Objects.requireNonNull(key, "key"); | |
| Objects.requireNonNull(value, "value"); | |
| steps.add(ctx -> ctx.put(key, value)); | |
| return this; | |
| } | |
| // 2) Supplier (no-arg lambda) | |
| public <T> Builder enrich(Key<T> key, Supplier<? extends T> supplier) { | |
| Objects.requireNonNull(key, "key"); | |
| Objects.requireNonNull(supplier, "supplier"); | |
| steps.add(ctx -> ctx.put(key, Objects.requireNonNull(supplier.get(), "supplier result"))); | |
| return this; | |
| } | |
| // 3) Context-aware lambda | |
| public <T> Builder enrich(Key<T> key, Function<ServiceContext, ? extends T> fn) { | |
| Objects.requireNonNull(key, "key"); | |
| Objects.requireNonNull(fn, "fn"); | |
| steps.add(ctx -> ctx.put(key, Objects.requireNonNull(fn.apply(ctx), "fn result"))); | |
| return this; | |
| } | |
| public ServiceContext build() { | |
| // Create a real context that delegates to base | |
| MapServiceContext ctx = new MapServiceContext(base); | |
| // Apply steps sequentially. | |
| // Later steps can depend on earlier ones using ctx.require(...) | |
| for (Enrichment s : steps) { | |
| s.apply(ctx); | |
| } | |
| return ctx; | |
| } | |
| } | |
| @FunctionalInterface | |
| interface Enrichment { | |
| void apply(ServiceContext ctx); | |
| } | |
| } | |
| // | |
| // This abstraction is the simplest base implementation to execute test | |
| // and experiment with the ServiceContext. This element make it possible | |
| // to complete the internal DSL usage but a more complete implementation | |
| // can be provided. About the design, the ServiceContext implementation | |
| // need to support the concept of Provider (to summort different kind of | |
| // Context) and more enriching-type (to now approached as function interface | |
| // Enrichment. | |
| // | |
| public final class MapServiceContext implements ServiceContext { | |
| private final ServiceContext parent; | |
| private final Map<Key<?>, Object> values = new HashMap<>(); | |
| public MapServiceContext(ServiceContext parent) { | |
| // Normalize null to VOID_CONTEXT | |
| this.parent = (parent == null) ? ServiceContext.VOID_CONTEXT : parent; | |
| } | |
| @Override | |
| public <T> Optional<T> get(Key<T> key) { | |
| Objects.requireNonNull(key, "key"); | |
| Object v = values.get(key); | |
| if (v != null) { | |
| // Safe by construction if users only put via put(enforced by Key<T>) | |
| @SuppressWarnings("unchecked") | |
| T casted = (T) v; | |
| return Optional.of(casted); | |
| } | |
| return parent.get(key); | |
| } | |
| @Override | |
| public <T> void put(Key<T> key, T value) { | |
| Objects.requireNonNull(key, "key"); | |
| // allow null values? choose one policy; here we disallow null for simplicity | |
| Objects.requireNonNull(value, "value"); | |
| values.put(key, value); | |
| } | |
| @Override | |
| public <T> T require(Key<T> key) { | |
| return get(key).orElseThrow(() -> new NoSuchElementException(key + " non available!")); | |
| } | |
| } | |
| // | |
| // A simple type to express a Warning Condition. | |
| // A Warning is related to a Success result that need to | |
| // be evaluated by Devs to become a Failure or to remain a Success | |
| // The real impl is TBD | |
| // | |
| public record Warning(String reason, Map<String,Object> metadata) { | |
| } | |
| // | |
| // Algebric Data Type for Successes/Failures | |
| // The design used here play with the fold method as base for | |
| // shared methods between "specializations" while Success/Failure | |
| // override some base methods. | |
| // The final implementation can be o not more specific | |
| // | |
| public sealed interface Result<T> permits Result.Success, Result.Failure { | |
| record Success<T>(T value, List<Warning> warnings) implements Result<T> { | |
| public Success { | |
| warnings = List.copyOf(Objects.requireNonNull(warnings, "warnings")); | |
| } | |
| public Success(T value) { | |
| this(value, List.of()); | |
| } | |
| @Override | |
| public boolean isSuccess() { | |
| return true; | |
| } | |
| @Override | |
| public <R> R fold(Function<? super Success<T>, ? extends R> onSuccess, | |
| Function<? super Failure<T>, ? extends R> onFailure) { | |
| Objects.requireNonNull(onSuccess, "onSuccess"); | |
| Objects.requireNonNull(onFailure, "onFailure"); | |
| return onSuccess.apply(this); | |
| } | |
| } | |
| record Failure<T>(String id, String reason, Map<String, Object> metadata, List<Failure<?>> errors) | |
| implements Result<T> { | |
| public Failure { | |
| Objects.requireNonNull(id, "id"); | |
| Objects.requireNonNull(reason, "reason"); | |
| metadata = Map.copyOf(Objects.requireNonNull(metadata, "metadata")); | |
| errors = List.copyOf(Objects.requireNonNull(errors, "errors")); | |
| } | |
| public Failure(String id, String reason) { | |
| this(id, reason, Map.of(), List.of()); | |
| } | |
| public Failure(String id, String reason, List<Failure<?>> errors) { | |
| this(id, reason, Map.of(), errors); | |
| } | |
| public Failure(String id, String reason, Map<String, Object> metadata) { | |
| this(id, reason, metadata, List.of()); | |
| } | |
| @Override | |
| public boolean isSuccess() { | |
| return false; | |
| } | |
| @Override | |
| public <R> R fold(Function<? super Success<T>, ? extends R> onSuccess, | |
| Function<? super Failure<T>, ? extends R> onFailure) { | |
| Objects.requireNonNull(onSuccess, "onSuccess"); | |
| Objects.requireNonNull(onFailure, "onFailure"); | |
| return onFailure.apply(this); | |
| } | |
| } | |
| boolean isSuccess(); | |
| <R> R fold(Function<? super Success<T>, ? extends R> onSuccess, | |
| Function<? super Failure<T>, ? extends R> onFailure); | |
| default boolean isFailure() { | |
| return !isSuccess(); | |
| } | |
| // ------------------------------------------- | |
| // | |
| // Per ridurre il codice scritto viene sfruttato | |
| // il metodo fold per scomporre le casistiche dei | |
| // specifici rami! | |
| // | |
| // ------------------------------------------- | |
| default T orElse(T fallback) { | |
| Objects.requireNonNull(fallback, "fallback"); | |
| return fold(s -> s.value(), f -> fallback); | |
| } | |
| default T orElseGet(Supplier<? extends T> fallback) { | |
| Objects.requireNonNull(fallback, "fallback"); | |
| return fold(s -> s.value(), f -> fallback.get()); | |
| } | |
| default T orElseThrow(Supplier<? extends RuntimeException> failer) { | |
| Objects.requireNonNull(failer, "failer"); | |
| return fold(s -> s.value(), f -> { | |
| throw failer.get(); | |
| }); | |
| } | |
| default T orElseThrow(Function<? super Failure<T>, ? extends RuntimeException> failer) { | |
| Objects.requireNonNull(failer, "failer"); | |
| return fold(s -> s.value(), f -> { | |
| throw failer.apply(f); | |
| }); | |
| } | |
| default <U> Result<U> map(Function<? super T, ? extends U> mapper) { | |
| Objects.requireNonNull(mapper, "mapper"); | |
| return fold(success -> Result.success(mapper.apply(success.value()), success.warnings()), | |
| failure -> Result.failure(failure.id(), failure.reason(), failure.metadata(), failure.errors())); | |
| } | |
| default <U> Result<U> flatMap(Function<? super T, ? extends Result<U>> mapper) { | |
| Objects.requireNonNull(mapper, "mapper"); | |
| return fold( | |
| success -> { | |
| Result<U> next = Objects.requireNonNull(mapper.apply(success.value()), "mapper result"); | |
| return next.fold( | |
| successInner -> Result.success(successInner.value(), concat(success.warnings(), successInner.warnings())), | |
| failureInner -> failureInner | |
| ); | |
| }, | |
| failure -> Result.failure(failure.id(), failure.reason(), failure.metadata(), failure.errors()) | |
| ); | |
| } | |
| // ----- Factory methods ----- | |
| static <R> Success<R> success(R value) { | |
| return new Success<>(value); | |
| } | |
| static <R> Success<R> success(R value, List<Warning> warnings) { | |
| return new Success<>(value, warnings); | |
| } | |
| static <R> Failure<R> failure(String id, String reason) { | |
| return new Failure<>(id, reason); | |
| } | |
| static <R> Failure<R> failure(String id, String reason, List<Failure<?>> errors) { | |
| return new Failure<>(id, reason, errors); | |
| } | |
| static <R> Failure<R> failure(String id, String reason, Map<String, Object> metadata, List<Failure<?>> errors) { | |
| return new Failure<>(id, reason, metadata, errors); | |
| } | |
| static <R> Failure<R> failure(Success<R> success, Function<Success<R>, Failure<R>> mapper) { | |
| Objects.requireNonNull(mapper, "mapper"); | |
| Objects.requireNonNull(success, "success"); | |
| List<Warning> warnings = success.warnings(); | |
| if (warnings == null || warnings.isEmpty()) { | |
| throw new IllegalArgumentException("Passed Success instance has no warnings"); | |
| } | |
| return mapper.apply(success); | |
| } | |
| //------------------------ | |
| // commodity | |
| //------------------------ | |
| // already opimized for memory, immutability and speed | |
| private static <E> List<E> concat(List<E> a, List<E> b) { | |
| if (a.isEmpty()) return b; | |
| if (b.isEmpty()) return a; | |
| var out = new ArrayList<E>(a.size() + b.size()); | |
| out.addAll(a); | |
| out.addAll(b); | |
| return java.util.Collections.unmodifiableList(out); | |
| } | |
| } | |
| // | |
| // This is the main abstraction for Services. | |
| // Trying also to isolate/decoupling from Spring Boot | |
| // In future release can be richer than actual | |
| // | |
| public interface ServiceLifeCycle extends AutoCloseable{ | |
| public void init(); | |
| @Override | |
| public void close(); | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment