RESS - Nouvelle architecture pour les applications mobiles



Contrairement au titre provocateur, ce n'est pas une nouvelle architecture, mais une tentative de traduire des pratiques simples et éprouvées en Newspeak, qui est parlé par la communauté Android moderne

Présentation


Récemment, il est devenu douloureux de regarder ce qui se passe dans le monde du développement des plateformes mobiles. L'astronautique architecturale est en plein essor, chaque hipster considère qu'il est de son devoir de proposer une nouvelle architecture, et de résoudre une tâche simple, au lieu de deux lignes, insérer plusieurs cadres à la mode.

Ces sites ont rempli des didacticiels sur les cadres à la mode et les architectures sophistiquées, mais il n'y a même pas de meilleure pratique pour les clients Android REST. Bien que ce soit l'un des cas d'application les plus fréquents. Je veux que l'approche normale du développement aille aussi dans les masses. Par conséquent, j'écris cet article

Pourquoi les solutions existantes sont-elles mauvaises?


Dans l'ensemble, le problème des nouveaux MVP, VIPER et similaires est exactement le même, leurs auteurs ne savent pas comment concevoir. Et leurs adeptes - encore plus. Et par conséquent, ils ne comprennent pas les choses importantes et évidentes. Et ils sont engagés dans la suringénierie conventionnelle.

1. L'architecture doit être simple


Plus c'est simple, mieux c'est. Cela le rend plus facile à comprendre, plus fiable et plus flexible. Tout imbécile peut re-compliquer et faire un tas d'abstractions, mais pour le faire simplement, vous devez réfléchir soigneusement.

2. La suringénierie est mauvaise


Vous devez ajouter un nouveau niveau d'abstraction uniquement lorsque l'ancien ne résout pas les problèmes. Après avoir ajouté un nouveau niveau, le système devrait devenir plus facile à comprendre et moins de code. Si, par exemple, après cela, vous aviez trois fichiers au lieu d'un seul, et que le système devenait plus compliqué, alors vous avez fait une erreur, et si d'une manière simple, vous avez écrit des ordures .

Les fans de MVP, par exemple, eux-mêmes dans leurs articles écrivent en clair que MVP mène bêtement à une complication significative du système. Et ils le justifient par le fait qu'il est si flexible et plus facile à entretenir . Mais, comme nous le savons par le paragraphe 1, ce sont des choses mutuellement exclusives.

Maintenant à propos de VIPER, regardez, par exemple, le diagramme de cet article.

Schéma
image

Et c'est pour chaque écran! Ça me fait mal aux yeux. Je sympathise particulièrement avec ceux qui, au travail, doivent faire face à cette situation contre leur gré. À ceux qui l'ont présenté moi-même, je sympathise avec des raisons légèrement différentes .

Nouvelle approche


Hé, je veux aussi un nom à la mode . Par conséquent, l'architecture proposée est appelée RESS - R equest, E vent, S screen, S torage. Les lettres et les noms sont détaillés si bêtement afin d'obtenir un mot lisible. Eh bien, pour ne pas créer de confusion avec les noms déjà utilisés. Eh bien, avec REST en phase.

Faites immédiatement une réservation, cette architecture est destinée aux clients REST. Pour d'autres types d'applications, cela ne fonctionnera probablement pas.



1. Stockage


