Реактивное программирование
Как только ООП-языки доросли до массового применения, разработчики осознали, насколько иногда не хватает возможностей С-подобных языков. Поскольку написание кода в стиле функционального программирования серьезно разрушает качество ООП-кода, а значит, и поддерживаемость проекта, был придуман гибрид — реактивное программирование.
Парадигма реактивной разработки строится на идее постоянного отслеживания изменений состояния объекта. Если такие изменения произошли, то все заинтересованные объекты должны получить уже обновленные данные и работать только с ними, забыв про старые.
Хорошим примером идеи реактивного программирования может служить Excel-таблица. Если связать несколько ячеек одной формулой, результат вычисления будет меняться каждый раз, когда изменятся данные в этих ячейках. Для бухгалтерии такое динамическое изменение данных — привычное дело, но для программистов это скорее исключение.
a=3; b=4;
c=a + b;
F1(c);
a=1;
F2(c);
В этом примере функции F1 и F2 будут работать с разными значениями переменной C. Часто требуется, чтобы у обеих функций были только самые актуальные данные, — реактивное программирование позволит без изменения логики самих функций сразу же вызвать F1 с новыми параметрами. Такое построение кода дает приложению возможность моментально реагировать на любые изменения, что сделает его быстрым, гибким и отзывчивым.
ReactiveX
Воплощать с нуля идеи реактивного программирования может быть довольно хлопотно — есть подводные камни, да и времени это займет прилично. Поэтому для многих разработчиков эта парадигма оставалась только теоретическим материалом, пока не появился ReactiveX.
Фреймворк ReactiveX — это инструмент для реактивного программирования, работающий со всеми популярными ООП-языками. Сами создатели называют его мультиплатформенным API для асинхронной разработки, основанным на паттерне «Наблюдатель» (Observer).
Если термин «реактивное программирование» — это своего рода теоретическая модель, то паттерн «Наблюдатель» — готовый механизм отслеживания изменений в программе. А отслеживать их приходится довольно часто: загрузку и обновление данных, оповещения о событиях и так далее.
Паттерн «Наблюдатель» существует примерно столько же, сколько и само ООП. Объект, состояние которого может поменяться, называется издателем (популярный перевод термина Observable). Все остальные участники, которым интересны эти изменения, — подписчики (Observer, Subscriber). Для получения уведомлений подписчики регистрируются у издателя, явно указывая свой идентификатор. Издатель время от времени генерирует уведомления, которые им же рассылаются по списку зарегистрированных подписчиков.
Собственно, создатели ReactiveX не придумали ничего революционного, они просто удобно реализовали паттерн. И хотя во многих ООП-языках, и в Java в частности, есть готовые реализации паттерна, в этом фреймворке присутствует дополнительный «тюнинг», который превращает «Наблюдатель» в очень мощный инструмент.
RxAndroid
Порт библиотеки ReactiveX для мира Android называется rxAndroid и подключается, как всегда, через Gradle.
compile 'io.reactivex:rxandroid:1.1.0'
Издатель, генерирующий уведомления, здесь задается с помощью класса Observable. У издателя может быть несколько подписчиков, для их реализации воспользуемся классом Subscriber. Стандартное поведение для Observable — выпустить одно или несколько сообщений для подписчиков, а затем завершить свою работу или выдать сообщение об ошибке. В качестве сообщений могут быть как переменные, так и целые объекты.
rx.Observable myObserv = rx.Observable.create(
new rx.Observable.OnSubscribe<Object>() {
@Override
public void call(Subscriber<? super Object> subscriber) {
subscriber.onNext("Hello");
subscriber.onNext("world");
subscriber.onCompleted();
}
}
);
В данном случае издатель myObserv сначала отправит строки hello и message, а затем сообщение об успешном завершении работы. Издатель может вызвать методы onNext()
, onCompleted()
и onEror()
, поэтому у подписчиков они должны быть определены.
Subscriber mySub = new Subscriber<String>() {...
@Override
public void onNext(String value)
{Log.e("got data", " " + value);}
};
Все готово для работы. Осталось связать объекты между собой — и «Hello, world!» в реактивном программировании готов!
myObserv.subscribe(mySub);
Надо сказать, что это был очень простой пример. В ReactiveX есть множество вариантов поведения всех участников паттерна: фильтрация, группирование, обработка ошибок. Пользу от реактивного программирования можно ощутить, только попробовав его в деле. Приступим к задаче посерьезнее.
MVP
Академические примеры работы ReactiveX ты найдешь на официальном сайте фреймворка, а в этой статье мы с тобой разберем более приземленный пример.
Как показывает практика, создавая что-то сложнее студенческого курсовика, следует строить приложение с использованием еще одного паттерна — MVP (Model, View, Presenter). Он позволяет разбить ООП-проект на отдельные, слабо связанные и легко заменяемые логические блоки кода.
Как правило, каждый из блоков в MVP имеет свое предназначение — это помогает использовать паттерн с полной эффективностью.
- Model — поставщик данных в приложении. Внутри него аккумулируется вся механика запросов: сетевое взаимодействие, работа с файлами и прочее.
- View отвечает за UI и все, что рядом. В этом блоке генерируются запросы на выдачу необходимых данных и, при желании, ведется их финальная обработка: сортировка, выборка отдельных значений и так далее.
- Presenter действует как посредник. Поскольку View и Model мало что знают друг и о друге, этот блок служит своего рода «переходником», перенаправляя запросы от одного к другому.
ООП-код обязательно нужно стараться делать поддерживаемым, иначе программе сложно будет дожить хотя бы до версии 1.1. Разработчику часто приходится вносить изменения уже во время рабочего процесса: добавлять кеширование, изменять дизайн, делать новое меню и так далее. Следование принципам MVP помогает изменять логику приложения практически без боли и потери времени.
При этом редкое приложение обходится без длительных вычислений, а значит, разработчику придется еще и как-то управляться с дополнительными потоками. Наш журнал уже не раз затрагивал эту тему, и ты должен быть в курсе возможных проблем: взаимные блокировки, перерасход памяти, потеря результатов... ReactiveX позволяет не только легко внедрить паттерн «Наблюдатель», но и раскидать вычисления по разным потокам, причем реализацию многопоточности он берет на себя.
Для демонстрации сказанного создадим небольшой проект, в котором код построен в соответствии с паттерном MVP, а все тяжелые вычисления делегированы фреймворку. Начнем с блока Model. При использовании MVP нужно стремиться передавать как можно меньше данных между логическими блоками. Достаточно ограничиться, к примеру, ссылкой на ресурс для доступа к данным.
public class Model {
НeavyOperation heavyOp;
public Model(String baseUrl)
{
...
Источников, генерирующих данные, наверняка окажется несколько, поэтому внутри Model будет объект класса HeavyOperation.
heavyOp = new HeavyOperation();
Как понятно из названия, HeavyOperation будет работать очень долго, а в Android длительные операции нужно всегда выполнять в отдельном потоке.
private UserList prepareList()
{
UserList repoList = new UserList();
Thread.sleep(10000);
repoList.addItem(new UserItem("jack"));
addItem(new UserItem("bob"));
...
return repoList;
}
Кстати, некоторые библиотеки самостоятельно решают такие проблемы: к примеру, при загрузке файла с помощью Retrofit не надо что-то придумывать, она сама загрузит файл в новом потоке. А когда нужно будет работать с файловой системой или базой данных, поможет ReactiveX.
Используем фреймворк, чтобы выполнить все эти вычисления где-нибудь в фоне, а результат вернуть обратно — в главный поток. Издатель выполнит у себя метод prepareList
, а результат, объект класса UserList, уйдет подписчикам.
Observable<UserList> observableHeavyOp;
...
public Observable<UserList> doTheJob(){
observableHeavyOp = Observable.create(new Observable.OnSubscribe<UserList>() {
@Override
public void call(Subscriber<? super UserList> subscriber)
{subscriber.onNext(prepareList());}
});
}
Многопоточность в ReactiveX создается просто: указываем, что вычисления нужно выполнять в отдельном потоке (subscribeOn), а результат вернуть в главный поток (observeOn).
observableHeavyOp.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread());
Блок Model должен не только создать объект на основе класса HeavyOperation, но и реализовать get-метод, дающий доступ к нему извне.
public HeavyOperation getAPI()
{... return heavyOp;}
Теперь перейдем к Presenter, связующему блоку. Здесь необходимо сохранить ссылки на блоки Model и View, а также связать их между собой get-методами.
public PresenterSimple(MainActivity v, Model model)
{
this.view=v;
this.model = model;
}
Генерация данных будет происходить только по команде от View, для этого создан метод loadDatawithRx. Вполне возможно, действий будет больше, чем просто генерация списка. К примеру, имеет смысл сразу запустить на экране анимацию загрузки данных.
public void loadDatawithRx(String url) {
view.inProgress(true);
В Presenter нужно прошить из Model ссылку на объект издателя, для этих целей и были созданы get-методы.
Observable<UserList> myObservable = model.getAPI().doTheJob();
Издатель уже есть, осталось сгенерировать подписчика. Создадим его тут же, используя метод subscribe и класс Observer.
myObservable.subscribe(new Observer<UserList>() {
...
public void onNext(UserList repoList) {
view.inProgress(false);
view.showResult(repoList);
...
}
});
И вот сила ReactiveX в действии — нам удалось связать паттерном «Наблюдатель» блоки Model и View. Метод doTheJob сгенерирует новый поток, создаст в нем список, а потом данные сразу же попадут во View через метод showResult. При этом приложение продолжает жить активной жизнью, без задержек и перерасхода памяти.
Кстати, мы еще совсем ничего не знаем о блоке View. По сути, код тут может быть совершенно произвольным, никак не завязанным на реактивном программировании. Все, что от View требуется, — реализовать методы inProgress и showResult, вызываемые из связующего блока.
public void inProgress(Boolean inProgress)
{
int visibility=(inProgress)? View.VISIBLE : View.GONE;
progress.setVisibility(visibility);
}
public void showResult(UserList list)
{
view.setText(...);
...
}
Чтобы паттерн заработал, осталось только инициализировать объекты на основе созданных классов — сделать это можно где-нибудь в MainActivity.
model = new Model("url");
presenterSimple = new PresenterSimple(this, model);
presenterSimple.loadDatawithRx("url");
Созданной программе не страшны (в пределах разумного) ни масштабирование, ни расширение функциональности. Задачи, ради которых раньше пришлось бы долго-долго править код и вставлять костыли, теперь решаются намного проще и изящнее с помощью реактивного программирования.
Лямбда-выражения
При работе с ReactiveX каждый раз приходится создавать новые объекты и тут же переопределять какие-то методы. В результате возникает нагромождение малоиспользуемых объектов и такой код плохо читается.
В Java 8 появились так называемые лямбда-выражения, которые очень похожи на функции в С-подобных языках. Не так давно они стали доступны и в Android (с обратной совместимостью API до версии 23), теперь можно сделать код более читаемым и лаконичным.
observableHeavyOp=Observable.create(v->v.onNext(prepareList()))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread());
...
myObservable.subscribe(userList ->
{view.inProgress(false);view.showResult(userList);}
);
ReactiveX v.2
Этот фреймворк очень популярен и активно развивается. Совсем недавно вышла его новая версия, разработчики добавили новые возможности. К примеру, если требуется создать издателя, генерирующего не более одного сообщения подписчикам, удобней пользоваться новым классом Maybe. В целом отличия выплывают уже после активного использования фреймворка.
Outro
ReactiveX может выручить в ситуациях, которые раньше казались практически нерешаемыми. Попробуй написать что-нибудь самостоятельно, и ты поймешь, насколько это мощный инструмент. Как обычно, на нашем сайте будет весь исходный код созданного сегодня приложения. Если останутся вопросы, пиши мне на почту, разберемся вместе. Удачи!