Welcher iOS-Entwickler kennt es nicht: Die Tendenz, dass je grösser ein Projekt wird, je fetter die View Controller werden. Das führt zu vielerlei Problemen, denn es leidet nicht nur die Testbarkeit des Codes darunter; auch die Abhängigkeiten sind dermassen gross, das später kaum Änderungen möglich sind ohne Stunden zu investieren. Einige Gedanken zur iOS Architektur.
Das Problem
Je weiter ein typisches iOS-Projekt forgeschritten ist, je schlimmer wird es in der Regel.
Zu Beginn ist noch alles gut: Da sind diese kleinen 30-Zeilen Controllers welche noch übersichtlich sind und jedem Entwickler dieses Gefühl geben, das noch alles in Ordnung ist.
Doch dann beginnt es, hier ein neues Feature, da ein neues, hier brauchen wir den Core-Location Ortunsdienst, da einen Core Data Managed Context für die Persistierung, und plötzlich ist er da: Der fette View Controller, über 1000 Zeilen gross, von Flexibilität keine Rede mehr.
Massive Abhängigkeiten, Testbarkeit welche nicht vorhanden ist und noch weitere Dinge sind Konsequenzen davon.
Die Ursache
Wie kommt es so weit? Einerseits sind teilweise die Entwickler Schuld: Es ist einfacher einen Controller zu erweitern, als eine neue Klasse zu erstellen und diese per Dependency Injection oder sonstwie dem Controller bekannt zu machen.
Aber zu einem grossen Teil ist das Ganze auch der MVC-Implementierung von Apple zu zuzuschreiben.
Ein essentielles Prinzip der modernen Programmierung ist das Single-Responsibillity Prinzip welches besagt, dass jegliches Element in einer OO-Umgebung genau eine Funktion und eine Aufgabe hat. Dieses Prinzip sorgt für geringere Abhängigkeit und einfacher Isolation von einzelnen Elementen, was der Testbarkeit massiv zu Gute kommt.
Anhand dieses Prinzips kann man eigentlich recht einfach das Problem identifizieren: Ein View Controller im iOS-Umfeld hat viel zu viele Tätigkeiten und ist für zu viel verantwortlich.
Mal kurz aufgezählt was der View Controller in vielen Fällen macht:
- Die ganze Verwaltung der Views (der bekannte viewDidLoad-Missbrauch wo alle Attribute der View-Komponenten dort gesetzt werden, später mehr dazu)
- Die Delegates der Views (auf Änderungen von Textfeldern reagieren z.B.)
- Erstellung der Daten-Persistierungs-Instanzen: Das ganze Erstellen von Managed Object Context etc. von Core Data
- Ausführen von Core-Data Fetchs
- UITableView mit seinem riesigen Rattenschwanz von Code, der nötig ist um diese laufen zu lassen
- Webservice-Zugriffe über NSURLConnection mit dem dazugehörigem NSURLConnectionDelegate
- und und und
- Reagieren auf Änderungen im Model
- Reagieren auf Änderungen in View (User-Eingaben) & das Weiterreichen von Selbigen zum Model
Man kann ihn als Kleber betrachten welcher das Model und die View zusammenhält ohne dass diese beiden etwas voneinander wissen sollen oder dürfen.
Lösung
Es ist schwierig dieses Problem konsequent zu beheben, aber es ist möglich. Ein einfacher Ansatz ist, sich zu Beginn Klarheit darüber zu verschaffen was der Controller später genau machen soll und welche Verantwortlichkeiten er innehält.
Bereits hier soll darauf geachtet werden, diese so minimal wie nur möglich zu halten.
Dabei hat sich insbesondere in unseren Projekten ein Ansatz sehr bewährt: Fat Model, Thin Controllers.
Hierbei versucht man so viel wie nur möglich in den Model-Layer zu bringen, denn dort lassen sich diese sehr einfach auf verschiedene Klassen verteilen.
Ein einfacher Leitsatz für die Entscheidung was in Model gehört ist immer das Szenario: Eine App, zwei Zielgeräte: iPhone & iPad.
Wenn etwas in beiden Apps gebraucht wird & nichts mit dem UI zu tun ha gibt es keine Diskussion: Ab in den Model-Layer damit!
View-Code hat nichts im Controller zu suchen
Ein weiterer Fall von typischen Missbrauchs der immer wieder vorkommt: View-Relevanter Code innerhalb des View-Controllers:
-(void)viewDidLoad{ self.submitButton.backgroundColor = [UIColor greenColor]; self.messageLabel.font = [UIFont customFont]; }
Dieser Code hat wirklich nichts dort zu suchen. Aber schon der Methoden-Name des Lifecycle-Events zeigt es: Apple hat tatsächlich sehr viele View-Relevante Sachen innerhalb des View Controllers reingetan, es ist für den Entwickler schwierig dem entgegen zu wirken.
Eine Möglichkeit in diesem Fall ist, eine Klasse zu erstellen die UIView ableitet und dort den gesamten Code zu machen. Dann hat man innerhalb des Controllers die View-Instanz und führt über diese sämtliche relevanten Dinge auf:
-(void)viewDidLoad{ [self.view loadView]; }
Der View-Controller muss also nichts wissen von den View-Details (z.B. welche Farbe ein Button bekommen soll etc.), und die View ist wiederverwendbar in anderen Controllern.
UITableView und dessen Delegate / Datasource
Ein oft verwendetes Element in iOS-Apps ist das UITableView.
Achtet mal im täglichen Gebrauch darauf, nahezu sämtliche Apps bestehen aus Ihnen: Die Instant Messaging-Apps wie z.B. WhatsApp, Social-Media Clients wie Facebook, etc. etc.
TableViews bestehen aus drei Komponenten: Die Tableview welche die UIView erweitert (bzw. die UIScrollView), die Datasource, welche auf dem Papier für das Daten-Liefern zuständig ist & der Delegate welcher auf Änderungen in der View reagiert.
Das ist die Theorie. In Tat und Wahrheit klebt das Delegate so mit dem Datasource zusammen das man diese nicht trennen soll. Man könnte es, aber das erzeugt so viele Probleme aufgrund der nicht sauber durchdachten Aufgabenverteilung die Apple bei diesen beiden Protokollen gemacht hat. Hier macht man sich also absolut keinen Gefallen wenn man sie trennt.
Was man jedoch trennen kann ist die TableView vom Rest. Wir pflegen üblicherweise den Delegaten & die Datasource in eine Klasse zu nehmen welche wir dann in der Regel als DataProvider bezeichnen. Diese liefert sowohl die eigentliche Daten aus dem Model, bietet aber auch die Reaktionen auf UI-Aktionen.
Dabei ist der Data-Provider keine eigentliche Model-Klasse sondern als weitere Schnittstelle zwischen View und Model zu verstehen.
Was der Controller im Falle einer Tabellarischen Auflistung macht ist relativ simpel: Er nimmt den Dataprovider, instanziert Ihn, und tätigt die Verbindung mit der Tableview. Die Tableview selber weiss nicht das es den Dataprovider überhaupt gibt, umgekehrt verhält es sich gleichermassen, es lässt sich also sagen: Dataprovider & Tableview wissen voneinander gar nichts. Der Controller als Bindeglied ist der einzige der von den beiden „weiss“, aber auch nur genau so viel wie er benötigt.
So ist auch die detaillierte Implementierung von Dataprovidern dem Controller selber egal.
Der Vorteil liegt auf der Hand: Neben der grossen Wiederverwendbarkeit haben wir so eine Trennung von verschiedenen Domänen und erzeugen eine Zusammenarbeit die relativ lose gekoppelt ist.
Dabei ist noch wichtig zu sagen dass der Dataprovider selber keine Daten holen kann, sondern Ihm diese über den Controller gegeben werden. Es wäre also theoretisch möglich, dass der Dataprovider selber gefälschte Daten bekommt, denn er nimmt eifach entgegen was Ihm der Controller gibt.
Der Code im Controller könnte dann folgendermassen ausschauen:
-(void)viewDidLoad{ [self initTableview:self.eventsList]; } -(void)initTableview:(UITableView*)tableView{ // hole zuerst Daten - woher sie kommen ist hier egal NSArray* events = [self.eventsMapper getAll]; // erstelle einen Dataprovider und weise ihm die Daten zu id dataProvider = [[EventsDataProvider alloc] init]; dataProvider.data = events; // verbinde Tableview mit DataProvider // der dataprovider ist sowohl dataSource als auch delegate // diese funktionalitäten lassen sich relativ schlecht trennen. tableView.dataSource = dataProvider; tableView.delegate = dataProvider; }
Fazit zum iOS Architektur Problem
Das Problem sollten die meisten iOS-Entwickler kennen, wir hoffen wir konnten euch mit diesem Beitrag zumindest ein bisschen helfen oder vielleicht zum Denken anregen.
Falls Fragen vorhanden sind, zögern Sie nicht sie zu stellen!
Weiterführende Links: