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 { 	public interface RequestListener 	{ 		default void onApiMethod1(Json answer) {} 		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.

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

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

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

Заключение

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


ссылка на оригинал статьи https://habr.com/post/424081/