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(...))
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.