如何处理流利的界面,使每个步骤都可以成为终端操作?

时间:2019-05-06 19:23:00

标签: java generics functional-programming

我正在构建一个大致像这样的流利的API(假设存在一个带有吸气剂Person的类getId并返回一个Long):

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

如您所见,只有.end函数充当终端操作员。即使前一个.end(Function.identity())调用已经具有正确的类型,我也常常不得不以.pipe调用结尾,这使该API的整体用法混乱。

是否有什么方法可以使Fluent-API成为终端操作员和“桥接操作员”?我只是不想用像pipe这样的专用pipeTo-变量(仅接受Function<CurrentType, ExpectedType>并内部调用.end的专用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(...)) -变量使API混乱)迫使用户考虑对我来说似乎不必要的API的非常特定的部分。

编辑: 根据要求简化的上下文实现:

EventListener

3 个答案:

答案 0 :(得分:1)

如果我了解您的需求,我会重载end()并摆脱最后的函数组成:

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

再想一想,我认为Context类的第三个类型参数可以消除,因为仅在方法级别需要中间类型。检查一下:

class OtherContext<I, O> {

    private final Function<I, O> getter;

    public OtherContext(Function<I, O> getter) {
        this.getter = getter;
    }

    public <T> OtherContext<I, T> pipe(Function<O, T> mapper) {

        return new OtherContext<I, T>(getter.andThen(mapper));
    }

    public <T> Function<I, T> end(Function<O, T> mapper) {
        return getter.andThen(mapper);
    }

    public Function<I, O> end() {
        return getter;
    }
}

答案 1 :(得分:1)

您不能在Java中定义具有相同名称和不同返回类型的方法。您的方法可能返回类似Wrapped<T>的内容,而您想返回T。通常,我可能建议为每种方法都使用类似*andEnd(...)的方法。因此pipeAndEnd(...)将完成管道操作,然后以终端操作结束。这样做可能会很乏味,因此如果您有很多方法,您可能希望研究一些代码生成。

另一方面,似乎您正在实现自己版本的Stream API。除非您这样做是出于教育目的,否则使用现有的,经过良好测试/记录的代码(尤其是标准jdk的代码部分)总是比重新实现自己的同一版本更好。

答案 2 :(得分:0)

我遇到的主要问题是任何pipe步骤都可能是终端操作。正如下面的讨论中概述的那样,每个答案和主要帖子:在Java中不可能两次使用具有相同名称的函数,并且将其用作终端操作。

我为解决这个问题而head之以鼻,并尝试了多种方法,每种方法均无效。那就是当我意识到我在做什么与Java Stream-API基本上相同时:您有一个源(源),做一些花哨的东西(管道),然后结束(收集)。如果我们对问题采用相同的方案,则无需pipe进行终端操作,我们只需要另一个操作(例如end)作为终点即可。由于我对何时可能结束有一些扩展的要求(当前类型必须与另一种类型匹配),因此我通过只允许特定于上下文的功能实现了end,而对于该功能而言,只有一种合理的实现可用(很难解释)。这是当前实现的示例(pipe已重命名为mapend重命名为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();

如您所见,.to充当终端运算符,如果当前类型与预期类型不匹配,ConstructorParameter::bind将抱怨类型不匹配。

关于to部分,请参见here,有关ConstructorParameterhere的定义方式,请参见here