Observable, Transformer, .compose()
Сегодня я расскажу, как описывать преобразования над данными так, чтобы их можно было многократно использовать. На птичьем языке это может звучать как «создание кастомных операторов».
Возьмём простой пример, в котором есть повторяющаяся операция.
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()));