Zustandsautomaten im Dienst von MVP. Yandex Vortrag

Das Finite-State-Machine-Modell (FSM) wird zum Schreiben von Code für eine Vielzahl von Plattformen verwendet, einschließlich Android. Sie können den Code weniger umständlich machen, passen gut in das MVP-Paradigma (Model-View-Presenter) und eignen sich für einfache Tests. Entwickler Vladislav Kuznetsov erklärte der Droid Party, wie dieses Modell bei der Entwicklung der Yandex.Disk-Anwendung hilft.


- Lassen Sie uns zunächst über die Theorie sprechen. Ich denke, jeder von Ihnen hat von MVP und der Zustandsmaschine gehört, aber wir werden es wiederholen.



Sprechen wir über Motivation, warum all dies benötigt wird und wie es uns helfen kann. Fahren wir mit dem fort, was wir getan haben. An einem realen Beispiel werde ich Codeteile zeigen. Und am Ende werden wir über das Testen sprechen, darüber, wie dieser Ansatz dazu beigetragen hat, alles bequem zu testen.

Die Zustandsmaschine und MVP oder ähnliches - wahrscheinlich MVI - wurden von allen verwendet.

Es gibt viele Zustandsautomaten. Hier ist die einfachste Definition, die ihnen gegeben werden kann: Dies ist eine Art mathematische Abstraktion, die je nach Ereignis in Form einer endlichen Menge von Zuständen, Ereignissen und Übergängen vom aktuellen in einen neuen Zustand dargestellt wird.



Hier ist ein einfaches Diagramm eines abstrakten Programmierers, der manchmal schläft, manchmal isst, aber meistens Code schreibt. Das reicht uns. Es gibt eine große Anzahl von Varianten einer Finite-State-Maschine, aber das reicht uns.



Der Umfang der Zustandsmaschine ist ziemlich groß. Für jeden Artikel werden sie verwendet und erfolgreich angewendet.



Wie jeder Ansatz unterteilt MVP unsere Anwendung in mehrere Ebenen. Ansicht - meistens eine Aktivität oder ein Fragment, dessen Aufgabe es ist, eine Aktion an den Benutzer weiterzuleiten, um den Präsentator zu identifizieren, den der Benutzer etwas getan hat. Wir betrachten Model als Datenanbieter. Es kann wie eine Datenbank sein, wenn wir über saubere Architektur oder Interactor sprechen, kann alles sein. Presenter ist ein Vermittler, der die Ansicht und das Modell verbindet und gleichzeitig die Ansicht aus dem Modell abrufen und aktualisieren kann. Das reicht uns.

Wer kann in einem Satz sagen, was ein Programm ist? Ausführbarer Code? Zu allgemein, detaillierter. Ein Algorithmus? Ein Algorithmus ist eine Folge von Aktionen.

Dies ist ein Datensatz und eine Art Kontrollfluss. Es spielt keine Rolle, wer diese Daten manipuliert: der Benutzer oder nicht. Es folgt der Gedanke, dass der Status einer Anwendung zu jedem Zeitpunkt durch die Gesamtheit aller ihrer Daten bestimmt wird. Und je mehr Daten in der Anwendung enthalten sind, desto schwieriger ist es, sie zu verwalten, desto unvorhersehbarer kann eine Situation werden, wenn etwas schief geht.



Stellen Sie sich eine einfache Klasse mit drei Booleschen Flags vor. Um sicherzustellen, dass Sie alle Szenarien zum Kombinieren dieser Flags abdecken, benötigen Sie 2³ Szenarien. Es ist notwendig, acht Szenarien mit der Garantie abzudecken, dass ich mit Sicherheit alle Flaggenkombinationen verarbeite. Wenn Sie ein weiteres Flag hinzufügen, erhöht es sich proportional.

Wir hatten ein ähnliches Problem. Es schien eine einfache Aufgabe zu sein, aber als wir uns entwickelten und daran arbeiteten, stellten wir fest, dass etwas schief lief. Ich werde über die Funktionen sprechen, die wir gestartet haben. Es wird als Löschen lokaler Fotos bezeichnet. Der Punkt ist, dass der Benutzer einige Daten im automatischen Modus in die Cloud hochlädt. Höchstwahrscheinlich sind dies Fotos und Videos, die er auf seinem Handy aufgenommen hat. Es stellt sich heraus, dass sich die Dateien in der Cloud zu befinden scheinen. Warum sollten Sie wertvollen Speicherplatz auf Ihrem Telefon beanspruchen, wenn Sie diese Fotos löschen können?



