How to handle a fluent interface where each step can be a terminal operation?

roookeee :

I am building a fluent API that roughly works like this (assuming a class Person with a getter getId exists that returns a Long):

String result = context.map(Person::getId)
     .pipe(Object::toString)
     .pipe(String::toUpperCase)
     .end(Function.identity())

As you can see only the .end-function acts a terminal operator. This clutters the overall usage of said API as I often have to end in a .end(Function.identity())-call even though the preceding .pipe-call already has the right type.

Is there any way to make a fluent-API that enables a part of it to be both a terminal operator and a 'bridge-operator'? I just dont want to clutter the API with specialized pipe-variants like pipeTo (a pipe that only accepts a Function<CurrentType, ExpectedType> and internally calls .end)that emulate said behaviour as it forces the user to think about a very specific part of the API that seems unnecessary to me.

EDIT: A simplified context-implementation as requested:

class Context<InType, CurrentType, TargetType> {
    private final Function<InType, CurrentType> getter;

    public Context(Function<InType, CurrentType> getter) {
        this.getter = getter;
    }

    public <IntermediateType> Context<InType, IntermediateType, TargetType>
    pipe(Function<CurrentType, IntermediateType> mapper) {

        return new Context<>(getter.andThen(mapper));
    }

    public Function<InType, TargetType> end(Function<CurrentType, TargetType> mapper) {
        return getter.andThen(mapper);
    }
}

//usage
Function<Person, String> mapper = new Context<Person, Long, String>(Person::getId)
    .pipe(Object::toString)
    .pipe(String::toUpperCase)
    .end(Function.identity());

mapper.apply(new Person(...))
roookeee :

The main problem I had was that any pipe step could be a terminal operation. As outlined in the discussions below each answer and the main post: using a function with the same name twice and one being a terminal operation just is not possible in java.

I banged my head against this problem and tried multiple approaches each of which did not work. Thats when I realized what I am doing is essentially the same as Javas Stream-API: you have an origin (source), do some fancy stuff (pipe) and then end (collect). If we apply the same scheme to my question there is no need for pipe to be a terminal operation, we just need another operation (e.g. end) which serves as the end point. As I had some extended requirements on when end is possible (the current type must match another type) I implemented end by only allowing a context specific funtion for which there is only one sane implementation available (hard to explain). Here is an example of the current implementation (pipe has since been renamed to map and end to to):

Mapper<Person, PersonDTO> mapper = Datus.forTypes(Person.class, PersonDTO.class).immutable(PersonDTO::new)
    .from(Person::getFirstName).to(ConstructorParameter::bind)
    .from(Person::getLastName)
        .given(Objects::nonNull, ln -> ln.toUpperCase()).orElse("fallback")
        .to(ConstructorParameter::bind)
    .build();

As you can see .to acts as the terminal operator and ConstructorParameter::bind would complain about a type mismatch if the current type would not match the expected type.

See here for the to part, here for an implementation of ConstructorParameter and here how it is defined.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=149432&siteId=1