Warum VIPER eine schlechte Wahl für Ihre nächste Anwendung ist

Im letzten Jahr gab es einen großen Hype um VIPER, und jeder ist davon begeistert. Die meisten dieser Artikel sind voreingenommen und versuchen zu zeigen, wie cool es ist. Ist es aber nicht. Es hat mindestens die gleiche Menge an Problemen, wenn nicht sogar mehr, wie andere Architekturmuster. In diesem Artikel möchte ich erklären, warum VIPER nicht so gut ist, wie es angepriesen wird, und für die meisten Ihrer Anwendungen nicht geeignet ist.

In einigen Artikeln über den Vergleich von Architekturen wird gewöhnlich behauptet, dass VIPER sich völlig von allen MVC-basierten Architekturen unterscheidet. Diese Behauptung ist nicht wahr: Es ist nur ein normales MVC, bei dem wir den Controller in zwei Teile aufteilen: den Interactor und den Presenter. Die Ansicht bleibt die gleiche, aber das Modell wird in Entität umbenannt. Router verdient einige besondere Worte: Ja, es stimmt, andere Architekturen bewerben diesen Teil nicht in ihrer Abkürzung, aber es gibt ihn immer noch, in impliziter (ja, wenn Sie pushViewController aufrufen, schreiben Sie einen einfachen Router) oder expliziterer Weise (zum Beispiel FlowCoordinators).

Nun möchte ich über die Vorteile sprechen, die VIPER Ihnen bietet. Ich werde dieses Buch als eine vollständige Referenz über VIPER verwenden. Beginnen wir mit dem zweiten Ziel, der Einhaltung des SRP (Single Responsibility Principle). Es ist ein wenig grob, aber welcher seltsame Geist kann das als Vorteil bezeichnen? Man wird für das Lösen von Aufgaben bezahlt, nicht für die Konformität mit irgendeinem Schlagwort. Ja, Sie benutzen immer noch TDD, BDD, Unit-Tests, Realm oder SQLite, Dependency Injection und was Ihnen sonst noch einfällt, aber Sie benutzen es, um das Problem des Kunden zu lösen, nicht um es einfach nur zu benutzen.

Ein anderes Ziel ist viel interessanter. Die Testbarkeit ist ein sehr wichtiges Anliegen. Sie verdient einen eigenen Artikel, da viele Leute darüber reden, aber nur wenige ihre Anwendungen wirklich testen und noch weniger es richtig machen.

Einer der Hauptgründe dafür ist der Mangel an guten Beispielen. Es gibt viele Artikel über das Erstellen von Unit-Tests mit assert 2 + 2 == 4, aber keine Beispiele aus dem wirklichen Leben (btw, Artsy macht einen ausgezeichneten Job, indem sie ihre Anwendungen Open-Sourcing, sollten Sie einen Blick auf ihre Projekte).

VIPER schlägt vor, alle Logik in eine Reihe von kleinen Klassen mit getrennten Verantwortlichkeiten zu trennen. Das kann das Testen einfacher machen, aber nicht immer. Ja, das Schreiben von Unit-Tests für eine einfache Klasse ist einfach, aber die meisten dieser Tests testen nichts. Betrachten wir zum Beispiel die meisten Methoden eines Presenters, so sind sie nur ein Proxy zwischen einer Ansicht und anderen Komponenten. Sie können Tests für diesen Proxy schreiben und damit die Testabdeckung Ihres Codes erhöhen, aber diese Tests sind nutzlos. Sie haben auch einen Nebeneffekt: Sie sollten diese nutzlosen Tests nach jeder Änderung im Hauptcode aktualisieren.

Der richtige Ansatz zum Testen sollte das gleichzeitige Testen von Interaktor und Presenter beinhalten, da diese beiden Teile tief integriert sind. Da wir zwischen zwei Klassen trennen, brauchen wir außerdem viel mehr Tests im Vergleich zu einem Fall, in dem wir nur eine Klasse haben. Es ist eine einfache Kombinatorik: Klasse A hat 4 mögliche Zustände, Klasse B hat 6 mögliche Zustände, also hat eine Kombination von A und B 20 Zustände, und man sollte sie alle testen.

Der richtige Ansatz, um das Testen zu vereinfachen, ist die Einführung von Reinheit in den Code, anstatt komplizierte Zustände einfach in einen Haufen von Klassen aufzuteilen.

Es mag seltsam klingen, aber das Testen von Views ist einfacher als das Testen von Geschäftscode. Ein View stellt den Zustand als eine Reihe von Eigenschaften dar und erbt das visuelle Erscheinungsbild von diesen Eigenschaften. Dann können Sie FBSnapshotTestCase für den Abgleich von Zustand und Aussehen verwenden. Es behandelt immer noch nicht einige Randfälle wie benutzerdefinierte Übergänge, aber wie oft implementieren Sie es?

Overengineered Design

VIPER ist das, was passiert, wenn ehemalige Enterprise-Java-Programmierer in die iOS-Welt eindringen. -n0damage, reddit comment

Ganz ehrlich, kann sich das jemand ansehen und sagen: „Ja, diese zusätzlichen Klassen und Protokolle haben meine Fähigkeit zu verstehen, was in meiner Anwendung vor sich geht, wirklich verbessert“.

Stellen Sie sich eine einfache Aufgabe vor: wir haben eine Schaltfläche, die eine Aktualisierung von einem Server auslöst, und eine Ansicht mit Daten aus einer Serverantwort. Raten Sie mal, wie viele Klassen/Protokolle von dieser Änderung betroffen sein werden? Ja, mindestens 3 Klassen und 4 Protokolle werden für diese einfache Funktion geändert. Erinnert sich niemand mehr daran, wie Spring mit einigen Abstraktionen begann und mit AbstractSingletonProxyFactoryBean endete? Ich will immer eine „bequeme Proxy-Factory-Bean-Superklasse für Proxy-Factory-Beans, die nur Singletons erzeugen“ in meinem Code.

Redundante Komponenten

Wie ich bereits erwähnt habe, ist ein Presenter normalerweise eine sehr dumme Klasse, die nur Proxy-Aufrufe von View zu Interactor (so etwas in der Art) macht. Ja, manchmal enthält er komplizierte Logik, aber in den meisten Fällen ist er nur eine redundante Komponente.

„DI-freundliche“ Menge an Protokollen

Es gibt eine häufige Verwirrung mit dieser Abkürzung: VIPER implementiert SOLID-Prinzipien, wobei DI „dependency inversion“ bedeutet, nicht „injection“. Dependency Injection ist ein Spezialfall des Inversion of Control Patterns, das mit der Dependency Inversion verwandt ist, sich aber von ihr unterscheidet.

Bei der Dependency Inversion geht es um die Trennung von Modulen auf verschiedenen Ebenen durch die Einführung von Abstraktionen zwischen ihnen. Zum Beispiel sollte ein UI-Modul nicht direkt von einem Netzwerk- oder Persistenzmodul abhängig sein. Inversion of Control ist etwas anderes, nämlich wenn ein Modul (normalerweise aus einer Bibliothek, die wir nicht ändern können) etwas an ein anderes Modul delegiert, das typischerweise dem ersten Modul als Abhängigkeit zur Verfügung gestellt wird. Ja, wenn Sie eine Datenquelle für Ihre UITableView implementieren, verwenden Sie das IoC-Prinzip. Die Verwendung der gleichen Sprachfunktionen für verschiedene High-Level-Zwecke ist hier eine Quelle der Verwirrung.

Zurück zu VIPER. Es gibt viele Protokolle (mindestens 5) zwischen den einzelnen Klassen innerhalb eines Moduls. Und sie sind überhaupt nicht erforderlich. Presenter und Interactor sind keine Module aus verschiedenen Schichten. Die Anwendung des IoC-Prinzips kann sinnvoll sein, aber stellen Sie sich eine Frage: Wie oft implementieren Sie mindestens zwei Presenter für eine Ansicht? Ich glaube, die meisten von Ihnen haben mit Null geantwortet. Warum ist es also notwendig, diesen Haufen von Protokollen zu erstellen, die wir nie benutzen werden?

Auch kann man wegen dieser Protokolle nicht einfach mit einer IDE durch den Code navigieren, weil cmd+click zu einem Protokoll führt, statt zu einer echten Implementierung.

Performance-Probleme

Es ist ein entscheidender Punkt, und es gibt viele Leute, die sich einfach nicht darum kümmern, oder die Auswirkungen schlechter Architekturentscheidungen unterschätzen.

Ich werde nicht über das Typhoon-Framework sprechen (das in der ObjC-Welt sehr beliebt für Dependency Injection ist). Natürlich hat es einen gewissen Einfluss auf die Leistung, vor allem, wenn Sie Auto-Injection verwenden, aber VIPER verlangt nicht, dass Sie es verwenden. Stattdessen möchte ich über die Laufzeit und den Start der Anwendung sprechen und darüber, wie VIPER Ihre Anwendung buchstäblich überall verlangsamt.

Startzeit der Anwendung. Darüber wird selten gesprochen, aber es ist ein wichtiges Thema, denn wenn Ihre App sehr langsam startet, werden die Nutzer sie nicht benutzen. Es gab eine Sitzung auf der letzten WWDC über die Optimierung der App-Startzeit im letzten Jahr. TL; DR: Die Startzeit Ihrer App hängt direkt davon ab, wie viele Klassen Sie haben. Wenn Sie 100 Klassen haben, ist das in Ordnung, niemand wird das bemerken. Aber wenn Ihre App nur 100 Klassen hat, brauchen Sie dann wirklich diese komplizierte Architektur? Wenn Ihre Anwendung jedoch sehr groß ist, z. B. wenn Sie an einer Facebook-Anwendung arbeiten (18.000 Klassen), ist die Auswirkung enorm, d. h. etwa 1 Sekunde, wie in der zuvor erwähnten Sitzung beschrieben. Ja, der Kaltstart Ihrer App wird 1 Sekunde dauern, nur um alle Klassen-Metadaten zu laden und sonst nichts, Sie haben richtig gelesen.

Laufzeitversand. Es ist komplizierter, viel schwieriger zu profilieren und meist nur für Swift-Compiler anwendbar (weil ObjC reiche Laufzeitfähigkeiten hat und ein Compiler diese Optimierungen nicht sicher durchführen kann). Lassen Sie uns darüber sprechen, was unter der Haube vor sich geht, wenn Sie eine Methode aufrufen (ich verwende „aufrufen“ anstelle von „eine Nachricht senden“, weil der zweite Begriff für Swift nicht immer korrekt ist). Es gibt 3 Arten von Dispatch in Swift (von schneller zu langsamer): statisch, Tabellen-Dispatch und das Senden einer Nachricht. Der letzte Typ ist der einzige, der in ObjC verwendet wird, und er wird in Swift verwendet, wenn Interop mit ObjC-Code erforderlich ist, oder wenn wir eine Methode als dynamic deklarieren. Natürlich ist dieser Teil der Laufzeitumgebung enorm optimiert und für alle Plattformen in Assembler geschrieben. Aber was wäre, wenn wir diesen Overhead vermeiden könnten, weil der Compiler zur Kompilierzeit weiß, was aufgerufen werden soll? Das ist genau das, was der Swift-Compiler mit statischer und tabellarischer Abarbeitung macht. Statisches Dispatching ist schnell, aber der Compiler kann es nicht verwenden, wenn er sich nicht zu 100 % auf die Typen im Ausdruck verlassen kann. Aber wenn der Typ unserer Variablen ein Protokoll ist, ist der Compiler gezwungen, Dispatch über Protokollzeugen Tabellen zu verwenden. Es ist nicht buchstäblich langsam, aber 1ms hier, 1ms dort, und jetzt ist die Gesamtausführungszeit mehr als eine Sekunde höher als wir mit reinem Swift-Code erreichen könnten. Dieser Absatz hängt mit dem vorhergehenden über Protokolle zusammen, aber ich denke, es ist besser, die Bedenken über die unbedachte Menge an unbenutzten Protokollen von der wirklichen Verwirrung des Compilers zu trennen.

Schwache Abstraktionen trennen

Es sollte einen – und vorzugsweise nur einen – offensichtlichen Weg geben, dies zu tun.

Eine der beliebtesten Fragen in der VIPER-Gemeinschaft ist „wo soll ich X unterbringen?“. Auf der einen Seite gibt es viele Regeln, wie man es richtig machen sollte, aber auf der anderen Seite basiert vieles auf Meinungen. Es kann sich um komplizierte Fälle handeln, zum Beispiel die Behandlung von CoreData mit NSFetchedResultsController oder UIWebView. Aber auch alltägliche Fälle, wie die Verwendung von UIAlertController, sind ein Thema für Diskussionen. Schauen wir uns das mal an, hier haben wir einen Router, der mit einer Meldung umgeht, aber dort haben wir eine Ansicht, die eine Meldung präsentiert. Man kann versuchen, dies mit dem Argument zu umgehen, dass eine einfache Meldung ein Spezialfall für eine einfache Meldung ohne irgendwelche Aktionen außer dem Schließen ist.

Spezialfälle sind nicht speziell genug, um die Regeln zu brechen.

Ja, aber warum haben wir hier eine Fabrik für die Erstellung dieser Art von Meldungen? Also, wir haben ein Durcheinander sogar mit solch einem einfachen Fall mit UIAlertController. Willst du das?

Codeerzeugung

Lesbarkeit zählt.

Wie kann das ein Problem mit einer Architektur sein? Es wird einfach ein Haufen von Template-Klassen generiert, anstatt sie selbst zu schreiben. Wo ist das Problem dabei? Das Problem ist, dass man die meiste Zeit Code liest (es sei denn, man arbeitet in einem Wegwerf-Outsourcing-Unternehmen), anstatt ihn zu schreiben. Die meiste Zeit liest man also Standardcode, der mit echtem Code vermischt ist. Ist das gut? Ich glaube nicht.

Schlussfolgerung

Ich verfolge nicht das Ziel, Sie davon abzubringen, VIPER überhaupt zu benutzen. Es gibt immer noch einige Anwendungen, die von allen profitieren können. Bevor Sie jedoch mit der Entwicklung Ihrer App beginnen, sollten Sie sich ein paar Fragen stellen:

  1. Ist diese App für eine lange Lebensdauer ausgelegt?
  2. Sind die Spezifikationen stabil genug? Sonst kann es passieren, dass Sie selbst bei kleinen Änderungen endlos große Refactorings durchführen müssen.
  3. Testen Sie Ihre Anwendungen wirklich? Sei ehrlich zu dir selbst.

Nur wenn du alle Fragen mit „Ja“ beantwortest, kann VIPER eine gute Wahl für deine App sein.

Zu guter Letzt: Du solltest dein eigenes Gehirn für deine Entscheidungen benutzen, vertraue nicht einfach blind irgendeinem Typen von Medium oder einer Konferenz, die du besucht hast, der sagt: „Nimm X, X ist cool.“ Auch diese Leute können sich irren.