Designer haben ein solches Konzept entworfen. Es scheint nur ein Dialog zu sein, es hat eine Überschrift, in der der freie Speicherplatz gezeichnet wird, den Nachrichtentext und ein Häkchen, dass es zwei Reinigungsmodi gibt: Löschen Sie alle Fotos, die der Benutzer hochgeladen hat, oder nur diejenigen, die älter als ein Monat sind.



Wir haben nachgesehen - es scheint nichts Kompliziertes zu geben. Dialogfeld, zwei Textansichten, Kontrollkästchen, Schaltflächen. Als wir jedoch anfingen, dieses Problem im Detail zu bearbeiten, wurde uns klar, dass es eine langfristige Aufgabe ist, Daten darüber zu erhalten, wie viele Dateien wir löschen können. Daher müssen wir dem Benutzer eine Art Stub zeigen. Dies ist ein Pseudocode, im wirklichen Leben sieht er anders aus, aber die Bedeutung ist dieselbe.



Wir überprüfen einen Status, überprüfen, ob wir rechnen, und ziehen einen Wartestecker.



Wenn die Berechnungen beendet sind, haben wir verschiedene Möglichkeiten, was dem Benutzer angezeigt werden soll. Zum Beispiel ist die Anzahl der Dateien, die wir löschen können, Null. In diesem Fall ziehen wir eine Nachricht an den Benutzer, dass nichts zu löschen ist. Kommen Sie also beim nächsten Mal. Dann kommen Designer zu uns und sagen, dass wir zwischen Situationen unterscheiden müssen, in denen der Benutzer die Dateien bereits gelöscht hat oder nichts gelöscht hat, nichts geladen wurde. Daher erscheint eine andere Bedingung, dass wir auf den Start warten und ihm eine weitere Nachricht zeichnen.



Dann gibt es Situationen, in denen dennoch etwas funktioniert hat und der Benutzer beispielsweise ein Häkchen hat, um keine neuen Dateien zu löschen. In diesem Fall gibt es auch zwei Möglichkeiten. Entweder können die Dateien bereinigt werden oder die Dateien können nicht bereinigt werden, dh es wurden bereits alle Dateien gelöscht. Wir warnen Sie daher, dass Sie bereits alle neuen Dateien gelöscht haben.




Es gibt noch eine Bedingung, unter der wir wirklich etwas löschen können. Deaktiviert, und es gibt eine Option, mit der Sie etwas löschen können. Sie sehen sich diesen Code an und es scheint, dass etwas nicht stimmt. Ich habe noch nicht alles aufgelistet. Wir haben eine Permishin-Prüfung, da ohne sie nichts funktioniert. Wir können die Dateien auf der Karte nicht berühren. Außerdem müssen wir überprüfen, ob der Benutzer das automatische Laden aktiviert hat, da die Funktionen ohne automatisches Laden unbrauchbar sind zu reinigen. Und noch ein paar Bedingungen. Und verdammt, es scheint so einfach zu sein, und so viele Probleme sind dadurch entstanden.

Und offensichtlich treten sofort mehrere Probleme auf. Erstens ist dieser Code nicht lesbar. Hier ist ein bestimmter Pseudocode dargestellt, aber in einem realen Projekt ist er über verschiedene Funktionen, Codeteile, verteilt und mit dem Auge nicht so leicht wahrzunehmen. Die Unterstützung für solchen Code ist ebenfalls recht kompliziert. Besonders wenn Sie zu einem neuen Projekt kommen, wird Ihnen gesagt, dass Sie eine solche Funktion erstellen müssen, Sie fügen eine Bedingung hinzu, überprüfen ein positives Szenario, alles funktioniert, aber dann kommen Tester und sagen, dass unter bestimmten Bedingungen alles kaputt ist. Dies geschieht, weil Sie einfach keine Szenarien berücksichtigt haben.

Außerdem ist es in dem Sinne überflüssig, dass wir, da wir einen großen Zweig von Bedingungen haben, alle Bedingungen, die nicht zu uns passen, im Voraus prüfen müssen. Sie sind im Voraus negativ, aber da sie mit solchen Zweigen geschrieben sind, müssen wir sie überprüfen. Tatsache ist, dass ich im Beispiel eine Art Boolesche Flags habe, aber in der Praxis können Sie Funktionen aufrufen, die irgendwo tiefer in die Datenbank gehen. Alles kann sein, aufgrund von Redundanz wird es zusätzliche Bremsen geben.

Und das Traurigste ist ein unerwartetes Verhalten, das während der Testphase übersehen wurde, dort passierte nichts, und irgendwo in der Produktion passierte der Benutzer bestenfalls nicht, eine Art UI-Kurve, und im schlimmsten Fall fiel es oder die Daten gingen verloren . Nur die Anwendung hat sich nicht konsistent verhalten.

Wie kann man dieses Problem lösen? Durch die Kraft der Zustandsmaschine.



Die Hauptaufgabe, die die Zustandsmaschine erledigt, besteht darin, eine große komplexe Aufgabe in kleine diskrete Zustände zu zerlegen, mit denen einfacher zu interagieren und zu verwalten ist. Nachdem wir gesessen haben und nachgedacht haben, wie wir unseren Staat an all das binden können, da wir versuchen, etwas MVP zu tun? Wir sind zu einem solchen Schema gekommen. Wer auch immer das GOF-Buch liest, ist ein klassisches Zustandsmuster, genau das, was es als Kontext bezeichnet, ich habe es als Staats-Oner bezeichnet, und tatsächlich ist es ein Moderator. Der Präsentator hat diesen Status, weiß, wie er sie wechselt, und kann unseren Status dennoch einige Daten bereitstellen, wenn er etwas wissen möchte, z. B. die Dateigröße oder eine asynchrone Anforderung anfordern möchte.



Hier gibt es nichts Super-Super, die nächste Folie ist wichtiger.



Damit müssen Sie mit der Entwicklung beginnen, wenn Sie mit der Erstellung einer Zustandsmaschine beginnen. Sie sitzen an Ihrem Computer oder irgendwo am Tisch und zeichnen entweder auf einem Blatt Papier oder mit Spezialwerkzeugen ein Zustandsdiagramm. Es gibt auch nichts Kompliziertes, aber diese Phase hat viele Vorteile. Erstens können Sie frühzeitig einige Inkonsistenzen in der Geschäftslogik erkennen. Ihre Produkte mögen kommen, ihren Wunsch ausdrücken, alles ist in Ordnung, aber wenn Sie anfangen, Code zu schreiben, verstehen Sie, dass etwas nicht zusammenpasst. Ich denke, jeder hatte eine solche Situation. Wenn Sie jedoch ein Diagramm erstellen, können Sie frühzeitig erkennen, dass etwas nicht andockt. Es wird ganz einfach gezeichnet, es gibt spezielle Tools wie PlantUML, in denen Sie nicht einmal zeichnen müssen, Sie müssen in der Lage sein, Pseudocode zu schreiben, und es selbst generiert Grafiken.

Unser Diagramm sieht folgendermaßen aus und beschreibt den Status dieses Dialogfelds. Es gibt mehrere Zustände und die Logik des Übergangs zwischen ihnen.



Fahren wir mit dem Code fort. Geben Sie an, es gibt nichts Wichtiges. Hauptsache, es gibt drei Methoden: onEnter, das bei der Eingabe zuerst invalidateView aufruft. Warum wird das gemacht? Sobald wir in den Status gelangen, wird die Benutzeroberfläche aktualisiert. Außerdem gibt es die invalidateView-Methode, die wir überladen, wenn wir etwas mit der Benutzeroberfläche tun müssen, und die onExit-Methode, mit der wir etwas tun können, wenn wir den Status verlassen.



Staatsbesitzer. Eine Schnittstelle, die den Klickstatus bietet. Wie wir herausgefunden haben, wird es ein zukünftiger Moderator sein. Und dies sind Methoden, die zusätzlichen Zugriff auf Daten bieten. Wenn Daten zwischen Status durchsucht werden, können wir sie im Presenter behalten und über diese Schnittstelle weitergeben. In diesem Fall können wir die Größe der Dateien angeben, die wir bereinigen können, und die Möglichkeit bieten, eine Anfrage zu stellen. Wir sind in einem Zustand, wir möchten etwas anfordern und über StateOwner können wir eine Methode aufrufen.

Ein weiterer solcher Nutzen ist, dass auch er einen Link zur Ansicht zurückgeben kann. Dies geschieht, damit Sie, wenn Sie einen Status haben und einige Daten eintreffen, nicht in einen neuen Status wechseln möchten, sondern nur redundant sind. Sie können die Ansicht und den Text direkt aktualisieren. Wir verwenden dies, um die Anzahl der Ziffern zu aktualisieren, die der Benutzer sieht, wenn er den Dialog betrachtet. Wir sind in der Laufzeit beim Herunterladen von Dateien, er schaut sich den Dialog an und die Zahlen werden aktualisiert. Wir bewegen uns nicht in einen neuen Zustand, wir aktualisieren nur die aktuelle Ansicht.



Hier ist das Standard-MVP, alles sollte extrem einfach sein, keine Logik, einfache Methoden, die etwas zeichnen. Ich halte mich an dieses Konzept. Es sollte keine Logik geben, zumindest keine Aktion. Wir nehmen sauber eine Textansicht, ändern sie, nicht mehr.



Moderator Es gibt interessantere Dinge. Zunächst können wir Daten für einige Zustände durchsuchen. Wir haben zwei Variablen, die mit der Zustandsanmerkung markiert sind. Wer Icepick benutzt hat, ist damit vertraut. Wir schreiben keine Serialisierung mit unseren Händen in Partible, wir verwenden eine vorgefertigte Bibliothek.

Das Folgende ist der Ausgangszustand. Es ist immer nützlich, den Ausgangszustand festzulegen, auch wenn nichts unternommen wird. Der Nutzen ist, dass Sie keine Nullprüfungen durchführen müssen, aber wenn wir sagen, dass es etwas tun kann. Beispielsweise müssen Sie für den Lebenszyklus Ihrer Anwendung einmal etwas tun. Wenn wir beginnen, müssen Sie die Prozedur einmal ausführen und nie wieder ausführen. Wenn wir den Ausgangszustand verlassen, können wir immer so etwas tun, und wir kehren nie in diesen Zustand zurück. Geben Sie so ein, dass das Zustandsdiagramm gezeichnet wird. Obwohl wer weiß, wer zeichnen wird, können Sie vielleicht zurückkommen.

Ich bin dafür, die Überprüfungen auf Null usw. zu minimieren, daher behalte ich hier einen Link zu einer einfachen Ansichtsimplementierung. Wir müssen nichts synchronisieren, nur irgendwann, wenn das Trennen passiert, ersetzen wir die Ansicht durch eine leere, und der Präsentator kann irgendwo in den Status wechseln, denken, dass es eine Ansicht gibt, sie aktualisiert sie, aber tatsächlich funktioniert sie mit leerer Implementierung.

Es gibt mehrere weitere Methoden, um den Status zu speichern, aber wir möchten den Umbruch der Aktivität erleben. In diesem Fall erfolgt alles über den Konstruktor. Alles ist etwas komplizierter, hier ein übertriebenes Beispiel.



Es ist notwendig, saveState weiterzuleiten. Wenn jemand mit ähnlichen Bibliotheken gearbeitet hat, ist alles ziemlich trivial. Sie können mit Ihren Händen schreiben. Und zwei Methoden sind sehr wichtig: Anhängen, aufgerufen bei onStart, und Trennen, aufgerufen bei onStop.



Welche Bedeutung haben sie? Ursprünglich wollten wir onCreateView, onDestroyView anhängen und trennen, aber das war nicht genug. Wenn Sie eine Ansicht haben, wird Ihr Text möglicherweise aktualisiert oder ein Dialogfragment wird angezeigt. Wenn Sie sich nicht in onStop verfangen und dann versuchen, das Fragment anzuzeigen, wird die bekannte Ausnahme angezeigt, dass Sie keine Transaktion festschreiben können, wenn der Status noch vorhanden ist. Verwenden Sie entweder Commit State Loss oder nicht. Daher werden wir in onStop detailliert beschrieben, während der Präsentator dort weiterarbeiten, den Status wechseln und Ereignisse abfangen wird. In dem Moment, in dem der Start erfolgt, wird das Ereignis "Ansicht angehängt" ausgelöst, und der Präsentator aktualisiert die Benutzeroberfläche so, dass sie dem aktuellen Status entspricht.




Es gibt eine Freigabemethode, die normalerweise in onDestroy aufgerufen wird. Sie trennen Ressourcen und geben sie zusätzlich frei.



Eine weitere wichtige setState-Methode. Da wir planen, die Benutzeroberfläche in onEnter und onExit zu ändern, wird der Hauptthread überprüft. Dies führt zu einer Einschränkung für uns, dass wir hier nichts Schweres tun. Alle Anforderungen müssen entweder an die Benutzeroberfläche oder asynchron sein. Der Vorteil dieses Ortes ist, dass wir hier den Ein- und Ausgang des Staates reservieren können. Dies ist sehr nützlich beim Debuggen. Wenn beispielsweise etwas schief geht, können Sie sehen, wie das System geklickt hat, und verstehen, was falsch war.



Einige Beispiele für Bedingungen. Es gibt einen Anfangszustand, der lediglich die Berechnung auslöst, wie viel Speicherplatz Sie zum Zeitpunkt der Verfügbarkeit der Ansicht freigeben müssen. Dies geschieht nach onStart. Sobald onStart passiert, wechseln wir in einen neuen Zustand und das System beginnt, Daten anzufordern.





Ein Beispiel für den Status ist Berechnen. Wir geben die Größe der Dateien mit stateOwner an, sie kriechen irgendwie in die Datenbank und dann gibt es noch eine inValidateView. Wir aktualisieren die aktuelle Benutzeroberfläche. Und viewAttached wird aufgerufen, wenn die Ansicht erneut angehängt wird. Wenn wir im Hintergrund waren, war das Rechnen im Hintergrund, wir kehren wieder zu unserer Aktivität zurück, diese Methode wird aufgerufen und aktualisiert alle Daten.



Als Beispiel für ein Ereignis haben wir stateOwner gefragt, wie viele Dateien freigegeben werden können, und es wird die Methode filesSizeUpdated aufgerufen. Hier war ich zu faul, es war möglich drei separate Methoden zu schreiben, wie zum Beispiel aktualisiert, es gibt so viele alte Dateien wie man verschiedene Ereignisse trennt. Aber Sie müssen verstehen, wenn es für Sie schwierig wird, wenn es viel einfacher wird. Es ist nicht notwendig, in Überentwicklung zu verfallen, dass jedes Ereignis eine separate Methode ist. Sie können mit einem einfachen Wenn auskommen, ich sehe nichts falsch daran.



Ich sehe mehrere mögliche Verbesserungen. Ich mag es nicht, dass wir gezwungen sind, unsere Hände um diese Methoden wie onStart, on Stop, onCreate, onSave und mehr zu werfen. Sie können an Lifecycle angehängt werden, es ist jedoch unklar, was mit saveState geschehen soll. Es gibt beispielsweise die Idee, ein Präsentatorfragment zu erstellen. Warum nicht? Ein Fragment ohne Benutzeroberfläche, das den Lebenszyklus erfasst, und im Allgemeinen brauchen wir dann nichts, alles wird von selbst zu uns fliegen.

Ein weiterer interessanter Punkt: Dieser Präsentator wird jedes Mal neu erstellt. Wenn Sie große Datenmengen im Präsentator gespeichert haben, zur Datenbank gegangen sind und einen großen Cursor gedrückt haben, ist es nicht akzeptabel, bei jedem Drehen des Bildschirms eine Anforderung anzufordern. Daher können Sie den Präsentator zwischenspeichern, z. B. ViewModule aus Architekturkomponenten, ein Fragment erstellen, das den Präsentator-Cache enthält, und diese für jede Ansicht zurückgeben.

