Abseitsfalle ReactiveCocoa

Ich bin mit dem iOS Code offenbar großer Fans von ReactiveCocoa in Kontakt gekommen, wurde davon aber zum Glück nicht nachhaltig kontaminiert. Ich kenne es also nicht nur durch geschönte Beispiele, die vorführen sollen, wie toll das alles ist, sondern auch durch Produktiv-Code aus der Praxis. Üblicherweise wird Kritikern von ReactiveCocoa nämlich vorgeworfen, sie sollten doch erstmal etwas damit bauen. Das greift also in meinem Fall nicht.

ReactiveCocoa ist inspiriert von den Reactive Extensions (Rx) für .NET von Microsoft. Ich verrate das jetzt schon, weil man sich sonst den ganzen Artikel über fragt, woher diese verqueren Ideen nur kommen. Geständnis der Macher:

Für .NET Entwickler wird dies alles unheimlich vertraut klingen. ReactiveCocoa ist im Wesentlichen eine Objective-C Version von .NETs Reactive Extensions (Rx).

For .NET developers, this all might sound eerily familiar. ReactiveCocoa essentially an Objective-C version of .NET's Reactive Extensions (Rx).

Ein User, der offenbar nicht wußte, woher ReactiveCocoa stammt, hatte anhand der enthaltenen Features genau den richtigen Verdacht:

ReactiveCocoa ist der am engsten gekoppelte Mist, den ich je gesehen habe, und liefert keinen erkennbaren Vorteil. Tut mir leid. Das ist genau die Art von Klugscheisser Framework, die man sonst nur von C# und Microsoft kennt. Es ist dämlich.

RAC is the most tightly coupled bullshit I have ever seen and provides no benefit I can see. Sorry. This is exactly the kind of DICK framework that would come from C# and Microsoft. Its stupid.

Das erklärt auch, warum sich ReactiveCocoa nicht wie eine native Cocoa (Touch) API anfühlt. Jede Zeile davon schreit Dich an: Ich bin ein Alien!

ReactiveCocoa möchte hip sein, indem es eine Form von funktionaler und reaktiver Programmierung anbietet. ReactiveCocoa geht dabei allerdings völlig an den iOS bzw. Objecive-C Design Patterns, Best Practices und den Programmierwerkzeugen (der sog. "Toolchain") vorbei. Sie machen das, vor dem Apple gerne warnt als "Don't fight the frameworks". Sie machen sich das Leben echt selbst schwer, weil sie etwas cool finden, und meinen, dies auf alles anwenden zu müssen, egal, ob es paßt und geeignet ist oder nicht. Wenn Du einen Hammer hast, dann sieht alles aus wie ein Nagel. Das Golden-Hammer-Syndrom.

ReactiveCocoa bietet zwar interessante Ansätze und Ideen, ist aber leider in seiner Umsetzung nur dazu geeignet, ein Projekt umständlich, unlesbar und unwartbar zu machen.

Ich würde ja noch ein Auge zudrücken, wenn dieses alles zu weniger Code oder einfacherem Code oder wartbarerem Code führen würde. Tut es jedoch nicht. Was hier schiefläuft, darum geht dieser Artikel.

Versprechen und Verbrechen

Cocoa und Cocoa Touch bieten von sich aus schon unter anderem mit Bindings, Outlets und Observern standardmäßig Reactive Programming an. Und Funktionale Programmierung läßt sich ebenfalls mit Blocks als Rückgabewert oder Parameter für Methoden realisieren. Unveränderbare Objekte, die auch zu Funktionaler Programmierung gehören, finden sich in den Basisklassen von Objective-C, die alle immutable sind: NSString beispielsweise.

Cocoa Touch hat im Gegensatz zu Cocoa keine Bindings. Man sagt, es läge wohl daran, daß es zuviel Performance kosten könnte auf mobilen Geräten. Darum ist es möglicherweise auch keine gute Idee von ReactiveCocoa, eine Variante davon selbst einzuführen.

Bad performance

Der Entwickler Nikita Leonov, der zu ReactiveCocoa beigetragen hat, bestätigt die Performance-Probleme von ReactiveCocoa.

Manche Infizierte schreiben ihre App neu mit ReactiveCocoa, nur um zu erkennen, daß sie dank ReactiveCocoa spürbar lahmer als vorher reagiert. Und das dies ein genrelles Problem mit ReactiveCocoa ist, zeigt, daß der Hauptentwickler von ReactiveCocoa, Justin Spahr-Summers, ihm auch nicht helfen konnte: "Sorry to hear about RAC's failings here". Der Workaround war dann, weniger ReactiveCocoa einzusetzen.

ReactiveCocoa möchte all die spezialisierten Mechanismen wie Delegates, Callbacks, Notifications, Target-Action, Responder Chain, Key-Value-Observing, die ihren Sinn und ihre Aufgabe haben, durch einen eigenen Mechanismus ersetzen: Signale und deren Verkettung. Es ist ein Framework zum Erstellen und Transformieren von Sequenzen von Werten.

In einem Blogpost beschreiben die ReactiveCocoa Autoren die uniformierte Verpackung diverser Apple APIs als ihr Hauptziel.

Diese Vereinheitlichung wäre einer der großen Vorteile. Das sieht dann so aus:

[[[[[[self requestAccessToTwitterSignal]
  then:^RACSignal *{
    @strongify(self)
    return self.searchText.rac_textSignal;
  }]
  filter:^BOOL(NSString *text) {
    @strongify(self)
    return [self isValidSearchText:text];
  }]
  flattenMap:^RACStream *(NSString *text) {
    @strongify(self)
    return [self signalForSearchWithText:text];
  }]
  deliverOn:[RACScheduler mainThreadScheduler]]
  subscribeNext:^(NSDictionary *jsonSearchResult) {
    NSArray *statuses = jsonSearchResult[@"statuses"];
    NSArray *tweets = [statuses linq_select:^id(id tweet) {
      return [RWTweet tweetWithStatus:tweet];
    }];
    [self.resultsViewController displayTweets:tweets];
  } error:^(NSError *error) {
    NSLog(@"An error occurred: %@", error);
  }];

Das Erstellen von Signalen und Vereinheitlichen von APIs sieht dann so aus, daß aus vielen einfachen APIs eine komplizierte gemacht wurde. Aus spezialisierten direkten Lösungen wurde eine allgemeine, verpackende für alles nicht so richtig passende. ReactiveCocoa kann nicht mal Arrays direkt verarbeiten, sondern muß diese erst in Sequenzen umwandeln und am Schluß wieder zurück in Arrays.

ReactiveCocoa kann einen Selector in ein Signal verpacken, wobei die benannten Parameter verloren gehen und durch ein namenloses Tupel ersetzt werden. Möchte man das Signal auswerten und auf einen bestimmten Parameter zugreifen, dann geht das nicht per Name, sondern nur noch mit Positionsangabe. Das ist einfach unlesbar. Sie machen das beispielsweise gern, um Delegate-Callbacks nicht einfach direkt zu implementieren, sondern erst in Signale zu verpacken und dann wieder schwer lesbar auszupacken.

ReactiveCocoa verliert die lose Kopplung, die Cocoa (Touch) Programmierung mit den Delegates auszeichnet. ReactiveCocoa nimmt sogar eine schichtenübergreifende enge Kopplung vom View, Controller und Model inklusive Networking vor, indem es alles über einen einzelnen wahnsinnig langen Befehl in eine Signalkette steckt. Typischerweise landet die komplette Logik bei Nutzung von ReactiveCocoa in viewDidLoad, was diese Methode nicht im Sinne des Erfinders aufbläht und überfrachtet mit artfremden Aufgaben. Betroffene berichten davon, daß viewDidLoad zu einer RACParty verkommt.

ReactiveCocoa hängt an jede UI-Klasse von Apple ein reaktives Anhängsel, das bestimmte originale Funktionen zusätzlich verpackt. Sie lassen für ihre Implementierung von Class-Swizzling bis objc_setAssociatedObject keinen Trick aus. Bei manchem Kommentar, zum Beispiel in NSObject+RACSelectorSignal.m, wird klar, daß sie sich bei ihrem Hack teilweise auf ihr Glück verlassen:

Since the object's class was almost certainly dynamically changed, we shouldn't see another of these classes in the hierarchy.

Wie derbe das in die Hose gehen kann, sieht man, wenn ein zweites 3rd Party Framework, sagen wir mal NewRelic, dieselben System-Methoden austauscht: Dann knallt es so richtig.

An manchen Bugs wird klar, daß sie nicht ahnen, was sie tun. Zum Beispiel nutzen sie Spinlocks, die auf iOS illegal sind.

Um einen Block zeitversetzt auszuführen, nutzt ReactiveCocoa NSThread +sleepUntilDate:, was eine üble Verschwendung von Systemressourcen ist. ReactiveCocoa blockiert Threads anstatt asynchron zu arbeiten.

Warum sollte man 16.000 Zeilen Dritt-Code in sein Projekt aufnehmen? Da sind mehr Fehler drin, als in dem Code, den man in Summe für die eigentliche App schreibt ohne so etwas.

Mir sind APIs unsympathisch, die Seiteneffekte als festes Design-Prinzip haben: Mindestens die Methode subscribeNext: und ihre Verwandten werden für Side-Effects benutzt. Nachzulesen u. a. im einzigen Buches über ReactiveCocoa auf Seite 62.

Justin Spahr Summers

Bei Dritt-APIs ist immer die Frage: Wie lange leben die? Der sog. Busfaktor ist meist ziemlich gering. Der Hauptentwickler von ReactiveCocoa war Justin Spahr-Summers. Von ihm ist bei weitem der meiste Code. Inzwischen ist er laut seinem Twitter-Account aber raus aus der Entwicklung: Formerly contributed to ReactiveCocoa heißt es dort. Der Grund für seinen Ausstieg dürfte sein, daß er seitdem für Facebook in London arbeitet. Version 3 von ReactiveCocoa, das eine andere API hat, gibt es nur als Swift-Version mit einer Objective-C Bridge. Auf einem Vortrag in 2014 über die Pläne für Version 3 gibt Justin auch offen zu, daß ReactiveCocoa schwer verständlich ist und einige üble Sachen macht. Vielen Dank, aber das ist mir schon vorher allein aufgefallen.

Josh Abernathy

Der Entwickler mit den zweitmeisten Commits ist Josh Abernathy. Seine circa 1000 kommen allerdings noch lange nicht an Justins 2500 heran. Dann gibt es noch sechs Entwickler, die zwischen 100 und 500 Commits beigetragen haben. Der Beitrag der restlichen Leute sieht unwesentlich aus.

Am Standard vorbei

Wenn man ein privates Projekt macht, dann kann man ja tun, was man möchte. Aber sobald der Code auch von anderen Leuten gelesen, weiterentwickelt und gepflegt werden soll, ist es wichtig, sich an das Standardvorgehen der Plattform zu halten. Das tut ReactiveCocoa in keiner Weise. Aber selbst wenn es Cocoa-konform wäre, dann ist es immer noch eine weitere externe Library, die alle Beteiligten lernen und pflegen müssen.

Programme, entwickelt mit ReactiveCocoa, funktionieren anders als beispielsweise normale iOS-Apps. Der Kontroll- und Daten-Fluß ist grundlegend anders. Es werden eigene Makros definiert und exzessiv benutzt. Die gesamte API von ReactiveCocoa muß gelernt werden und erschließt sich selbst einem erfahrenem iOS-Entwickler nicht auf Anhieb.

Bad to learn

Selbst ein Programmierer, Sergey Gavrilyuk, der zu ReactiveCocoa etwas beigetragen hat, berichtet von erheblichem Aufwand: Er habe ein Jahr gebraucht, um ReactiveCocoa zu lernen. Ich hatte seinen Namen wiedererkannt auf Twitter, weil ich die Rankliste der GitHub-Commits zu diesem Projekt zuvor durchgegangen war.

No doc

Ein Entwickler, der sich schon in vorherige Versionen von ReactiveCocoa eingearbeitet hat, schreibt: Er verzweifele schon wieder an ReactiveCocoa (an der jeweils aktuellen Version); es würden nicht nur Best Proctices fehlen, sondern so ziemlich alles. Die Dokumentation von ReactiveCocoa wäre ja schon immer nicht vorhanden gewesen und es würde leider nicht besser. ReactiveCocoa zu lernen, wäre halt scheiße hart.

Die Dokumentation ist wirklich schmal und das einzige offiziell verlinkte Buch deckt auch nicht alles Nötige ab.

Es geht ReactiveCocoa darum, den nicht-reaktiven, also normalen Anteil der App komplett zu verdrängen bzw. wo das nicht möglich ist, abzukapseln in ReactiveCocoa Signale. Das führt dazu, daß Entwickler, die gemäß Apples Vorgaben iOS-Apps schreiben, konfrontiert mit einer ReactiveCocoa-App, keine Ahnung haben, was da abgeht.

