Как обращаться с свободным интерфейсом, где каждый шаг может быть терминальной операцией? - PullRequest
2 голосов
/ 06 мая 2019

Я создаю свободный API, который примерно работает следующим образом (предполагается, что существует класс Person с геттером getId, который возвращает Long):

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

Как видите, только функция .end действует как оператор терминала. Это мешает общему использованию указанного API, так как мне часто приходится заканчивать .end(Function.identity()) -колл, даже если предыдущий .pipe -колл уже имеет правильный тип.

Есть ли способ создать свободный API, позволяющий его части быть одновременно оператором терминала и «оператором моста»? Я просто не хочу загромождать API специализированными pipe -вариантами, такими как pipeTo (канал, который принимает только Function<CurrentType, ExpectedType> и внутренне вызывает .end), эмулирующим указанное поведение, так как он заставляет пользователя думать о очень определенная часть API, которая мне кажется ненужной.

EDIT: Упрощенная реализация контекста по запросу:

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

Ответы [ 3 ]

1 голос
/ 06 мая 2019

Вы не можете определять методы в Java с одинаковыми именами и разными типами возвращаемых данных. Ваши методы, вероятно, возвращают что-то вроде Wrapped<T>, и вместо этого вы хотите вернуть T. В общем, я бы порекомендовал иметь что-то вроде *andEnd(...) для каждого из ваших методов. Таким образом, pipeAndEnd(...) выполнит конвейер и завершит работу терминала. Это, вероятно, станет утомительным, поэтому вы можете захотеть взглянуть на генерацию кода, если у вас много методов.

& # x200B;

С другой стороны, похоже, что вы реализуете свою собственную версию Stream API. Если вы не делаете это для образовательных целей, почти всегда лучше использовать существующий и хорошо протестированный / документированный код (особенно часть кода стандартного jdk), чем повторно реализовать свою собственную версию того же самого.

1 голос
/ 06 мая 2019

Если я понимаю, что вы ищете, я бы перегрузил 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;
    }
}
0 голосов
/ 29 мая 2019

Основная проблема, с которой я столкнулся, заключалась в том, что любой шаг pipe мог быть терминальной операцией.Как указано в обсуждениях ниже, каждый ответ и основной пост: использование функции с одним и тем же именем дважды, а одна - терминальная операция, просто невозможна в Java.

Я ударился головой об эту проблему и попробовал несколько подходов, каждый из которых не работал.Вот когда я понял, что я делаю, по сути, то же самое, что и Javas Stream -API: у вас есть источник (источник), вы делаете какие-то причудливые вещи (труба), а затем заканчиваете (собираете).Если мы применяем ту же схему к моему вопросу, нет необходимости, чтобы pipe была терминальной операцией, нам просто нужна другая операция (например, end), которая служит конечной точкой.Поскольку у меня были некоторые расширенные требования, когда конец возможен (текущий тип должен соответствовать другому типу), я реализовал end, разрешив только контекстно-зависимую функцию, для которой доступна только одна вменяемая реализация (трудно объяснить).Вот пример текущей реализации (с тех пор pipe был переименован в map и end в 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, здесь для реализации ConstructorParameter и здесь как это определено.

...