RESS - Neue Architektur für mobile Anwendungen



Im Gegensatz zum provokativen Titel handelt es sich hierbei nicht um eine neue Architektur, sondern um den Versuch, einfache und bewährte Praktiken in Newspeak zu übersetzen, was von der modernen Android-Community gesprochen wird

Einleitung


In letzter Zeit ist es schmerzhaft geworden zu beobachten, was in der Welt der Entwicklung für mobile Plattformen passiert. Die architektonische Astronautik boomt, jeder Hipster sieht es als seine Aufgabe an, eine neue Architektur zu entwickeln und anstelle von zwei Zeilen mehrere modische Rahmen einzufügen, um eine einfache Aufgabe zu lösen.

Tolle Websites mit Tutorials zu trendigen Frameworks und ausgefeilten Architekturen, aber es gibt nicht einmal eine Best Practice für Android REST-Clients. Dies ist zwar einer der häufigsten Anwendungsfälle. Ich möchte, dass die normale Herangehensweise an die Entwicklung auch an die Massen geht. Deshalb schreibe ich diesen Artikel

Als bestehende Lösungen sind schlecht


Im Großen und Ganzen ist das Problem von neuem MVP, VIPER und dergleichen genau dasselbe, und ihre Autoren wissen nicht, wie sie entwerfen sollen. Und ihre Anhänger - umso mehr. Und deshalb verstehen sie wichtige und offensichtliche Dinge nicht. Und sie beschäftigen sich mit konventioneller Überentwicklung .

1. Die Architektur sollte einfach sein


Je einfacher desto besser. Dies macht es verständlicher, zuverlässiger und flexibler. Jeder Dummkopf kann eine Reihe von Abstraktionen komplizieren, aber um es einfach zu machen, müssen Sie sorgfältig überlegen.

2. Überengineering ist schlecht


Sie müssen nur dann eine neue Abstraktionsebene hinzufügen, wenn die alte keine Probleme löst. Nach der Zugabe sollte ein neues Niveau des Systems seine leichter zu verstehen, aber den Code weniger . Wenn Sie zum Beispiel danach drei statt einer Datei hatten und das System komplizierter wurde, haben Sie einen Fehler gemacht, und wenn Sie auf einfache Weise Müll geschrieben haben .

MVP-Fans schreiben beispielsweise selbst in ihren Artikeln im Klartext, dass MVP dumm zu einer erheblichen Komplikation des Systems führt. Und sie begründen dies damit, dass es so flexibel und wartungsfreundlich ist . Wie wir jedoch aus Absatz 1 wissen, schließen sich diese Dinge gegenseitig aus.

Nun, über den VIPER, nur einen Blick, zum Beispiel in dem Schema von diesem artikel.

Schema
Bild

Und das ist für jeden Bildschirm! Es tut mir in den Augen weh. Besonders sympathisiere ich mit denen, die sich bei der Arbeit gegen ihren Willen damit auseinandersetzen müssen. Denjenigen, die es mir vorgestellt haben, sympathisiere ich aus etwas anderen Gründen.

Neuer Ansatz


Hey, ich möchte auch einen trendigen Namen . Die vorgeschlagene Architektur heißt daher RESS - R equest, E vent, S creen, S torage. Die Buchstaben und Namen sind so blöd detailliert, um ein lesbares Wort zu bekommen. Nun, um nicht mit den bereits verwendeten Namen zu verwechseln. Nun, mit REST in Einklang.

Diese Architektur ist ab sofort für REST-Kunden reservierbar. Für andere Arten von Anwendungen wird es wahrscheinlich nicht funktionieren.



1. Lagerung


Data Warehouse (in anderen Begriffen Modell, Repository). Diese Klasse speichert Daten und verarbeitet sie (speichert, lädt, fügt sie der Datenbank hinzu usw.) sowie alle Daten aus dem REST-Service, die zuerst hier abgerufen, analysiert und gespeichert werden.

2. Bildschirm


Der Anwendungsbildschirm ist im Fall von Android Ihre Aktivität. Anders ausgedrückt handelt es sich um einen regulären ViewController wie den MVC von Apple.

3. Anfrage


Die Klasse, die für das Senden von Anforderungen an den REST-Service sowie das Empfangen von Antworten und das Benachrichtigen über die Antwort anderer Systemkomponenten verantwortlich ist.

4. Ereignis


Die Verbindung zwischen den anderen Komponenten. Beispielsweise sendet Request ein Ereignis über die Antwort des Servers an die Abonnenten. Und Storage sendet ein Ereignis über Datenänderungen.

Das Folgende ist ein Beispiel für eine vereinfachte Implementierung. Der Code wurde mit Annahmen geschrieben und nicht überprüft, daher können Syntaxfehler und Tippfehler auftreten

Anfrage
public class Request
{
	public interface RequestListener
	{
		default void onApiMethod1(Json answer) {}
		default void onApiMethod2(Json answer) {}
	}
	private static class RequestTask extends AsyncTask
	{
		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 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");
	}
}


Lagerung
public class DataStorage
{
	public interface DataListener
	{
		default void onData1Changed() {}
		default void onData2Changed() {}
	}
	private static MyObject1 myObject1 = null;
	private static List 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 getMyObjects2()
	{
		return myObjects2;
	}
	public static Request.RequestListener listener = new Request.RequestListener()
	{
		private T fromJson(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();
		}
	};
}


Bildschirm
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 data = DataStorage.getObjects2();
		// ...
		// Тут обновляем нужные вьюшки
		// ...
	}
	@Override
	public void onData1Changed()
	{
		updateView1();
	}
	@Override
	public void onData2Changed()
	{
		updateView2();
	}
}


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


Die gleiche Shemka, aber in Bezug auf RESS, zum Verständnis


Dies funktioniert folgendermaßen: Wenn Sie auf die Schaltfläche klicken, wird die gewünschte Anforderungsmethode verdreht. Request sendet eine Anforderung an den Server, verarbeitet die Antwort und benachrichtigt zuerst DataStorage. DataStorage analysiert die Antwort und speichert die Daten zu Hause im Cache. Die Anforderung benachrichtigt dann den aktuell aktiven Bildschirm. Der Bildschirm entnimmt Daten aus dem DataStorage und aktualisiert die Benutzeroberfläche.

In onResume und onPause wird die Mittelmäßigkeit auf dem Bildschirm an- und abgemeldet. Zusätzlich zu onResume wird auch die Benutzeroberfläche aktualisiert. Was gibt es? Benachrichtigungen werden nur in der aktuell aktiven Aktivität gesendet, und es treten keine Probleme bei der Verarbeitung der Anforderung im Hintergrund oder beim Aktivieren der Aktivität auf. Die Aktivitäten werden immer auf dem neuesten Stand sein. Die Benachrichtigung erreicht die Hintergrundaktivität nicht und nach der Rückkehr in den aktiven Zustand werden die Daten aus dem DataStorage übernommen. Infolgedessen treten keine Probleme auf, wenn Sie den Bildschirm drehen und die Aktivität neu erstellen.

Und für all das sind die Standard-APIs aus dem Android SDK genug.

Fragen und Antworten für zukünftige Kritik


1. Was ist der Gewinn?


Echte Einfachheit, Flexibilität, Wartbarkeit, Skalierbarkeit und ein Minimum an Abhängigkeiten. Sie können jederzeit einen bestimmten Teil des Systems komplizieren, wenn dies erforderlich ist. Zu viele Daten? Zerlegen Sie den DataStorage vorsichtig in mehrere. Riesige Service-REST-API? Machen Sie ein paar Anfragen. Ist Listering zu einfach, umständlich und unmodern? Nehmen Sie den EventBus. Suchen Sie einen Friseurladen auf der HttpConnection? Nimm Retrofit. Mutige Aktivität mit ein paar Fragmenten? Bedenken Sie einfach, dass jedes Fragment Screen ist, oder unterteilen Sie es in Unterklassen.

2. AsyncTask ist ein böser Mann, nimm wenigstens Retrofit!


Huh? Und welche Probleme verursacht dieser Code? Speicherlecks? Nein, hier speichert AsyncTask keine Links zu Aktivierungen, sondern nur einen Link zu einer statischen Methode. Die Antwort ist verloren? Nein, die Antwort kommt immer in den statischen DataStorage, bis die Anwendung beendet wird. Versuchen Sie, die Aktivität nach einer Pause zu aktualisieren? Nein, Benachrichtigungen werden nur in der aktiven Aktivität angezeigt.

Und wie hilft Retrofit dabei? Schau einfach hier . Der Autor hat RxJava und Retrofit verwendet und arbeitet immer noch an Krücken, um ein Problem zu lösen, das RESS einfach nicht hat.

3. Bildschirm ist der gleiche ViewController! Logik und Präsentation müssen getrennt werden, arrr!


Lassen Sie dieses Mantra bereits fallen. Ein typischer Client für einen REST-Service ist eine große Ansicht für die Serverseite. Ihre gesamte Geschäftslogik besteht darin, den richtigen Status für eine Schaltfläche oder ein Textfeld festzulegen. Was wirst du dort teilen? Sagen Sie, es wird einfacher zu warten sein? 3 Dateien mit 3 Tonnen Code pflegen, statt 1 Datei mit 1 Tonne einfacher? Ok Und wenn wir Aktivität mit 5 Fragmenten haben? Dies sind bereits 3 x (5 + 1) = 18 Dateien.

Die Trennung in Controller und View erzeugt in solchen Fällen einfach eine Reihe von bedeutungslosem Code. Es ist an der Zeit, dies zuzugeben. Das Hinzufügen von Funktionen zu einem Projekt mit MVP macht besonders Spaß: Möchten Sie eine Schaltflächenbehandlungsroutine hinzufügen? Ok, Presenter, Activity und View-Interface korrigieren. In RESS werde ich dazu einige Codezeilen in eine einzige Datei schreiben.

Aber in großen Projekten wächst ViewController schrecklich? Sie haben also keine großen Projekte gesehen. Ihr REST-Client für die nächste Site mit 5.000 Zeilen ist ein kleines Projekt, und 5.000 Zeilen gibt es nur, weil es auf jedem Bildschirm 5 Klassen gibt. Wirklich große Projekte auf RESS mit über 100 Bildschirmen und mehreren Teams von 10 Personen fühlen sich großartig an. Machen Sie einfach ein paar Anfragen und Speicher. Und Bildschirm für fette Bildschirme enthält zusätzlichen Bildschirm für große Benutzeroberflächenelemente, z. B. dieselben Fragmente. Ein Projekt auf einem MVP der gleichen Größenordnung wird einfach in einer Reihe von Präsentatoren, Schnittstellen, Aktivierungen, Fragmenten und nicht offensichtlichen Zusammenhängen ertrinken. Und der Übergang zu VIPER wird im Allgemeinen das gesamte Team an einem Tag verlassen.

Fazit


Ich hoffe, dieser Artikel wird viele Entwickler ermutigen, ihre Ansichten zur Architektur zu überdenken, keine Abstraktionen zu produzieren und einfachere und erprobtere Lösungen zu suchen.

Jetzt auch beliebt: