RESS - Nova arquitetura para aplicativos móveis



Ao contrário do título provocativo, essa não é uma arquitetura nova, mas uma tentativa de traduzir práticas simples e testadas pelo tempo no Newspeak, falado pela moderna comunidade Android

1. Introdução


Recentemente, tornou-se doloroso observar o que está acontecendo no mundo do desenvolvimento de plataformas móveis. A astronáutica arquitetônica está prosperando, cada hipster considera seu dever criar uma nova arquitetura e resolver uma tarefa simples, em vez de duas linhas, inserir várias estruturas da moda.

Esses sites preencheram tutoriais sobre estruturas modernas e arquiteturas sofisticadas, mas não existe uma prática recomendada para os clientes REST do Android. Embora este seja um dos casos de aplicação mais frequentes. Quero que a abordagem normal do desenvolvimento também vá para as massas. Portanto, estou escrevendo este artigo

Por que as soluções existentes são ruins?


Em geral, o problema do MVP, VIPER e outros semelhantes, é exatamente o mesmo, seus autores não sabem como projetar. E seus seguidores - ainda mais. E, portanto, eles não entendem coisas importantes e óbvias. E eles estão envolvidos em excesso de engenharia convencional.

1. A arquitetura deve ser simples


Quanto mais simples, melhor. Isso facilita a compreensão, é mais confiável e mais flexível. Qualquer tolo pode se complicar e fazer um monte de abstrações, mas, para simplificar, é preciso pensar com cuidado.

2. O excesso de engenharia é ruim


Você precisa adicionar um novo nível de abstração apenas quando o antigo não resolve problemas. Depois de adicionar um novo nível, o sistema deve se tornar mais fácil de entender e com menos código. Se, por exemplo, depois disso, você tinha três em vez de um arquivo, e o sistema ficou mais complicado, você cometeu um erro e, de uma maneira simples, escreveu lixo .

Os fãs do MVP, por exemplo, em seus artigos escrevem em texto simples que o MVP estupidamente leva a uma complicação significativa do sistema. E eles justificam isso pelo fato de ser tão flexível e fácil de manter . Mas, como sabemos do parágrafo número 1, essas são coisas mutuamente exclusivas.

Agora, sobre o VIPER, basta olhar, por exemplo, o diagrama deste artigo.

Esquema
imagem

E isso é para todas as telas! Isso machuca meus olhos. Eu particularmente simpatizo com aqueles que no trabalho têm que lidar com isso contra sua vontade. Para aqueles que o apresentaram, simpatizo com razões ligeiramente diferentes .

Nova abordagem


Ei, eu também quero um nome na moda . Portanto, a arquitetura proposta é chamada de RESS - R equest, E vent, Sreen, S torage. As letras e os nomes são detalhados de maneira estúpida para obter uma palavra legível. Bem, para não criar confusão com os nomes já usados. Bem, com o REST em sintonia.

Faça uma reserva imediatamente, essa arquitetura é para clientes REST. Para outros tipos de aplicativos, provavelmente não funcionará.



1. Armazenamento


Data Warehouse (em outros termos, Modelo, Repositório). Essa classe armazena dados e os processa (salva, carrega, adiciona ao banco de dados, etc.), bem como todos os dados do serviço REST chegam aqui, analisados ​​e armazenados aqui.

2. Tela


A tela do aplicativo, no caso do Android, é sua Atividade. Em outros termos, é um ViewController regular como o MVC da Apple.

3. Pedido


A classe responsável por enviar solicitações ao serviço REST, além de receber respostas e notificar sobre a resposta de outros componentes do sistema.

4. Evento


O link entre os outros componentes. Por exemplo, a Solicitação envia um evento sobre a resposta do servidor para aqueles que se inscrevem. E o Storage envia um evento sobre alterações de dados.

A seguir, é apresentado um exemplo de implementação simplificada. O código foi escrito com suposições e não foi verificado, portanto, pode haver erros de sintaxe e erros de digitação

Pedido
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"); } } 


Armazenamento
 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(); } }; } 


Ecrã
 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(); } } 


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


O mesmo shemka, mas em termos de RESS, para entender


Funciona assim: Quando você clica no botão, o método desejado para Solicitação está se contraindo, a Solicitação envia uma solicitação ao servidor, processa a resposta e notifica o DataStorage primeiro. DataStorage analisa a resposta e armazena em cache os dados em casa. A solicitação notifica a tela ativa no momento, a tela pega os dados do DataStorage e atualiza a interface do usuário.

Sinais de tela e cancelamentos de inscrição de mediocridade em onResume e onPause, respectivamente. Ele também atualiza a interface do usuário, além do onResume. O que isso dá? As notificações vêm apenas na Atividade ativa atual, sem problemas com o processamento da solicitação em segundo plano ou com a ativação da Atividade. A atividade estará sempre atualizada. A notificação não alcançará a atividade em segundo plano e, ao retornar ao estado ativo, os dados serão obtidos do DataStorage. Como resultado, não há problemas ao girar a tela e recriar a atividade.

E por tudo isso, a API padrão do SDK do Android é suficiente.

Perguntas e respostas para críticas futuras


1. Qual é o lucro?


Simplicidade real, flexibilidade, capacidade de manutenção, escalabilidade e um mínimo de dependências. Você sempre pode complicar uma certa parte do sistema, se precisar. Muitos dados? Quebre delicadamente o DataStorage em vários. API REST de serviço enorme? Faça alguns pedidos. Listering é muito simples, desajeitado e fora de moda? Pegue o EventBus. Olhando de soslaio para uma barbearia no HttpConnection? Bem, faça o Retrofit. Atividade ousada com um monte de fragmentos? Apenas considere que cada fragmento é Screen ou divida-o em subclasses.

2. O AsyncTask é um homem mau, faça pelo menos o Retrofit!


Hein? E que problemas isso causa neste código? Vazamentos de memória? Não, aqui o AsyncTask não armazena links para ativações, mas apenas um link para um método estático. A resposta está perdida? Não, a resposta sempre vem para o DataStorage estático até que o aplicativo seja morto. Tentando atualizar a atividade em pausa? Não, as notificações só vêm na Atividade ativa.

E como o Retrofit pode ajudar aqui? Apenas olhe aqui . O autor pegou RxJava, Retrofit e ainda esculpe muletas para resolver um problema que o RESS simplesmente não possui.

3. A tela é a mesma do ViewController! Precisa separar lógica e apresentação, arrr!


Largue esse mantra já. Um cliente típico para um serviço REST é uma grande visão para o lado do servidor. Toda a sua lógica de negócios é definir o estado correto para um botão ou campo de texto. O que você vai compartilhar lá? Diga que será mais fácil de manter? Manter 3 arquivos com 3 toneladas de código, em vez de 1 arquivo com 1 tonelada mais fácil? Ok E se tivermos atividade com 5 fragmentos? Já são 3 x (5 + 1) = 18 arquivos.

A separação entre Controller e View nesses casos simplesmente produz um monte de código sem sentido, é hora de admitir. Adicionar funcionalidade a um projeto com MVP é especialmente divertido: você deseja adicionar um manipulador de botão? Ok, corrija a interface do Presenter, Activity e View. No RESS, para isso, escreverei algumas linhas de código em um único arquivo.

Mas em grandes projetos, o ViewController cresce muito? Então você não viu grandes projetos. Seu cliente REST para o próximo site com 5 mil linhas é um projeto pequeno e 5 mil linhas apenas porque existem 5 classes em cada tela. Projetos realmente grandes no RESS com mais de 100 telas e várias equipes de 10 pessoas se sentem bem. Basta fazer alguns pedidos e armazenamento. E Tela para telas em negrito contém Tela adicional para elementos grandes da interface do usuário, por exemplo, os mesmos fragmentos. Um projeto em um MVP da mesma escala simplesmente se afogará em vários apresentadores, interfaces, ativações, fragmentos e conexões não óbvias. E a transição para o VIPER geralmente fará com que toda a equipe saia um dia.

Conclusão


Espero que este artigo incentive muitos desenvolvedores a reconsiderar seus pontos de vista sobre arquitetura, não para produzir abstrações e procurar soluções mais simples e testadas pelo tempo.

Source: https://habr.com/ru/post/pt424081/


All Articles