Сегодня я расскажу, как описывать преобразования над данными так, чтобы их можно было многократно использовать. На птичьем языке это может звучать как «создание кастомных операторов».

Возьмём простой пример, в котором есть повторяющаяся операция.

Observable
    .just(1, 2, 3, 4, 5, 6, 7)
    .map(i -> i * 2) // раз
    .map(i -> i * 2) // два
    .map(i -> i + 1)
    .map(i -> i % 3 == 0)
    .map(b -> b ? "a" : "b")
    .subscribe(o -> System.out.println(o.toString()));

Чтобы понять, что здесь происходит, преобразуем этот код в привычный любому олдскульному программисту вид:

Observable<Integer> o1 = Observable.just(1, 2, 3, 4, 5, 6, 7);
Observable<Integer> o2 = o1.map(i -> i * 2);
Observable<Integer> o3 = o2.map(i -> i * 2);
Observable<Integer> o4 = o3.map(i -> i + 1);
Observable<Boolean> o5 = o4.map(i -> i % 3 == 0);
Observable<String> o6 = o5.map(b -> b ? "a" : "b");
o6.subscribe(o -> System.out.println(o.toString()));

Сразу становится понятно, что любой оператор — это преобразования одного Observable в другой. И если мы хотим сохранить на будущее преобразование, которое удваивает целое число, его можно описать в виде функции:

static Observable<Integer> doubleIt (Observable<Integer> o) {
    return o.map(i -> i * 2);
}

Теперь эту функцию можно использовать:

Observable<Integer> o1 = Observable.just(1, 2, 3, 4, 5, 6, 7);
Observable<Integer> o2 = doubleIt(o1);
Observable<Integer> o3 = doubleIt(o2);
Observable<Integer> o4 = o3.map(i -> i + 1);
Observable<Boolean> o5 = o4.map(i -> i % 3 == 0);
Observable<String> o6 = o5.map(b -> b ? "a" : "b");
o6.subscribe(o -> System.out.println(o.toString()));

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

Observable
     .just(1, 2, 3, 4, 5, 6, 7)
     .unknownSpecialOperator( doubleIt() )
     .unknownSpecialOperator( doubleIt() )
     .map(i -> i % 3 == 0)
     .map(b -> b ? "a" : "b")
     .subscribe(o -> System.out.println(o.toString()));

Также очевидно, что в скобках должна быть лямбда, вроде такой: o -> doubleIt(o). Но это ещё не ответ: лямбда в Java — это всегда реализация какого-то интерфейса. Нужно понять, какого?

Нужный нам оператор называется compose(), а интерфейс — Transformer. Зная это, мы можем написать работающий код:

Observable.Transformer<Integer, Integer>
    doubleItTransformer = o -> o.map(i -> i * 2);
 
Observable
    .just(1, 2, 3, 4, 5, 6, 7)
    .compose(doubleItTransformer)
    .compose(doubleItTransformer)
    .map(i -> i % 3 == 0)
    .map(b -> b ? "a" : "b")
    .subscribe(o -> System.out.println(o.toString()));

Теперь мы умеем создавать наши собственные операторы и использовать их по мере необходимости. Это позволяет структурировать код и избавиться от самоцитирования.

Ещё один пример

Оператор может быть сложнее, чем просто лямбда. Сделаем оператор, печатающий проезжающий по конвейеру элемент:

public static class Logger<T> implements Observable.Transformer<T, T> {
    String prefix;
 
    public Logger(String prefix) {
        this.prefix = prefix;
    }
 
    @Override
    public Observable<T> call(Observable<T> observable) {
        return observable.doOnNext(o -> System.out.println(prefix + " : " + o));
    }
}

Воспользуемся теперь этим оператором:

Observable
    .just(1, 2, 3, 4, 5, 6, 7)
    .compose(doubleItTransformer)
    .compose(new Logger<>("after first double")) // первый 
    .compose(doubleItTransformer)
    .map(i -> i % 3 == 0)
    .compose(new Logger<>("check %3")) // второй
    .map(b -> b ? "a" : "b")
    .subscribe(o -> System.out.println(o.toString()));

Ссылки