Entrepôt de données (en d'autres termes, modèle, référentiel). Cette classe stocke les données et les traite (enregistre, charge, ajoute à la base de données, etc.), ainsi que toutes les données du service REST arrivent ici, analysées et stockées ici.

2. Écran


L'écran de l'application, dans le cas d'Android, c'est votre activité. En d'autres termes, c'est un ViewController classique comme le MVC d'Apple.

3. Demande


Classe responsable de l'envoi des demandes au service REST, ainsi que de la réception des réponses et de la notification de la réponse des autres composants du système.

4. Événement


Le lien entre les autres composants. Par exemple, Request envoie un événement sur la réponse du serveur à ceux qui y sont abonnés. Et le stockage envoie un événement sur les modifications de données.

Voici un exemple d'implémentation simplifiée. Le code a été écrit avec des hypothèses et n'a pas été vérifié, il peut donc y avoir des erreurs de syntaxe et des fautes de frappe

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


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


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


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


La même shemka, mais en termes de RESS, pour comprendre


Cela fonctionne comme ceci: lorsque vous cliquez sur le bouton, la méthode de requête souhaitée est le contraction, Request envoie une requête au serveur, traite la réponse et notifie DataStorage en premier. DataStorage analyse la réponse et met en cache les données à la maison. La demande notifie ensuite l'écran actuellement actif, l'écran prend les données du DataStorage et met à jour l'interface utilisateur.

Ecran des signes et désabonnement de la médiocrité dans onResume et onPause, respectivement. Il met également à jour l'interface utilisateur en plus de onResume. Qu'est-ce que ça donne? Les notifications ne viennent que dans l'activité active actuelle, aucun problème avec le traitement de la demande en arrière-plan ou la rotation de l'activité. L'activité sera toujours à jour. La notification n'atteindra pas l'activité d'arrière-plan, et lors du retour à l'état actif, les données seront prises à partir du DataStorage. Par conséquent, aucun problème lorsque vous faites pivoter l'écran et recréez l'activité.

Et pour tout cela, les API par défaut du SDK Android suffisent.

Questions et réponses pour les futures critiques


1. Quel est le profit?


Véritable simplicité, flexibilité, maintenabilité, évolutivité et un minimum de dépendances. Vous pouvez toujours compliquer une certaine partie du système si vous en avez besoin. Trop de données? Divisez doucement le DataStorage en plusieurs. Un énorme service API REST? Faites quelques demandes. L'écoute est-elle trop simple, maladroite et démodée? Prenez l'EventBus. Vous cherchez de travers dans un salon de coiffure sur HttpConnection? Eh bien, prenez Retrofit. Activité audacieuse avec un tas de fragments? Considérez simplement que chaque fragment est Screen ou divisez-le en sous-classes.

2. AsyncTask est un mauvais homme, prenez au moins Retrofit!


Hein? Et quels problèmes cela provoque-t-il dans ce code? Des fuites de mémoire? Non, ici AsyncTask ne stocke pas de liens vers des activations, mais uniquement un lien vers une méthode statique. La réponse est perdue? Non, la réponse vient toujours du DataStorage statique jusqu'à ce que l'application soit supprimée. Vous essayez de rafraîchir l'activité en pause? Non, les notifications ne viennent que dans l'activité active.

Et comment Retrofit peut-il aider ici? Regardez ici . L'auteur a pris RxJava, Retrofit et sculpte toujours des béquilles pour résoudre un problème que RESS n'a tout simplement pas.

3. L'écran est le même ViewController! Besoin de séparer la logique et la présentation, arrr!


Déposez déjà ce mantra. Un client typique pour un service REST est une vue d'ensemble du côté serveur. Toute votre logique métier consiste à définir le bon état pour un bouton ou un champ de texte. Qu'allez-vous partager là-bas? Vous dites que ce sera plus facile à entretenir? Maintenir 3 fichiers avec 3 tonnes de code, au lieu de 1 fichier avec 1 tonne plus facile? Ok Et si nous avons une activité avec 5 fragments? C'est déjà 3 x (5 + 1) = 18 fichiers.

La séparation en Controller et View dans de tels cas produit simplement un tas de code sans signification, il est temps de l'admettre. Ajouter des fonctionnalités à un projet avec MVP est particulièrement amusant: voulez-vous ajouter un gestionnaire de boutons? Ok, corrigez le présentateur, l'activité et la vue-interface. Dans RESS, j'écrirai pour cela quelques lignes de code dans un seul fichier.

Mais dans les grands projets, ViewController se développe terriblement? Vous n'avez donc pas vu de grands projets. Votre client REST pour le site suivant avec 5 000 lignes est un petit projet, et 5 000 lignes uniquement parce qu'il y a 5 classes sur chaque écran. De très gros projets sur RESS avec plus de 100 écrans et plusieurs équipes de 10 personnes se sentent bien. Faites juste quelques demandes et stockage. Et l'écran pour les écrans en gras contient un écran supplémentaire pour les grands éléments de l'interface utilisateur, par exemple, les mêmes fragments. Un projet sur un MVP de la même échelle se noiera simplement dans un tas de présentateurs, d'interfaces, d'activations, de fragments et de connexions non évidentes. Et la transition vers VIPER fera généralement quitter toute l'équipe un jour.

Conclusion


J'espère que cet article encouragera de nombreux développeurs à reconsidérer leur point de vue sur l'architecture, à ne pas produire d'abstractions et à rechercher des solutions plus simples et plus éprouvées.

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


All Articles