Шаблоны проектирования мобильных приложений. Command Processor

от автора

“Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте.”
Мартин Голдинг

При разработке одного из проектов, использующего 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

ссылка на оригинал статьи http://habrahabr.ru/post/266045/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *