Teile und herrsche. Modulare Monolithanwendung in Objective-C und Swift





Hallo Habr! Mein Name ist Vasily Kozlov, ich bin ein technischer Leiter für iOS im Delivery Club und ich habe das Projekt in seiner monolithischen Form gefunden. Ich gebe zu, dass ich an dem beteiligt war, gegen den dieser Artikel kämpfen wird, aber ich habe Buße getan und mein Bewusstsein zusammen mit dem Projekt verändert.



Ich möchte Ihnen sagen, wie ich ein vorhandenes Projekt in Objective-C und Swift in separate Module - Frameworks - aufgeteilt habe. Ein Framework ist laut Apple ein Verzeichnis einer bestimmten Struktur.



Zunächst haben wir uns ein Ziel gesetzt: den Code zu isolieren, der die Chat-Funktion für die Benutzerunterstützung implementiert, und die Erstellungszeit zu verkürzen. Dies führte zu nützlichen Konsequenzen, die ohne Gewohnheit schwer zu verfolgen sind und in der monolithischen Welt eines Projekts existieren.



Plötzlich nahmen die berüchtigten SOLID- Prinzipien Gestalt an, und vor allem die Formulierung des Problems zwang dazu, den Code entsprechend zu organisieren. Wenn Sie eine Entität in ein separates Modul verschieben, stoßen Sie automatisch auf alle Abhängigkeiten, die nicht in diesem Modul enthalten sein sollten, und werden auch im Hauptanwendungsprojekt dupliziert. Daher ist die Frage der Organisation eines zusätzlichen Moduls mit gemeinsamer Funktionalität reif. Ist dies nicht das Prinzip der Einzelverantwortung, wenn ein Unternehmen einen Zweck haben sollte?



Die Komplexität der Aufteilung eines Projekts in zwei Sprachen und ein großes Erbe in Module kann auf den ersten Blick abschrecken, was mir passiert ist, aber das Interesse an der neuen Aufgabe hat sich durchgesetzt.



In zuvor gefundenen Artikeln versprachen die AutorenEine wolkenlose Zukunft mit einfachen und klaren Schritten, die typisch für ein neues Projekt sind. Aber als ich die erste Basisklasse für allgemeinen Code in das Modul verschoben habe, wurden so viele nicht offensichtliche Abhängigkeiten zutage gefördert, dass so viele Codezeilen in Xcode rot bedeckt waren, dass ich nicht fortfahren wollte.



Das Projekt enthielt viel Legacy-Code, gegenseitige Abhängigkeiten von Klassen in Objective-C und Swift, verschiedene Ziele in Bezug auf die iOS-Entwicklung und eine beeindruckende Liste von CocoaPods. Jeder Schritt weg von diesem Monolithen führte dazu, dass das Projekt nicht mehr in Xcode erstellt wurde und manchmal an den unerwartetsten Stellen Fehler auftraten.



Aus diesem Grund habe ich beschlossen, die Abfolge der Maßnahmen aufzuschreiben, die ich ergriffen habe, um den Eigentümern solcher Projekte das Leben zu erleichtern.



Die ersten Schritte



Sie sind offensichtlich, viele Artikel wurden über sie geschrieben . Apple hat versucht, sie so benutzerfreundlich wie möglich zu gestalten.



1. Erstellen Sie das erste Modul: Datei → Neues Projekt → Cocoa Touch Framework.



2. Fügen Sie das Modul zum Projektarbeitsbereich hinzu.











3. Erstellen Sie die Abhängigkeit des Hauptprojekts vom Modul, und geben Sie letzteres im Abschnitt Eingebettete Binärdateien an. Wenn das Projekt mehrere Ziele enthält, muss das Modul in den Abschnitt "Eingebettete Binärdateien" jedes Ziels aufgenommen werden, das davon abhängt.



Ich werde nur einen Kommentar von mir hinzufügen: Eile nicht.



Wissen Sie, was in diesem Modul platziert wird, auf welcher Basis werden die Module aufgeteilt? In meiner Version hätte es sein sollenUIViewControllerzum Chatten mit Tabellen und Zellen. Cocoapods mit einem Chat sollten an das Modul angehängt werden. Aber es stellte sich etwas anders heraus. Ich musste die Implementierung des Chats verschieben, da UIViewControllersowohl der Moderator als auch die Zelle auf Basisklassen und Protokollen basierten, von denen das neue Modul nichts wusste.



Wie hebe ich ein Modul hervor? Der logischste Ansatz - für "ficham" ( Funktionen ), dh für einige Benutzeraufgaben. Chatten Sie beispielsweise mit dem technischen Support, den Registrierungs- / Anmeldebildschirmen und dem unteren Blatt mit den Einstellungen des Hauptbildschirms. Darüber hinaus benötigen Sie höchstwahrscheinlich eine grundlegende Funktionalität, bei der es sich nicht um eine Funktion handelt, sondern nur um eine Reihe von UI-Elementen, Basisklassen usw. Diese Funktionalität sollte in ein gemeinsames Modul verschoben werden, das der berühmten Utils- Datei ähnelt... Haben Sie keine Angst, dieses Modul auch zu teilen. Je kleiner die Würfel sind, desto einfacher ist es, sie in das Hauptgebäude einzubauen. Es scheint mir, dass so eines der SOLID- Prinzipien formuliert werden kann .



Es gibt vorgefertigte Tipps für die Aufteilung in Module, die ich nicht verwendet habe. Deshalb habe ich so viele Exemplare zerbrochen und sogar beschlossen, über das schmerzhafte zu sprechen. Dieser Ansatz - zuerst zu handeln, dann zu denken - öffnete mir jedoch nur die Augen für den Schrecken des abhängigen Codes in einem monolithischen Projekt. Wenn Sie sich am Anfang der Reise befinden, fällt es Ihnen schwer, die gesamte Menge an Änderungen zu erfassen, die erforderlich sind, um Abhängigkeiten zu beseitigen.



Verschieben Sie die Klasse einfach von einem Modul in ein anderes, sehen Sie, was in Xcode errötet ist, und versuchen Sie, die Abhängigkeiten herauszufinden. Xcode 10 ist schwierig: Wenn Sie Links zu Dateien von einem Modul in ein anderes verschieben, bleiben die Dateien an derselben Stelle. Daher wird der nächste Schritt folgendermaßen aussehen:



4. Verschieben Sie Dateien im Dateimanager, löschen Sie alte Links in Xcode und fügen Sie dem neuen Modul erneut Dateien hinzu. Wenn Sie diese eine Klasse nach der anderen machen, ist es einfacher, nicht in Abhängigkeiten verwirrt zu werden.



Um alle getrennten Entitäten von außerhalb des Moduls verfügbar zu machen, müssen Sie die Besonderheiten von Swift und Objective-C berücksichtigen.



5. In Swift müssen alle Klassen, Aufzählungen und Protokolle mit einem Zugriffsmodifikator gekennzeichnet seinpublicdann kann von außerhalb des Moduls auf sie zugegriffen werden. Wenn eine Basisklasse in ein separates Framework verschoben wird, sollte sie mit einem Modifikator markiert werden open, da sonst keine nachfolgende Klasse daraus erstellt werden kann.



Sie sollten sich sofort daran erinnern (oder zum ersten Mal erfahren), welche Zugriffsebenen in Swift vorhanden sind, und einen Gewinn erzielen!







Wenn Sie die Zugriffsebene für die portierte Klasse ändern, müssen Sie für Xcode die Zugriffsebene für alle überschriebenen Methoden auf dieselbe ändern.







Anschließend müssen Sie den Import des neuen Frameworks zur Swift-Datei hinzufügen, in der die ausgewählte Funktionalität zusammen mit etwas UIKit verwendet wird. Danach sollte es weniger Fehler in Xcode geben.



import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {
//..
}


Mit Objective-C ist die Sequenz etwas komplizierter. Die Verwendung eines Bridging-Headers zum Importieren von Objective-C-Klassen in Swift wird in Frameworks nicht unterstützt.







Daher muss das Feld Objective-C Bridging Header in den Framework-Einstellungen leer sein.







Es gibt einen Ausweg aus dieser Situation, und warum dies so ist, ist ein Thema für eine separate Studie.



6. Jedes Framework verfügt über eine eigene Umbrella-Header- Datei , über die alle öffentlichen Objective-C-Schnittstellen in die Außenwelt blicken.



Wenn Sie den Import aller anderen Header-Dateien in diesem Umbrella-Header angeben, sind diese in Swift verfügbar.







import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {    
    var vc: Obj2ViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }


In Objective-C müssen Sie mit den Einstellungen herumspielen, um auf Klassen außerhalb eines Moduls zuzugreifen: Machen Sie die Header-Dateien öffentlich.







7. Wenn alle Dateien einzeln in ein separates Modul übertragen wurden, vergessen Sie Cocoapods nicht. Das Podfile muss neu organisiert werden, wenn einige Funktionen in einem separaten Framework gespeichert werden. So war es für mich: Der Pod mit grafischen Indikatoren musste in den allgemeinen Rahmen gebracht werden, und der Chat - der neue Pod - wurde in einen eigenen Rahmen aufgenommen.



Es muss explizit angegeben werden, dass das Projekt jetzt nicht nur ein Projekt ist, sondern ein Arbeitsbereich mit Teilprojekten:



workspace 'myFrameworkTest'


Für Frameworks übliche Abhängigkeiten sollten beispielsweise in separate Variablen verschoben werden networkPodsund uiPods:



def networkPods
     pod 'Alamofire'
end



 def uiPods
     pod 'GoogleMaps'
 end


Anschließend werden die Abhängigkeiten des Hauptprojekts wie folgt beschrieben:



target 'myFrameworkTest' do
project 'myFrameworkTest'
    networkPods
    uiPods
    target 'myFrameworkTestTests' do
    end
end 


Die Framework-Abhängigkeiten mit Chat - so:



target 'FeatureOne' do
    project 'FeatureOne/FeatureOne'
    uiPods
    pod 'ChatThatMustNotBeNamed'
end


Unterwasserfelsen



Wahrscheinlich könnte dies beendet werden, aber später entdeckte ich einige implizite Probleme, die ich auch erwähnen möchte.



Alle gängigen Abhängigkeiten werden in ein separates Framework verschoben, Chat - in ein anderes, der Code ist etwas sauberer geworden, das Projekt wird erstellt, aber es stürzt beim Start ab.



Das erste Problem war in der Chat-Implementierung. In der Weite des Netzwerks tritt das Problem in anderen Pods auf. Google einfach " Bibliothek nicht geladen: Grund: Bild nicht gefunden ". Mit dieser Botschaft fand der Fall statt.



Ich konnte keine elegantere Lösung finden und musste die Pod-Verbindung mit Chat in der Hauptanwendung duplizieren:



target 'myFrameworkTest' do
    project 'myFrameworkTest'
    pod 'ChatThatMustNotBeNamed'
    networkPods
    uiPods
    target 'myFrameworkTestTests' do
    end
end


Mit Cocoapods kann die Anwendung die dynamisch verknüpfte Bibliothek beim Start und beim Kompilieren des Projekts anzeigen.



Ein weiteres Problem waren die Ressourcen, die ich sicher vergessen und nie erwähnt hatte, um diesen Aspekt zu berücksichtigen. Die Anwendung stürzte beim Versuch ab, die xib-Datei der Zelle zu registrieren: "NIB konnte nicht im Bundle geladen werden" .



Der Standard - init(nibName:bundle:)Klasse Konstruktor UINibsucht nach einer Ressource in der Hauptanwendungsmodul. Natürlich wissen Sie nichts darüber, wenn die Entwicklung in einem monolithischen Projekt durchgeführt wird.



Die Lösung besteht darin, das Bundle anzugeben, in dem die Ressourcenklasse definiert ist, oder den Compiler dies mithilfe des init(for:)Klassenkonstruktors selbst tun zu lassenBundle... Und vergessen Sie natürlich nicht, dass Ressourcen von nun an allen Modulen gemeinsam oder spezifisch für ein Modul sein können.



Wenn das Modul xibs verwendet, bietet Xcode wie gewohnt Schaltflächen an und UIImageViewwählt grafische Ressourcen aus dem gesamten Projekt aus. Zur Laufzeit werden jedoch nicht alle Ressourcen in anderen Modulen geladen. Ich habe Bilder mit dem Konstruktor der init(named:in:compatibleWith:)Klasse in Code geladen UIImage, wobei sich der zweite Parameter dort Bundlebefindet, wo sich die Bilddatei befindet.



Zellen in UITableViewund UICollectionViewjetzt müssen sich ebenfalls auf ähnliche Weise registrieren. Und wir müssen uns daran erinnern, dass Swift-Klassen in der Zeichenfolgendarstellung auch den Namen des Moduls enthalten und eine Methode NSClassFromString()aus Objective-C zurückgibtnilDaher empfehle ich, Zellen zu registrieren, indem Sie keine Zeichenfolge, sondern eine Klasse angeben. Für können UITableViewSie die folgende Hilfsmethode verwenden:



@objc public extension UITableView {

    func registerClass(_ classType: AnyClass) {
        let bundle = Bundle(for: classType)
        let name = String(describing: classType)
        register(UINib(nibName: name, bundle: bundle), forCellReuseIdentifier: name)
    }
}


Schlussfolgerungen



Jetzt müssen Sie sich keine Sorgen mehr machen, wenn eine Pull-Anforderung Änderungen in der Projektstruktur enthält, die in verschiedenen Modulen vorgenommen wurden, da jedes Modul über eine eigene xcodeproj-Datei verfügt. Sie können die Arbeit so verteilen, dass Sie nicht mehrere Stunden damit verbringen müssen, die Projektdatei zusammenzustellen. Es ist nützlich, eine modulare Architektur in großen und verteilten Teams zu haben. Infolgedessen sollte sich die Entwicklungsgeschwindigkeit erhöhen, aber das Gegenteil ist auch der Fall. Ich habe viel mehr Zeit mit meinem ersten Modul verbracht als mit einem Chat in einem Monolithen.



Von den offensichtlichen Pluspunkten, auf die Apple ebenfalls hinweist, - die Fähigkeit, den Code wiederzuverwenden. Wenn die Anwendung unterschiedliche Ziele hat (App-Erweiterungen), ist dies der am besten zugängliche Ansatz. Vielleicht ist Chat nicht das beste Beispiel. Ich hätte mit der Gestaltung der Netzwerkschicht beginnen sollen, aber seien wir ehrlich zu uns selbst. Dies ist eine sehr lange und gefährliche Straße, die am besten in kleine Abschnitte unterteilt wird. Und da in den letzten Jahren ein zweiter Dienst zur Organisation des technischen Supports eingeführt wurde, wollte ich ihn implementieren, ohne ihn einzuführen. Wo sind die Garantien, dass der dritte nicht bald erscheint?



Ein nicht offensichtlicher Effekt bei der Entwicklung eines Moduls sind durchdachtere, sauberere Schnittstellen. Der Entwickler muss die Klassen so gestalten, dass bestimmte Eigenschaften und Methoden von außen zugänglich sind. Unweigerlich müssen Sie darüber nachdenken, was Sie verbergen und wie Sie das Modul herstellen, damit es problemlos wieder verwendet werden kann.



All Articles