Ich habe mir die Mühe gemacht und mich in ReactiveCocoa eingearbeitet mit dem Ziel: Was ich verstehe, kann ich auch wieder ersetzen durch normales Vorgehen, das jeder kennt.

An der Toolchain vorbei

Wie man in diesem ReactiveCocoa Tutorial – The Definitive Introduction sieht, ersetzt ReactiveCocoa das Target-Action Pattern im Interface Builder mit einer Eigenkonstruktion: "Der Touch Up Inside Event auf dem Button ist über eine Storyboard Action mit einer Methode im ViewController verbunden. Das soll durch eine reaktive Variante ersetzt werden, lösche also zuerst die Storyboard Action." Geht's noch? Der Hammer ist jedoch, mit welchem Code sie das ersetzen. Sämtlicher Target Action Code wird durch Verkettungen von Signalen ersetzt.

Und wie man im einzigen Buches über ReactiveCocoa u. a. auf Seite 43 nachlesen kann, bricht ReactiveCocoa mit Model-View-Controller. Dort übernimmt ein View auch schonmal die Aufgabe, die Daten zu besorgen. Auf den Seiten 50ff kann man sehen, wie sie sogar Netzwerk-Calls an Views hängen. Aber darüber habe ich bereits eine eigene Abhandlung geschrieben: Korrekte Netzwerk-Kommunikation.

Unter Formatting of Pipelines erzählt Colin in seinem "ReactiveCocoa Tutorial – The Definitive Introduction", wie man typischen ReactiveCocoa Code formatieren soll, damit er besser lesbar wird, und faselt dabei etwas von "generell akzeptierter Konvention". Dummerweise ist das aber nur in seinem Kopf, eine ReactiveCocoa-La-La-Land Konvention, die zudem inkompatibel ist zur automatischen Formatierung von Xcode. Das fällt ihm auch auf und er meint, man würde sich dann wohl unglücklicherweise im Krieg mit der Einrücken-Funktion befinden. Genau das, was einem gerade noch gefehlt hat, wenn man über Code nachdenkt: Jede Zeile händisch einrücken.

Natürlich ist der ReactiveCocoa-Code schlecht lesbar: Es ist keine Seltenheit, sechs oder mehr Signale zu verketten bei ReactiveCocoa, weil das den Kern dieser Methode ausmacht. Das sind dann sechs geschachtelte Methoden-Aufrufe, von denen jeder als Parameter einen Block bekommt. Natürlich führt so eine kranke Anhäufung von Komplexität zu Code-Lesestörungen. Aber man sollte das Übel an der Wurzel packen und nicht nur seine Symptome mit unüblicher Formattierung lindern.

Wer erwachsene Programmierer weinen sehen will, der muß sie mal einen Breakpoint setzen lassen in Code, der ReactiveCocoa verwendet: Du siehst einfach nur Müll und nichts, was Dir dabei weiterhilft, herauszufinden, was da passiert, woher der Aufruf kommt, wie es dazu kommt, daß Du hier landest.

Avoid Debug

Ein anderere Programmierer, Nikita Leonov, der ebenfalls etwas zu ReativeCocoa beigetragen hat, schreibt: ReactiveCocoa zu debuggen, wäre so hart, daß er es um jeden Preis vermeiden würde.

Fragt man nach Debugging, dann bekommt man Logging als Antwort. ReactiveCocoa listet unter dem Stichwort Debugging nur Logging.

Wenn man in diesem Code versucht die jeweils passierende Aktualisierung des Label-Textes mit einem Breakpoint zu debuggen, indem man sich someObject in dem Block ansieht, dann lernt man, daß dort nur einmal gehalten wird: Bei Erstellung des Signals, aber nicht bei dessen Nutzung.

RAC(self.someLabel, text) = [someSignal
flattenMap:^RACStream *(SomeClass *someObject)
{
    @strongify(self);
    self.someLabel.text = nil;
    return someObject ? someObject.titleSignal : [RACSignal return:@""];
}];

Komische Leute

Was für Leute kommen auf die Idee, ReactiveCococa für alles und jedes auf Teufel komm raus einzusetzen?

Jedenfalls keine Leute, denen die iOS-Entwicklung im Blut liegt. Keine Entwickler, die iOS-Spezialisten sind. Nach meiner Erfahrung sind das in der Regel Umsteiger aus der Windows-Welt, die versuchen, ihre gängigen Methoden auf iOS anzuwenden.

Da kenne ich zum Beispiel den Code einer Gruppe von Entwicklern, die ReactiveCocoa in einem iOS-Projekt komplett durchgezogen haben. Der beste iOS-Programmierer von ihnen bezeichnet sich selbst als Generalist. Das taugt nichts, denn man kann in iOS-Entwicklung nur gut sein, wenn man sich voll darauf konzentriert. Wer gleichrangig auch noch für andere Plattformen entwickelt, ist bestenfalls mittelmäßig als iOS Dev.

Ich kann die Mittelmäßigkeit bzw. Ahnungslosigkeit an einem konkreten Beispiel festmachen: Da war ein Kommentar im Code, der besagte: Er habe versucht, einen TapGestureRecognizer zu verwenden, um das Keyboard einzufahren, wenn der User außerhalb des Textfeldes toucht, wobei der Touch an die darunterliegenden Views weitergegeben werden sollte. Das habe er aber nicht hinbekommen. Darum habe er einen Interceptor-View dazwischen gelegt und diesen mit viel extra Code für Größenanpassungen und Hit-Tests versorgt. Ich habe den Unfug gelöscht, einen TapGestureRecognizer draufgelegt und dessen cancelTouchesInView Property auf NO gesetzt, fertig. Das kann man wissen, wenn man sich auch nur oberflächlich Apples Doku zu dieser Klasse angesehen hätte. Aber diese Szene war symptomatisch: Keine Ahnung haben, wie man die Bordmittel benutzt, und stattdessen einen umständlichen Workaround entwickeln. Jack of all trades, master of none. Bleib mir weg mit Deinem Code.

Ein wenig später kam noch hinzu: Sie kamen auch mit grundlegenden Dingen wie NSLocale und seinen Methoden nicht klar. Ihre Kommentare neben ihrem Code mit falschen Annahmen waren Ausdruck purer Verzweiflung. Apples Doku zu lesen war offenbar nicht ihr Ding.

Noch ein schönes Beispiel ist Ash Furrow, der Autor des offenbar einzigen Buches über ReactiveCocoa. Ein Windows-Entwickler, der sein Wissen für iOS-Entwicklung zum besten gibt. Er hatte eine schwere Kindheit (Seite 75: "I come from a .Net background") und ist mental noch nicht bei iOS angekommen (Seite 82: "… has to work within the confines of – I shudder – Objective-C.").

Ash schlägt auch MVVM als Lösung vor, wenn ein ViewController zu fett wird durch Geschäftslogik. Allerdings ist MVVM keine brauchbare Lösung, weil sie das Problem nur umbenennt bzw. verschiebt. Darüber hinaus ist MVVM ein Microsoft Pattern für "Silverlight for Windows Phone OS 7.1". Er schlägt das vor, weil er als einzige Alternative zur Verschlankung eines ViewControllers nur weitere ViewController-Subklassen sieht (Seite 87). Offenbar kennt er die üblichen Bordmittel für diese Fälle nicht: Man kann eine Objective-C Klasse mit Hilfe von Categories beliebig zerlegen (Swift: Extensions). Und man kann die wechselnde Geschäftslogik auf unterschiedliche unabhängige Klassen verteilen, die je nach wechselndem Bedarf dem ViewController als "Business"-Delegate dienen. Und im Falle von Table/CollectionViewControllern kann man auch noch Table/CollectionViewDataSource und Table/CollectionViewDelegate auslagern. Categories und Delegates, nix mit MVVM. Aber anstelle mal die Plattform kennenzulernen und zu nutzen, wird lieber Zeugs aus völlig anderen Welten portiert.

Am Schluß seines Buchs benutzt er für Tests auch nicht XCTest von Apple, sondern wieder ein anderes Framework. Wenn man nicht normal für diese Plattform programmieren will, dann sollte man doch besser da bleiben, wo man hergekommen ist.

Ashs Buch ist zwar als Schnupperkurs für angehende Code-Masochisten gegeignet, er läßt jedoch viele Teile der ReactiveCocoa API weg, was bei den unter hundert Seiten, die er Stand Dezember 2015 anbietet, kein Wunder ist. Nicht mal alle Basic Operators with -then: kommen da vor. Man könnte nach seinem Buch also obiges Beispiel in meinem Artikel gar nicht verstehen.

Valid XHTML 1.0!

Besucherzähler


Latest Update: 26. June 2022 at 11:29h (german time)
Link: macmark.de/dev/osx_dev_reactivecocoa.php