Pull to refresh

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

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

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

Articles