Sie können die Tabellenmaschinen tabellarisch angeben, da das von uns verwendete Statusmuster einen wesentlichen Nachteil aufweist: Sobald Sie einem neuen Ereignis eine Methode hinzufügen müssen, müssen Sie allen Nachkommen eine Implementierung hinzufügen. Zumindest leer. Oder im Grundzustand. Dies ist nicht sehr praktisch. Daher wird in allen Bibliotheken die tabellarische Methode zur Angabe von Zustandsautomaten verwendet. Wenn Sie auf GitHub nach dem Wort FSM suchen, finden Sie eine große Anzahl von Bibliotheken, die Ihnen eine Art Builder zur Verfügung stellen, in dem Sie den Anfangszustand, das Ereignis und den Endzustand festlegen. Das Erweitern und Warten einer solchen Zustandsmaschine ist viel einfacher.

Ein weiterer interessanter Punkt: Wenn Sie das Statusmuster verwenden und Ihre Statusmaschine zu wachsen beginnt, müssen Sie höchstwahrscheinlich einige Ereignisse auf die gleiche Weise behandeln, damit der Code nicht kopiert wird. Sie erstellen einen Basisstatus. Je mehr Ereignisse auftreten, desto mehr Grundbedingungen treten auf, die Hierarchie wächst und etwas geht schief.

Wie wir wissen, muss die Vererbung durch Delegierung ersetzt werden, und hierarchische Zustandsautomaten helfen, dieses Problem zu lösen. Sie haben Zustände, die nicht von der Vererbungsstufe abhängen. Erstellen Sie einfach einen Baum von Zuständen, die den obigen Handler übergeben. Sie können auch separat lesen, eine sehr nützliche Sache. In Android werden beispielsweise hierarchische Zustandsautomaten in WatchDog Wi-Fi verwendet, das den Netzwerkstatus überwacht. Sie befinden sich direkt in der Android-Quelle.



Zu guter Letzt. Wie kann das getestet werden? Zunächst können deterministische Zustände getestet werden. Es gibt einen separaten Status. Wir erstellen eine Instanz, ziehen die onEnter-Methode und sehen, dass die entsprechenden Werte in der Ansicht aufgerufen werden. Daher überprüfen wir, ob unser Status die Ansicht korrekt aktualisiert. Wenn Ihre Ansicht nichts Ernstes tut, werden Sie höchstwahrscheinlich eine Vielzahl von Szenarien abdecken.



Sie können einige Methoden mit einer Funktion sperren, die die Größe zurückgibt, nach onEnter ein anderes Ereignis aufrufen und sehen, wie ein bestimmter Status auf bestimmte Ereignisse reagiert. In diesem Fall müssen wir in den neuen Status CleanAllFiles wechseln, wenn das Ereignis filesSizeUpdated auftritt und wenn AllFilesSize größer als Null ist. Mit Hilfe des Layouts überprüfen wir dies alles.



Und das letzte - wir können das ganze System testen. Wir konstruieren den Status, senden ein Ereignis an ihn und überprüfen, wie sich das System verhält. Wir haben drei Testphasen.Wir testen separat, wie die Benutzeroberfläche aktualisiert wird, separat testen wir, wie die Logik des Übergangs und des Wechsels zwischen Zuständen erfolgt, und separat testen wir das gesamte System als Ganzes.

Wir haben den Videoplayer für ein solches Konzept umgeschrieben und eine Abdeckung von mehr als 70% erhalten. Unter 80% der Anweisungen wurden durch solche Tests abgedeckt. Ich denke, das ist ein sehr cooler Indikator.



Was haben wir mit diesem Konzept? Zunächst einmal testen. Die Zustandsmaschine und unser Moderator können sich leicht mit dem Lebenszyklus anfreunden.

Erweiterbarkeit. Mit diesem Ansatz können Sie sich auf ein bestimmtes Konzept beschränken. Sie können etwas härten, aber höchstwahrscheinlich wird jemand, der Ihren Code überprüft, sagen: Warum tun Sie das, wenn Sie einfach einen neuen Status hinzufügen können und alles funktioniert?

- , , , . , , . , , . - , , . , , . lock . - , .

— . , , , , . , - , , -, , . , . , .

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


All Articles