Главная » Хабрахабр » [Из песочницы] RESS — Новая архитектура для мобильных приложений

[Из песочницы] RESS — Новая архитектура для мобильных приложений

Вопреки провокационному заголовку, это никакая не новая архитектура, а попытка перевода простых и проверенных временем практик на новояз, на котором говорит современное Android-комьюнити

Введение

В последнее время стало больно смотреть на то, что творится в мире разработки под мобильные платформы. Архитектурная астронавтика процветает, каждый хипстер считает своим долгом придумать новую архитектуру, а для решения простой задачи вместо двух строчек вставить несколько модных фреймворков.

Хотя это один из самых частых кейсов приложений. Айтишные сайты заполонили туториалы по модным фреймворкам и переусложненным архитектурам, но при этом даже нет best practice для REST-клиентов под Android. Поэтому и пишу эту статью
Хочется чтобы нормальный подход к разработке тоже пошел в массы.

Чем плохи существующие решения

По большому счету проблема новомодных MVP, VIPER и им подобных — ровно одна, их авторы не умеют проектировать. А их последователи — тем более. И поэтому не понимают важных и очевидных вещей. И занимаются обычным оверинжинирингом.

1. Архитектура должна быть простой

Чем проще, тем лучше. Тем проще для понимания, надежнее и гибче. Переусложнить и наделать кучу абстракций может любой дурак, а чтобы сделать просто — нужно хорошенько подумать.

2. Оверинжиниринг это плохо

Добавлять новый уровень абстракции нужно только когда старый уже не решает проблем. После добавления нового уровня система должна стать проще для понимания, а кода меньше. Если, например, после этого у вас вместо одного файла, стало три, а система стала более запутанной, то вы сделали ошибку, а если по-простому — написали херню.

И оправдывают это тем что так гибче и поддерживать проще. Фанаты MVP, например, сами в своих статьях пишут открытым текстом что MVP тупо приводит к значительному усложнению системы. Но, как мы знаем из пункта номер 1, это взаимоисключающие вещи.

Теперь про VIPER, просто посмотрите, например, на схему из этой статьи.

Схема

image

И это для каждого экрана! Моим глазам больно. Особенно сочувствую тем, кому на работе с этим приходится сталкиваться не по своей воле. Тем же, кто это сам внедрил, сочувствую по немного другим причинам.

Новый подход

Эй, я тоже хочу модное название. Поэтому предлагаемая архитектура называется RESSRequest, Event, Screen, Storage. Буковки и названия подробраны так тупо для того чтобы получилось читаемое слово. Ну и чтобы не создавать путаницу с уже используемыми названиями. Ну и с REST созвучно.

Для других типов приложений она, вероятно, не подойдет. Сразу оговорюсь, эта архитектура для REST-клиентов.

1. Storage

Хранилище данных (в других терминах Model, Repository). Этот класс хранит данные и занимается их обработкой(сохраняет, загружает, складывает в БД и т.п.), а так же все данные от REST-сервиса сначала попадают сюда, парсятся и сохраняются здесь.

2. Screen

Экран приложения, в случае Android это ваше Activity. В других терминах это обычный ViewController как в MVC от Apple.

3. Request

Класс, который отвечает за посылку запросов к REST-сервису, а так же прием ответов и уведомление об ответе остальных компонентов системы.

4. Event

Связующее звено между остальными компонентами. Например, Request посылает эвент об ответе сервера, тем кто на него подписался. А Storage посылает эвент об изменении данных.

Код написан с допущениями и не проверялся, поэтому могут быть синтаксические ошибки и опечатки Далее пример упрощенной реализации.

Request

public class Request
default void onApiMethod2(Json answer) {} } private static class RequestTask extends AsyncTask<Void, Void, String> { public RequestTask(String methodName) { this.methodName = methodName; } private String methodName; @Override protected String doInBackground(Void ... params) { URL url = new URL(Request.serverUrl + "/" + methodName); HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection(); // ... // Делаем запрос и читаем ответ // ... return result; } @Override protected void onPostExecute(String result) { // ... // Парсим JSON из result // ... Requestr.onHandleAnswer(methodName, json); } } private static String serverUrl = "myserver.com"; private static List<OnCompleteListener> listeners = new ArrayList<>(); private static void onHandleAnswer(String methodName, Json json) { for(RequestListener listener : listeners) { if(methodName.equals("api/method1")) listener.onApiMethod1(json); else if(methodName.equals("api/method2")) listener.onApiMethod2(json); } } private static void makeRequest(String methodName) { new RequestTask(methodName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } public static void registerListener(RequestListener listener) { listeners.add(listener); } public static void unregisterListener(RequestListener listener) { listeners.remove(listener); } public static void apiMethod1() { makeRequest("api/method1"); } public static void onApiMethod2() { makeRequest("api/method2"); }
}

Storage

public class DataStorage
{ public interface DataListener { default void onData1Changed() {} default void onData2Changed() {} } private static MyObject1 myObject1 = null; private static List<MyObject2> myObjects2 = new ArrayList<>(); public static void registerListener(DataListener listener) { listeners.add(listener); } public static void unregisterListener(DataListener listener) { listeners.remove(listener); } public static User getMyObject1() { return myObject1; } public static List<MyObject2> getMyObjects2() { return myObjects2; } public static Request.RequestListener listener = new Request.RequestListener() { private T fromJson<T>(Json answer) { // ... // Парсим или десереализуем JSON // ... return objectT; } @Override public void onApiMethod1(Json answer) { myObject1 = fromJson(answer); for(RequestListener listener : listeners) listener.data1Changed(); } @Override public void onApiMethod2(Json answer) { myObject2 = fromJson(myObjects2); for(RequestListener listener : listeners) listener.data2Changed(); } };
}

Screen

public class MyActivity extends Activity implements DataStorage.DataListener
{ private Button button1; private Button button2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); button1.setOnClickListener((View) -> { Request.apiMethod1(); }); button2.setOnClickListener((View) -> { Request.apiMethod2(); }); updateViews(); } @Override protected void onPause() { super.onPause(); DataStorage.unregisterListener(this); } @Override protected void onResume() { super.onResume(); DataStorage.registerListener(this); updateViews(); } private void updateViews() { updateView1(); updateView2(); } private void updateView1() { Object1 data = DataStorage.getObject1(); // ... // Тут обновляем нужные вьюшки // ... } private void updateView2() { List<Object2> data = DataStorage.getObjects2(); // ... // Тут обновляем нужные вьюшки // ... } @Override public void onData1Changed() { updateView1(); } @Override public void onData2Changed() { updateView2(); }
}

App

public class MyApp extends Application
{ @Override public void onCreate() { super.onCreate(); Request.registerListener(DataStorage.listener); }
}

Та же схемка, но в терминах RESS, для понимания

Работает это так: При нажатии на кнопку, дергается нужный метод у Request, Request посылает запрос на сервер, обрабатывает ответ и уведомляет сначала DataStorage. DataStorage парсит ответ и кеширует данные у себя. Затем Request уведомляет текущий активный Screen, Screen берет данные из DataStorage и обновляет UI.

А так же обновляет UI дополнительно в onResume. Screen подписывается и отписывается от умедомлений в onResume и onPause соотвественно. Уведомления приходят только в текущую активную Activity, никаких проблем с обработкой запроса в фоне или поворотом Activity. Что это дает? До фоновой активити уведомление не дойдет, а при возвращении в активное состояние, данные возьмутся из DataStorage. Activity будет всегда в актуальном состоянии. В итоге никаких проблем при повороте экрана и пересоздании Activity.

И для всего этого хватает дефолтных апи из Android SDK.

Вопросы и ответы на будующую критику

1. Какой профит?

Реальная простота, гибкость, поддерживаемость, масштабируемость и минимум зависимостей. Вы всегда можете усложнить определенную часть системы, если вам необходимо. Очень много данных? Аккуратно разбиваете DataStorage на несколько. Огромное REST API у сервиса? Делаете несколько Request. Листенеры это слишком просто, некруто и немодно? Возьмите EventBus. Косо смотрят в барбершопе на HttpConnection? Ну возьмите Retrofit. Жирный Activity с кучей фрагментов? Просто считайте что каждый фрагмент это Screen, или разбейте на сабклассы.

2. AsyncTask это моветон, возьми хотя бы Retrofit!

Да? И какие проблемы он в данном коде вызывает? Утечки памяти? Нет, тут AsyncTask не хранит ссылки на активити, а только ссылку на статик метод. Ответ теряется? Нет, ответ всегда приходит в статик DataStorage, пока приложение не убито. Пытается обновить активити на паузе? Нет, уведомления приходят только в активную Activity.

Просто смотрим сюда. Да и как тут поможет Retrofit? Автор взял RxJava, Retrofit и все равно лепит костыли, чтобы решить проблему, которой в RESS попросту нет.

3. Screen это же ViewController! Нужно разделять логику и представление, arrr!

Бросьте уже эту мантру. Типичный клиент для REST-сервиса это одна большая вьюшка для серверной части. Вся ваша бизнес-логика это установить нужный стейт для кнопки или текстового поля. Что вы там собрались разделять? Говорите так будет проще поддерживать? Поддерживать 3 файла с 3 тоннами кода, вместо 1 файла с 1 тонной проще? Ок. А если у нас активити с 5 фрагментами? Это у нас уже 3 x (5 + 1) = 18 файлов.

Добавлять функционал в проект с MVP особенно весело: хочешь добавить обработчик кнопки? Разделение на Controller и View в таких кейсах просто плодит кучу бессмысленного кода, пора бы уже это признать. В RESS для этого я напишу пару строк кода в одном файле. Ок, поправь Presenter, Activity и View-интерфейс.

Так вы не видели больших проектов. Но ведь в больших проектах ViewController ужасно разрастается? Реально большие проекты на RESS с 100+ экранов и несколькими командами по 10 человек прекрасно себя чувствуют. Ваш REST-клиент для очередного сайта на 5тыс строк это мелкий проект, а 5тыс строк там только потому что на каждый экран по 5 классов. А Screen для жирных экранов содержат внутри себя дополнительные Screen для крупных элементов UI, например, тех же фрагментов. Просто делают несколько Request и Storage. А переход на VIPER вообще заставит всю команду уволиться одним днем. Проект на MVP тех же масштабов просто захлебнется в куче презентеров, интерфейсов, активити, фрагментов и неочевидных связей.

Заключение

Надеюсь эта статья сподвигнет многих разработчиков пересмотреть свои взгляды на архитектуру, не плодить абстракции и посмотреть на более простые и проверенные временем решения


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Как распознавание лиц помогает находить тестовые телефоны

Привет, хабровчане! В EastBanc Technologies ведётся большое количество проектов, связанных с мобильной разработкой. В связи с чем необходим целый зоопарк устройств для тестирования на всех этапах. И, что характерно, каждый отдельный девайс постоянно оказывается нужен самым разным людям, а найти ...

Security Week 39: на смерть Google+

На прошлой неделе Google объявил (новость) о закрытии социальной сети Google+, но сделано это было достаточно необычно. Компания Google вообще не стесняется закрывать проекты, которые по разным причинам не взлетели. Многие до сих пор не могут простить компании отказа от ...