Modulare Entwicklung oder Weg dorthin, nicht zurück


Wie wir zu einem neuen Ansatz für die Arbeit mit Modulen in der iOS-Anwendung RaiffeisenBank kamen.

Das Problem


In Raiffeisenbank-Anwendungen besteht jeder Bildschirm aus mehreren Modulen, die so unabhängig wie möglich voneinander sind. "Modul" nennen wir eine visuelle Komponente, die ihre eigene Idee hat. Beim Entwerfen einer Anwendung ist es sehr wichtig, Logik zu schreiben, damit die Module unabhängig sind und einfach hinzugefügt oder entfernt werden können, ohne auf Refactoring zurückgreifen zu müssen.

Welche Schwierigkeiten hatten wir:


Hervorheben der Abstraktion über Architekturmustern
Bereits in der ersten Entwicklungsphase wurde klar, dass wir nicht an ein bestimmtes Architekturmuster gebunden sein wollten. MVC ist gut, wenn Sie eine Seite mit einigen Informationen anzeigen müssen. Gleichzeitig ist die Interaktion mit dem Benutzer minimal oder überhaupt nicht. Zum Beispiel: die Seite "Über das Unternehmen" oder "Benutzervereinbarung". VIPER ist ein gutes Werkzeug für komplexe Module, die ihre eigene Logik für die Arbeit mit Diensten, Routing und vielem von allem haben.

Das Problem der Interaktion und Einkapselung
Jedes Architekturmuster hat eine eigene Konstruktionsstruktur und eigene Protokolle, die die Arbeit mit dem Modul einschränken. Um das Modul zu abstrahieren, müssen Sie die wichtigsten Interaktionsschnittstellen für Eingabe / Ausgabe hervorheben.

Hervorheben der Routing-Logik
Ein Modul als visuelle Einheit sollte und kann nicht wissen, wo und wie es gezeigt wird. Ein und dasselbe Modul sollte und kann als eigenständige Einheit auf jedem Bildschirm oder als Komposition implementiert werden. Die Verantwortung dafür kann nicht dem Modul selbst angelastet werden.

Vorherige Lösung: // Schlechtes Geschäft


Die erste Lösung, die wir in Objective-C geschrieben haben und die auf NSProxy basiert. Das Problem der Kapselung des Architekturmusters wurde durch Defenition gelöst, die durch die gegebenen Bedingungen, dh die Eingabe / Ausgabe des Moduls, bestimmt wurde, wodurch es möglich wurde, alle Aufrufe des Moduls an seine Eingabe zu übertragen und Nachrichten über die Ausgabe zu empfangen, falls vorhanden.

Es war ein Schritt nach vorne, aber es traten neue Schwierigkeiten auf:

  • Die Proxy- Schnittstelle garantierte nicht die Implementierung des Eingabeprotokolls .
  • Die Ausgabe musste beschrieben werden, auch wenn sie nicht benötigt wurde.
  • Es war notwendig, die Ausgabeeigenschaft zur Eingabeschnittstelle hinzuzufügen.

Zusätzlich zu NSProxy haben wir das Routing implementiert, indem wir uns die Idee von ViperMcFlurry angesehen haben: Wir haben eine Kategorie in ViewController erstellt , die mit dem Erscheinen verschiedener Optionen für die Anzeige des Moduls auf dem Bildschirm zu wachsen begann. Natürlich haben wir die Kategorie unterteilt, aber es war noch lange keine gute Lösung.

Im Allgemeinen ... der erste Pfannkuchen ist klumpig, es wurde klar, dass Sie das Problem anders lösen müssen.

Lösung: // Final


Als wir merkten , dass es mit NSProxy nichts weiter gab , nahmen wir Marker und gingen zum Zeichnen. Als Ergebnis haben wir das RFModule- Protokoll isoliert:

@objc protocol RFModule { var view: ViewController { get } var input: AnyObject? { get } var output: AnyObject? { get set } var transition: Transitioning { get set } } 

Wir haben die zugehörigen Typen auf Protokollebene absichtlich aufgegeben, und es gab einen guten Grund dafür: Zu diesem Zeitpunkt befanden sich 90% des Codes in Objective-C. Interoperabilität zwischen ObjC-Modulen ← → Swift wäre nicht möglich.

Um weiterhin Generika zu verwenden und die typisierte Verwendung von Modulen sicherzustellen, haben wir die Modulklasse eingeführt, die das Protokoll erfüllt
RFModule :

 final class Module<I: Any, O: Any>: RFModule { public typealias Input = I public typealias Output = O public var setOutput: ((O?) -> Void)? //... public var input: I? { get { return inputObjc as? I} set { inputObjc = newValue as AnyObject } } public var output: O? { get { return outputObjc as? O} set { outputObjc = newValue as AnyObject } } @objc(input) public weak var inputObjc: AnyObject? @objc(moduleOutput) public weak var outputObjc: AnyObject? { didSet{ setOutput?(output) } } } @objc protocol RFModule { var view: ViewController { get } @objc(input) var inputObjc: AnyObject? { get } @objc(moduleOutput) var outputObjc: AnyObject? { get set } var transition: Transitioning { get set } } public extension RFModule { public var input: AnyObject? { return inputObjc } public var output: AnyObject? { get { return outputObjc } set { outputObjc = newValue} } } 

Also haben wir ein getipptes Modul. Tatsächlich verwendet Swift die Klasse Module und das Objective-C RFModule . Darüber hinaus erwies es sich als praktisches Werkzeug zum Mischen von Typen an der Stelle, an der Sie Arrays erstellen müssen: zum Beispiel TabContainer .

Da sich die Modulerstellung DI im UserStory-Bereich befindet und der Wert der Ausgabe an der Stelle zugewiesen wird, an der sie verwendet wird, ist es unmöglich, einen einfachen Seter zu beschreiben. "SetOutput" ist im Wesentlichen eine Verteidigung, die in der Phase der Zuweisung der Ausgabe abhängig von der Logik des Moduls an die verantwortliche Person weitergegeben wird.

 class SomeViewController: UIViewController, ModuleInput { weak var delegate: ModuleOutput } class Assembly { func someModule() -> Module<ModuleInput, ModuleOutput> { let view = SomeViewController() let module = Module<ModuleInput, ModuleOutput>(view: view, input: view) { [weak view] output in view?.delegate = output } return module } } ... let assembly: Assembly let module = assembly.someModule() module.output = self 

Transitioning ist ein Protokoll, dessen Implementierungen, wie der Name schon sagt, für die Logik des Ein- und Ausblendens des Moduls verantwortlich sind.

 protocol Transitioning { var destination: ViewController? { get } // should be weak func perform(_ completion: (()->())?) // present func reverse(_ completion: (()->())?) // dissmiss } 

Zur Anzeige wird es verursacht - ausführen , zum Ausblenden - umkehren . Trotz der Tatsache, dass das Protokoll ein Ziel enthält und es zunächst scheint, dass es eine Quelle geben sollte. Tatsächlich ist die Quelle möglicherweise nicht vorhanden, und ihr Typ ist nicht immer ViewController . Wenn das Modul beispielsweise in einem neuen Fenster geöffnet werden soll, ist dies Fenster . Wenn wir einbetten müssen , benötigen wir AND parent: ViewController AND container: UIView .

 class PresentTransition: Transitioning { weak var source: ViewController? weak var destination: ViewController? ... func perform(_ completion: (()->())?) { source.present(viewController: self.destinaton) } } 

So haben wir die Idee, Erweiterungen auf dem ViewController zu schreiben, losgeworden und die Logik beschrieben, wie wir unsere Module in verschiedenen Objekten anzeigen. Dies gab uns Flexibilität beim Routing, d.h. Jetzt können wir jedes Modul sowohl unabhängig als auch in einem Komplex anzeigen und auch variieren, wie alles auf dem Bildschirm angezeigt wird: im Fenster (Fenster), Präsentieren, in der Navigation (Push-to-Navigation), eingebettet in den Vorhang (Cover) .

Das ist alles?


Es gibt noch eine Sache, die bisher verfolgt. Für die Möglichkeit, die Art und Weise, wie das Modul angezeigt wird, einfach auszuwählen und diese Logik daraus zu entfernen, haben wir den Verlust der Fähigkeit zum Festlegen der Darstellungseigenschaften bezahlt. Wenn wir es beispielsweise in der Navigation anzeigen , müssen wir angeben, welche Farbe barTintColor haben soll. Wenn wir das Modul im Vorhang zeigen, muss die Farbe des Handlers eingestellt werden .

Bisher haben wir dieses Problem mit dem untypisierten Erscheinungsbild gelöst: Jede Eigenschaft und jeder Übergang beim Öffnen des Moduls führt zu dem Typ, mit dem es funktioniert, und wenn dies erfolgreich war, werden die erforderlichen Eigenschaften entfernt.

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


All Articles