Мартин Голдинг
При разработке одного из проектов, использующего GooglePlacesAPI, у меня возникла проблема организации сетевого взаимодействия между моим Android–приложением и API сервера. Интуиция и “лапша” из AsyncTask’ов подсказывала, что должны быть другие способы организации такого рода взаимодействия. Так я наткнулся на шаблон проектирования CommandProcessor. Об использовании этого паттерна проектирования в Android-приложениях я и хочу рассказать.
Для начала, опишу задачу, которую нужно было решить. Требовалось написать приложение, использующее Google Places API, показывающее превью любого места на карте, которое выбрал пользователь, а далее, если пользователь захочет получить больше информации (например просмотреть больше картинок), то подгружать картинки по заданному Id выбранного места, и показывать уже все картинки, относящиеся к выбранному месту. Самым очевидным на тот момент для меня способом было использование AsyncTask. Но после некоторых попыток стало ясно, что должны быть и другие способы, более удобные. Использование AsyncTask’ ов было неудобным потому что:
1) Чтобы получить превью какого-нибудь места, необходимо было сначала сделать запрос для получения информации о всех местах, которые находились рядом с выбранным пользователем местом.
2) По полученным Id сформировать и отправить запрос о получении фотографии-превью.
3) При клике на превью получить все картинки относящиеся к этому месту.
Таким образом, при использовании AsyncTask’ов получался некий «водопад» и пришлось бы использовать один AsyncTask внутри другого. И тогда, погуглив, я нашел информацию о паттерне Command Processor, который отлично справляется с задачами, описанными выше.
Паттерн проектирования CommandProcessor разделяет запросы к сервису от их выполнения. Главный компонент паттерна — CommandProcessor, управляет запросами, планирует их выполнение, а также предоставляет дополнительный сервис, например, хранение запросов для позднего выполнения или отмены запроса. Диаграмма, заимствованная из [1] показывает отношения между компонентами паттерна:
Области применения паттерна
- Программы, в которых пользователь взаимодействует с системой через графический интерфейс. Например: текстовые редакторы, приложения для офиса. Команды (здесь и далее команды тоже самое что и запросы) используются для выполнения какой-то задачи, после того как пользователь нажал кнопку мыши, горячую клавишу, кнопку меню и т.д. Команды являются объектами, которые передаются процессору для выполнения.
- Распределенные или Параллельные системы. Используя этот паттерн можно передавать команды на выполнение в отдельный поток. Создать очередь команд и т.д.
- Приложения, взаимодействующие с сетью.
Реализация
Теперь, рассмотрим, как этот паттерн можно применять при разработке мобильных приложений, а конкретно, Android–приложений. Способов реализации много, можно использовать IntentService или HaMeR-фрэймворк (Handler, Messages, Runnable) Давайте рассмотрим, как я имплементировал данный паттерн в тестовом приложении. Итак, тестовое приложение показывает маршруты и список мест, которые содержатся в том или ином маршруте. Соответственно, у нас есть два типа запросов (команд): TracksRequest и PlacesRequest. Оба класса являются наследниками базового класса CommonRequest, для того чтобы обрабатываться нашим процессором (CommandProcessor).
//Базовый класс CommonRequest для запросов
public abstract class CommonRequest {
public abstract void sendRequest(int i);
public int requestId;
}
// Реализация PlaceRequest
public class PlacesRequest extends CommonRequest{
private MessageController handler_;
public PlacesRequest(MessageController handler){
this.handler_ = handler;
}
public void sendRequest(int id){
sendAsyncPlaceRequest(id);
}
}
// Реализация TracksRequest
public class TracksRequest extends CommonRequest {
private MessageController handler_;
public TracksRequest(MessageController handler) {
this.handler_ = handler;
}
public void sendRequest(int id) {
sendAsyncTracksRequest();
}
}
В методе sendAsyncPlaceRequest происходит вся работа: это может быть создание URL для запроса к API, создание нового Thread, парсинг ответа и передача результата работы контроллеру с помощью Handler.
private void sendAsyncPlaceRequest(final int id){
Thread background = new Thread(new Runnable() {
@Override
public void run() {
String response = sendRequest(getUrl(id));
List<Place> places = new ArrayList<>();
try {
places = getPlacesFromJson(response);
} catch (JSONException e) {
Log.e("JSONException", e.getMessage());
}
handler_.sendMessage(handler_.obtainMessage(States.PLACES_WERE_FOUND,places));
}
});
background.start();
}
Далее следует реализовать класс CommandProcessor, который будет управлять нашими запросами и выполнять их:
publicclassRequestProcessor {
private UpdateCallbackListener clientActivity_;
public RequestProcessor(UpdateCallbackListener clienActivity) {
this.clientActivity_ = clienActivity;
}
// В зависимости от типа объекта отправляется соответствующая команда
public void execute(CommonRequest request, int id) {
request.sendRequest(id);
}
public void updateActivity(List<? extends Result> results) {
clientActivity_.onUpdate(results);
}
}
Теперь нам нужен котроллер, который, в зависимости от состояния будет отправлять разные команды процессору. Результат работы запроса отправляется из потока с помощью Handler:
public class MessageController extends Handler {
private static MessageController instance = null;
private RequestProcessor processor_;
public void init (UpdateCallbackListener listener) {
processor_ = new RequestProcessor(listener);
}
public static MessageController getInstance(){
if (instance == null){
instance = new MessageController();
}
return instance;
}
public void handleMessage(Message msg) {
switch (msg.what) {
case States.INIT_REQUEST:
CommonRequest request = (CommonRequest)msg.obj;
processor_.execute(request);
break;
case States.REQUEST_COMPLETED:
List<Result> results = (List<Result>)msg.obj;
processor_.updateActivity(results);
break;
default:
break;
}
}
}
Для того, чтобы теперь вернуть результат работы в активити, и вызвать какой-нибудь updateUI() для обновления пользовательского интерфейса (наполнения ListView, отрисовка маркеров на карте и т.д.) нужно определить интерфейс UpdateCallbackListener:
public interface UpdateCallbackListener {
void onUpdate(List<? extends Result> results);
}
И реализовать его в нашей активити:
public void onUpdate(List<? extends Result> results){
tracks_ = (List<Track>) results;
TrackAdapter trackAdapter = new TrackAdapter(this,tracks_);
listView_.setAdapter(trackAdapter);
}
После того, как результат вернется в ответе на запрос (к примеру, запрос на получение всех мест по этому маршруту) нам необходимо обновить актвити и передать объекты Place в адаптер. Это мы можем сделать через метод processor_.updateActivity(places), который вызовет onUpdate() в активити, которая имплементировала данный метод. Следующая диаграмма, также взята из [1] показывает динамику поведения паттерна:
Чтобы инициировать запрос, нам нужно создать в активити объект TracksRequestи передать его в controller:
controller_ = MessageController.getInstance();
controller_.init(this);
TracksRequest tracksRequest = new TracksRequest(controller_);
controller_.sendMessage(controller_.obtainMessage(States.INIT_REQUEST,tracksRequest));
Реализация с помощью IntentService
Использование IntentService также отлично позволяет реализовать этот паттерн. Рассмотрим диаграмму:
В качестве объекта-команды можно использовтаь Intent и передавать его в наш процессор. Creator — это наша activity, которая создает объект-команду, и передает этот объект executor’у, то есть IntentService’у в нашем случае. Таким образом, роль CommandProcessor выполняет класс CustomIntentService, а именно метод onHandleIntent() который в зависимости от данных, содержащихся в Intent, может выполнять различные операции. Чтобы вернуть результат в активити, в данном случае можно использовать BroadcastReceiver.
Пошаговая инструкция
Итак, подведем итоги, чтобы реализовать данный паттерн необходимо выполнить следующее:
- Определить интерфейс абстрактной команды. Интерфейс инкапсулирует реализацию каждой отдельной команды. В нашем случае это был метод sendRequest(int i) который каждая из команд (запросов) реализовывала по – разному.
- Определить способ, которым команда вернет результат тому, кто её вызвал. Как было показано для этого мы определили интерфейс UpdateCallbackListener, с методом onUpdate() для каждой из активити.
- Реализовать каждую из команд. У нас было два вида запросов – один для получения информации о маршрутах (TracksRequest), второй для получения информации о местах(PlacesRequest).
- Реализовать контроллер, который будет отправлять команды. Создание и отправление команд можно реализовать с помощью Abstract Factory или Prototype. Однако не обязательно, чтобы контроллер был создателем команд. Как было видно из примера, объекты запросов были созданы в активти.
- Реализовать класс процессора, который будет принимать команду и обрабатывать. Для каждого из запросов класс RequestProcessor выполняет метод execute() .
Достоинства и недостатки паттерна.
Достоинства:
- В таких приложениях как текстовый редактор, различные элементы интерфейса могут использовать одни и те же команды. Например, кнопка в меню “Создать”, и кнопка с таким же названием в контекстном меню могут обращаться к одной и той же команде.
- Возможность отправлять асинхронные запросы, управлять порядком вызовов, в зависимости от результата запроса.
- Гибкость при добавлении нового функционала в виде новой команды (запроса), не нарушая уже существующий функционал. Изменение или добавление новой команды никак не повлияет на обработчик команд.
- Разные клиенты (в нашем случае активити) могут использовать одни и те же команды. К примеру, запрос на загрузку изображения по Id, может использоваться для разных активити.
Недостатки:
- Дополнительно нужно отслеживать каждое состояние и соответствующе его обрабатывать.
- Количество команд может быть очень большим
- Двусторонняя связь. После того, как активити инициировала создание запроса, и ответ был получен, необходимо результат вернуть в активити.
Заключение
Скорее всего, в том или ином виде вы уже видели реализацию паттерна. Но так как тема архитектуры мобильных приложений является достаточно актуальной, надеюсь, статья была полезной. В дальнейшем планируется рассмотреть еще несколько паттернов, применяемых в разработке мобильных приложений. Пишите вопросы в комментариях, до встречи.
Список источников
[1] PATTERN-ORIENTED SOFTWARE ARCHITECTURE VOLUME 1. Douglas Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann
[2] Command Revisited www.dre.vanderbilt.edu/~schmidt/PDF/CommandRevisited.pdf
Тестовый проект на гитхабе: github.com/GregaryMaster/CommandProcessor
Комментарии (6)
Gregary
04.09.2015 10:58Спасибо за совет, да, наслышан о нем. Но хотелось избежать black-boxy вещей и сделать все вручную.Теперь с RxJava начну экспериментировать.
atetc
05.09.2015 11:55Кстати в чате Android-разработчиков появился новый канал посвященный обсуждению паттернов и архитектуре приложений gitter.im/rus-speaking/android-patterns
DZVang
07.09.2015 07:30Не очень удачная реализация. Никак не обрабатывается поворот экрана, на activity хранится жесткая ссылка, которую не заменить, значит при её пересоздании мы не сможем даже результат вернуть. Как верно было замечено с EventBus всё бы было проще.
Gregary
09.09.2015 17:33Да, использование EventBus в данном примере было бы проще. А что если у вас много реквестов и они могут быть взаимосвязаны? К примеру запрос на получение Id и следующий за ним запрос на картинку по этому Id? В данном случае использование только EventBus приведет к большому количеству событий (и не только с обработкой в Activity, но и в классах-реквестах), и возможно будет полезным использовать EventBus только для обновления UI а управлять запросами можно через handler, то есть MessageController в данном примере. Ну или как сказали выше использовать RxJava
Artem_zin
Вам прямая дорога к RxJava, серьезно :) Это проще, чем паттерн Commander, отлично параллелится, комбинируется, пишется и читается.