2

So imagine this,

Given a class hierarchy:

S (abstract)
A extends S
B extends S

Let's say we have a chain of connected components handling objects of types of A and B. For example: K1.handle(A) -> K2.handle(A) -> K3.handle(A) meaning that K1 takes in A and calls K2 with A which calls K3 with A.

Similarly we have: K1.handle(B) -> K2.handle(B) -> K3.handle(B)

Thing is K2's logic does not depend on the sub type of S. So we could replace K2.handle(A) and K2.handle(B) with K2.handle(S). But the problem is that at some point we need to do type checking of the actual object type to call correct K3 method.

How does one solve this without doing if or switch etc. statements that inspect the type/properties of the actual objects?

Is it the visitor pattern I need to look into?

To give a concrete example of the case. Let's say K1 is a REST controller with different POST endpoints for A and B. It deserialises the payload and validates the data, then it passes the object to K2 for further processing. All the steps included in K2 (and other components it calls) are happy to work with objects of type S only. But in the very end at K3 we need to know the actual type of S.

Raipe
  • 786
  • 1
  • 9
  • 22
  • *"K2's logic does not depend on the sub type of S"* --- Sure it does, because calling `K3.handle()` is part of that logic, so it does depend on subtype of `S`. – Andreas Jun 18 '20 at 07:43
  • This question is substantially too abstract to provide a good answer. Common patterns include having a framework resolve the handlers (like Spring `ApplicationEvent`) or using Chain of Responsibility. – chrylis -cautiouslyoptimistic- Jun 18 '20 at 07:49
  • As long as the method called by K3 is defined in S, then both A and B will work fine. – NomadMaker Jun 18 '20 at 08:17

4 Answers4

0

But in the very end at K3 we need to know the actual type of S.

So all the logic in K2.handle() comes before the call to K3.handle().

Also, since K3.handle(A) and K3.handle(B) cannot be K3.handle(S), according to the wording of the questing, it would appear that K3.handle() is an overloaded method, even though that's not explicitly stated in the question.

One way to do this is then to move common logic to an internal helper method:

public class K2 {
    public void handle(A a) {
        handleInternal(a);
        K3.handle(a);
    }
    public void handle(B b) {
        handleInternal(b);
        K3.handle(b);
    }
    private void handleInternal(S s) {
        // K2 logic here
    }
}

Or you could use a method reference, allowing the call to K3 to be embedded in the K2 logic, not just at the end:

public class K2 {
    public void handle(A a) {
        handleInternal(a, K3::handle); // method reference to K3.handle(A)
    }
    public void handle(B b) {
        handleInternal(b, K3::handle); // method reference to K3.handle(B)
    }
    private <T extends S> void handleInternal(S s, Consumer<T> k3_handle) {
        // Some K2 logic here
        k3_handle.accept(s);
        // More K2 logic here
    }
}
Andreas
  • 154,647
  • 11
  • 152
  • 247
0

Given sample classes:

class S {}

class A extends S {
    public int getAInfo() {
        return 1;
    }
}

class B extends S {
    public int getBInfo() {
        return 2;
    }
}

We can make an interface K that takes advantage of a bounded type parameter as follows:

@FunctionalInterface
interface K<T extends S> {
    void handle(T s);

    @SafeVarargs
    static <FT extends S> K<FT> compose(K<FT> finalK, K<? super FT>... ks) {
        return s -> {
            Stream.of(ks).forEach(k -> k.handle(s));
            finalK.handle(s);
        };
    }
}

We can then use K.construct to make a new instance of K that will forward an s throughout an entire pipeline of Ks. This pipeline of Ks is structured such that all but the last k are generic handlers, accepting any instance of S. Only the last one will accept an instance of a specific subclass of S, namely FT (abbreviation for "final type").

With this K definition, all the following statements compile:

K<S> k1 = s -> System.out.println("K1 handled.");
K<S> k2 = s -> System.out.println("K2 handled.");
K<A> k3A = a -> System.out.println(a.getAInfo());
K<B> k3B = b -> System.out.println(b.getBInfo());

K.compose(k3A, k1, k2).handle(new A()); // Propagates instance of A through pipeline
K.compose(k3B, k1, k2).handle(new B()); // Propagates instance of B through pipeline

yet the ad-hoc polymorphism of all the Ks in the pipeline that are not the last k is still maintained, as both these statements compile as well:

k1.handle(new S());
k2.handle(new S());

One downside I see in this solution is that, unlike the other answer, the K handlers must be strictly sequentially executed, and calls to the next k's handler cannot happen in the middle of the previous one. Through I still think you can extend this type of design to enable support for that.

Mario Ishac
  • 5,060
  • 3
  • 21
  • 52
0

Yes, this is a visitor pattern.

The visitor is

interface K {
    void handle(A a);
    void handle(B b);
}

and K1, K2, K3 implement it.

What S needs is a method visit(K) with the implementations

class A implements S {
    public void visit(K visitor) {
        visitor.handle(this); // calls the handle(A) method
    }
}
class B implements S {
    public void visit(K visitor) {
        visitor.handle(this); // calls the handle(A) method
    }
}

Your list (ie your chain) is just a List<K> with something like

List<K> handlers = Arrays.asList(new K1(), new K2(), new K3());
S sample = new A();
for (K visitor : handlers) {
    sample.visit(visitor);
}
sample = new B();
for (K visitor : handlers) {
    sample.visit(visitor);
}

Here's a running version.

daniu
  • 14,137
  • 4
  • 32
  • 53
  • Should it be `sample.visit(visitor)`, since `visit` is defined under `S`? – Mario Ishac Jun 18 '20 at 08:33
  • @MarioIshac Yes, corrected, thank you. There might be other minor issues since I just typed it down, added a runnable version at ideone. – daniu Jun 18 '20 at 08:39
0

I never really appreciated Visitor pattern, so here's an alternative with using functional composition and Java generics. I don't think you need to add methods to existing classes or interfaces using this approach.

You can use the fact that K3.handle(A) is a Consumer<A> which you can easily transform into a Function<A,A> by using Function<A,A> handleA = a -> k3.handle(a); return a;. This returns the input element typed as A, which you can then compose() with the other functions k2::handle and k1::handle. You cannot do that as a chained call on a single line because that breaks Java's type inference, but this works:

K k1 = new K1();
K k2 = new K2();
// construct A chain
K3A specificA = new K3A();
Function<A, A> handleA = a -> specificA.handle(a);
handleA = handleA.compose(k2::handle);
handleA = handleA.compose(k1::handle);

In this case,

class K {
    <T> T handle(T item);
}
class K3A {
    A handle(A item) { ... }
}

but if the handle() methods are void, you can wrap them into lambdas as I've described above.

Not sure if you find this more readable/maintainable, but again I dislike Visitor so much that I had to try.

Here's a running sample and here a fork with the wrapped version (which represents exactly the case you presented IIUC, and without any code change in S, A, B or any of the Ks).

daniu
  • 14,137
  • 4
  • 32
  • 53
  • I will experiment with this, but I dislike the fact that there needs to be someone who knows that k1,k2 and k3 exist and how they are linked. It would be agains the current architecture of Spring injected services calling services. – Raipe Jun 21 '20 at 17:03
  • For example in this approach my REST API endpoint (K1) would need to have instance of the database access (K3). – Raipe Jun 21 '20 at 17:26
  • @Raipe Well someone will need to fill the list in your example as well, no? The way I do this kind of thing in Spring is to have a `@Configuration` that provides the single `K` methods and one that creates the composition end object (ie, the constructed chain). – daniu Jun 21 '20 at 18:31