Einführung
Im Moment gibt es viele Artikel über VIPER - saubere Architektur, von denen verschiedene Variationen zu einer Zeit für iOS-Projekte populär wurden. Wenn Sie mit Viper nicht vertraut sind, können Sie es
hier ,
hier oder
hier lesen.
Ich möchte über die VIPER-Alternative - Clean Swift - sprechen. Clean Swift sieht auf den ersten Blick wie VIPER aus, die Unterschiede werden jedoch sichtbar, nachdem das Prinzip der Interaktion zwischen Modulen untersucht wurde. In VIPER basiert die Interaktion auf Presenter, überträgt Benutzeranforderungen zur Verarbeitung an den Interactor und formatiert die von ihm empfangenen Daten zur Anzeige auf dem View Controller zurück:

In Clean Swift sind die Hauptmodule wie in VIPER View Controller, Interactor und Presenter.

Die Wechselwirkung zwischen ihnen erfolgt in Zyklen. Die Datenübertragung basiert auf Protokollen (ähnlich wie bei VIPER), mit denen zukünftige Änderungen an einer der Systemkomponenten einfach durch eine andere ersetzt werden können. Der Interaktionsprozess sieht im Allgemeinen folgendermaßen aus: Der Benutzer klickt auf die Schaltfläche, View Controller erstellt ein Objekt mit einer Beschreibung und sendet es an Interactor. Interactor wiederum implementiert ein bestimmtes Szenario gemäß der Geschäftslogik, erstellt ein Ergebnisobjekt und übergibt es an Presenter. Presenter bildet ein Objekt mit Daten, die für die Anzeige für den Benutzer formatiert sind, und sendet sie an den View Controller. Schauen wir uns jedes Clean Swift-Modul genauer an.
Ansicht (View Controller)
View Controller führt wie in VIPER alle VIew-Konfigurationen durch, sei es Farb-, UILabel- oder Layout-Schriftarteneinstellungen. Daher implementiert jeder UIViewController in dieser Architektur ein Eingabeprotokoll zum Anzeigen von Daten oder zum Reagieren auf Benutzeraktionen.
Interactractor
Interactor enthält die gesamte Geschäftslogik. Es akzeptiert Benutzeraktionen von der Steuerung, wobei Parameter (z. B. geänderter Text des Eingabefelds, Drücken einer Taste) im Eingabeprotokoll definiert sind. Nach dem Ausarbeiten der Logik muss Interactor bei Bedarf die Daten für die Vorbereitung an den Presenter übertragen, bevor sie im ViewController angezeigt werden. Interactor akzeptiert jedoch nur Anforderungen aus View als Eingabe, im Gegensatz zu VIPER, wo diese Anforderungen über Presenter gesendet werden.
Moderator
Der Präsentator verarbeitet die Daten zur Anzeige für den Benutzer. Das Ergebnis in diesem Fall ist das Eingabeprotokoll des ViewControllers. Hier können Sie beispielsweise das Textformat ändern, den Farbwert von enum in rgb übersetzen usw.
Arbeiter
Um Interactor nicht unnötig zu komplizieren und Details der Geschäftslogik nicht zu duplizieren, können Sie ein zusätzliches Worker-Element verwenden. In einfachen Modulen wird es nicht immer benötigt, aber in ausreichend geladenen Modulen können Sie einige Aufgaben aus Interactor entfernen. Beispielsweise kann die Logik der Interaktion mit der Datenbank im Worker erstellt werden, insbesondere wenn dieselben Datenbankabfragen in verschiedenen Modulen verwendet werden können.
Router
Der Router ist für die Übertragung von Daten zu anderen Modulen und die Übergänge zwischen diesen verantwortlich. Er hat eine Verbindung zum Controller, da Controller unter iOS leider unter anderem historisch für Übergänge verantwortlich sind. Die Verwendung von segue kann die Initialisierung von Übergängen vereinfachen, indem die Router-Methoden von Prepare for segue aufgerufen werden, da Router weiß, wie Daten übertragen werden, und dies ohne zusätzlichen Schleifencode von Interactor / Presenter. Die Datenübertragung erfolgt über die Data Warehouse-Protokolle jedes in Interactor implementierten Moduls. Diese Protokolle schränken auch den Zugriff auf interne Moduldaten vom Router aus ein.
Modelle
Modelle ist eine Beschreibung von Datenstrukturen zum Übertragen von Daten zwischen Modulen. Jede Implementierung der Geschäftslogikfunktion hat eine eigene Beschreibung der Modelle.
- Anfrage - um eine Anfrage vom Controller an den Interaktor zu senden.
- Antwort - Die Antwort des Interaktors auf die Übermittlung von Daten an den Präsentator.
- ViewModel - zur Datenübertragung in einer Form, die zur Anzeige in der Steuerung bereit ist.
Implementierungsbeispiel
Schauen wir uns diese Architektur anhand eines einfachen
Beispiels genauer an. Sie werden von der ContactsBook-Anwendung in einer vereinfachten, aber völlig ausreichenden Form bereitgestellt, um das Wesentliche der Architekturform zu verstehen. Die Anwendung enthält eine Liste von Kontakten sowie das Hinzufügen und Bearbeiten von Kontakten.
Ein Beispiel für ein Eingabeprotokoll:
protocol ContactListDisplayLogic: class { func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) }
Jeder Controller enthält einen Verweis auf ein Objekt, das das Interactor-Eingabeprotokoll implementiert
var interactor: ContactListBusinessLogic?
sowie zum Router-Objekt, das die Logik der Datenübertragung und des Modulwechsels implementieren soll:
var router: (NSObjectProtocol & ContactListRoutingLogic & ContactListDataPassing)?
Sie können die Modulkonfiguration in einer separaten privaten Methode implementieren:
private func setup() { let viewController = self let interactor = ContactListInteractor() let presenter = ContactListPresenter() let router = ContactListRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor }
oder erstellen Sie einen Configurator-Singleton, um diesen Code vom Controller zu entfernen (für diejenigen, die der Meinung sind, dass der Controller nicht an der Konfiguration beteiligt sein sollte) und sich nicht mit dem Zugriff auf Teile des Moduls im Controller zu verführen. Nach Ansicht von Onkel Bob und im klassischen VIPER gibt es keine Konfiguratorklasse. Die Verwendung des Konfigurators für das Modul zum Hinzufügen von Kontakten sieht folgendermaßen aus:
override func awakeFromNib() { super.awakeFromNib() AddContactConfigurator.sharedInstance.configure(self) }
Der Konfiguratorcode enthält die einzige Konfigurationsmethode, die mit der Setup-Methode in der Steuerung absolut identisch ist:
final class AddContactConfigurator { static let sharedInstance = AddContactConfigurator() private init() {} func configure(_ control: AddContactViewController) { let viewController = control let interactor = AddContactInteractor() let presenter = AddContactPresenter() let router = AddContactRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor } }
Ein weiterer sehr wichtiger Punkt bei der Implementierung des Controllers ist der Code in der Standardvorbereitungsmethode:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let scene = segue.identifier { let selector = NSSelectorFromString("routeTo\(scene)WithSegue:") if let router = router, router.responds(to: selector) { router.perform(selector, with: segue) } } }
Ein aufmerksamer Leser bemerkte höchstwahrscheinlich, dass der Router auch zur Implementierung von NSObjectProtocol erforderlich ist. Dies geschieht, damit wir die Standardmethoden dieses Protokolls für das Routing verwenden können, wenn Segues verwendet werden. Um diese einfache Umleitung zu unterstützen, sollte die Benennung der Segue-ID mit den Endungen der Routermethodennamen übereinstimmen. Um beispielsweise einen Kontakt anzuzeigen, gibt es einen Abschnitt, der an die Auswahl einer Zelle mit einem Kontakt gebunden ist. Die Kennung lautet "ViewContact". Hier ist die entsprechende Methode in Router:
func routeToViewContact(segue: UIStoryboardSegue?)
Die Anforderung, Daten für Interactor anzuzeigen, sieht ebenfalls sehr einfach aus:
private func fetchContacts() { let request = ContactList.ShowContacts.Request() interactor?.showContacts(request: request) }
Fahren wir mit Interactor fort. Interactor implementiert das ContactListDataStore-Protokoll, das für das Speichern / Zugreifen auf Daten verantwortlich ist. In unserem Fall ist dies nur eine Reihe von Kontakten, die nur durch die Getter-Methode begrenzt werden, um dem Router die Unzulässigkeit zu zeigen, ihn von anderen Modulen zu ändern. Ein Protokoll, das die Geschäftslogik für unsere Liste implementiert, lautet wie folgt:
func showContacts(request: ContactList.ShowContacts.Request) { let contacts = worker.getContacts() self.contacts = contacts let response = ContactList.ShowContacts.Response(contacts: contacts) presenter?.presentContacts(response: response) }
Es empfängt Kontaktdaten von ContactListWorker. In diesem Fall ist der Mitarbeiter dafür verantwortlich, wie die Daten heruntergeladen werden. Er kann sich an Dienste von Drittanbietern wenden, die beispielsweise entscheiden, Daten aus dem Cache zu entnehmen oder aus dem Netzwerk herunterzuladen. Nach dem Empfang der Daten sendet Interactor eine Antwort an den Präsentator, um die Anzeige vorzubereiten. Dieser Interactor enthält einen Link zum Präsentator:
var presenter: ContactListPresentationLogic?
Presenter implementiert nur ein Protokoll - ContactListPresentationLogic. In unserem Fall ändert es einfach zwangsweise den Fall des Vor- und Nachnamens des Kontakts, bildet das DisplayedContact-Präsentationsmodell aus dem Datenmodell und übergibt dieses zur Anzeige an den Controller:
func presentContacts(response: ContactList.ShowContacts.Response) { let mapped = response.contacts.map { ContactList .ShowContacts .ViewModel .DisplayedContact(firstName: $0.firstName.uppercaseFirst, lastName: $0.lastName.uppercaseFirst) } let viewModel = ContactList.ShowContacts.ViewModel(displayedContacts: mapped) viewController?.displayContacts(viewModel: viewModel) }
Danach endet der Zyklus und der Controller zeigt die Daten an, wobei die ContactListDisplayLogic-Protokollmethode implementiert wird:
func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) { displayedContacts = viewModel.displayedContacts tableView.reloadData() }
So sehen die Modelle zur Anzeige von Kontakten aus:
enum ShowContacts { struct Request { } struct Response { var contacts: [Contact] } struct ViewModel { struct DisplayedContact { let firstName: String let lastName: String var fullName: String { return firstName + " " + lastName } } var displayedContacts: [DisplayedContact] } }
In diesem Fall enthält die Anforderung keine Daten, da dies nur eine allgemeine Kontaktliste ist. Wenn beispielsweise der Listenbildschirm einen Filter enthalten würde, könnte der Filtertyp in diese Anforderung aufgenommen werden. Das Intrecator-Antwortmodell enthält die gewünschte Kontaktliste. ViewModel enthält auch ein Array von Daten, die zur Anzeige bereit sind - DisplayedContact.
Warum Swift reinigen?
Betrachten Sie die Vor- und Nachteile dieser Architektur. Erstens verfügt Clean Swift über Codevorlagen, die das Erstellen eines Moduls erleichtern. Diese Vorlagen können für viele Architekturen geschrieben werden, aber wenn sie sofort einsatzbereit sind, sparen Sie mindestens mehrere Stunden Zeit.
Zweitens ist diese Architektur wie VIPER gut getestet, Beispiele für Tests sind im Projekt verfügbar. Da das Modul, mit dem die Interaktion stattfindet, leicht durch einen Stub ersetzt werden kann, können Sie dies durch Festlegen der Funktionalität jedes Moduls mithilfe von Protokollen ohne Kopfschmerzen implementieren. Wenn wir gleichzeitig die Geschäftslogik und die entsprechenden Tests (Interactor, Interactor-Tests) erstellen, passt dies gut zum TDD-Prinzip. Aufgrund der Tatsache, dass die Ausgabe und Eingabe jedes Logikfalls durch das Protokoll definiert wird, reicht es aus, zuerst einen Test zu schreiben, der sein Verhalten bestimmt, und dann die Logik der Methode direkt zu implementieren.
Drittens implementiert Clean Swift (im Gegensatz zu VIPER) einen unidirektionalen Fluss der Datenverarbeitung und Entscheidungsfindung. Es wird immer nur ein Zyklus ausgeführt - Ansicht - Interaktor - Präsentator - Ansicht, was auch das Refactoring vereinfacht, da häufig weniger Entitäten geändert werden müssen. Aus diesem Grund können Projekte mit Logik, die sich häufig ändert oder ergänzt wird, mithilfe der Clean Swift-Methode bequemer umgestaltet werden. Mit Clean Swift trennen Sie Entitäten auf zwei Arten:
- Isolieren Sie Komponenten, indem Sie Eingabe- und Ausgabeprotokolle deklarieren
- Isolieren Sie Features, indem Sie Strukturen verwenden und Daten in separaten Anforderungen / Antworten / UI-Modellen kapseln. Jedes Feature hat seine eigene Logik und wird im Rahmen eines Prozesses gesteuert, ohne sich in einem Modul mit anderen Features zu überschneiden.
Clean Swift sollte nicht in kleinen Projekten ohne langfristige Perspektive oder in Prototypenprojekten verwendet werden. Beispielsweise ist es zu teuer, eine Anwendung für den Zeitplan einer Entwicklerkonferenz unter Verwendung dieser Architektur zu implementieren. Langfristige Projekte, Projekte mit viel Geschäftslogik, passen dagegen gut in den Rahmen dieser Architektur. Es ist sehr praktisch, Clean Swift zu verwenden, wenn das Projekt für zwei Plattformen implementiert ist - Mac OS und iOS, oder es ist geplant, es in Zukunft zu portieren.