Diviser pour régner. Application monolithique modulaire en Objective-C et Swift





Bonjour, Habr! Je m'appelle Vasily Kozlov, je suis responsable technique iOS au Delivery Club et j'ai trouvé le projet sous sa forme monolithique. J'avoue avoir participé à la lutte contre laquelle cet article est consacré, mais je me suis repenti et transformé ma conscience avec le projet.



Je veux vous dire comment j'ai divisé un projet existant dans Objective-C et Swift en modules séparés - cadres. Selon Apple , un framework est un répertoire d'une structure spécifique.



Au départ, nous nous sommes fixé un objectif: isoler le code qui implémente la fonction de chat pour le support utilisateur et réduire le temps de construction. Cela a conduit à des conséquences utiles qui sont difficiles à suivre sans habitude et existant dans le monde monolithique d'un projet.



Soudainement, les fameux principes SOLID ont commencĂ© Ă  prendre forme, et surtout, la formulation mĂȘme du problĂšme a obligĂ© Ă  organiser le code conformĂ©ment Ă  eux. En dĂ©plaçant une entitĂ© dans un module sĂ©parĂ©, vous rencontrez automatiquement toutes ses dĂ©pendances, qui ne devraient pas ĂȘtre dans ce module, et Ă©galement dupliquĂ©es dans le projet d'application principal. Par consĂ©quent, la question de l'organisation d'un module supplĂ©mentaire avec des fonctionnalitĂ©s communes est mĂ»re. N'est-ce pas le principe de la responsabilitĂ© unique, alors qu'une entitĂ© devrait avoir un seul but?



La complexitĂ© de diviser un projet avec deux langues et un hĂ©ritage important en modules peut effrayer au premier coup d'Ɠil, ce qui m'est arrivĂ©, mais l'intĂ©rĂȘt pour la nouvelle tĂąche a prĂ©valu.



Dans des articles dĂ©jĂ  trouvĂ©s, les auteurs ont promisun avenir sans nuages ​​avec des Ă©tapes simples et claires typiques d'un nouveau projet. Mais lorsque j'ai dĂ©placĂ© la premiĂšre classe de base vers le module pour le code gĂ©nĂ©ral, tant de dĂ©pendances non Ă©videntes sont apparues, tant de lignes de code Ă©taient couvertes en rouge dans Xcode que je ne voulais pas continuer.



Le projet contenait beaucoup de code hérité, des dépendances croisées sur les classes en Objective-C et Swift, différentes cibles en termes de développement iOS, une liste impressionnante de CocoaPods. Tout pas loin de ce monolithe a conduit au fait que le projet a cessé de se construire dans Xcode, trouvant parfois des erreurs aux endroits les plus inattendus.



J'ai donc décidé d'écrire la séquence des actions que j'ai entreprises pour faciliter la vie des propriétaires de tels projets.



Les premiers pas



Ils sont évidents, de nombreux articles ont été écrits à leur sujet . Apple a essayé de les rendre aussi conviviaux que possible.



1. CrĂ©ez le premier module: Fichier → Nouveau projet → Cocoa Touch Framework



2. Ajoutez le module Ă  l'espace de travail du projet











3. CrĂ©ez la dĂ©pendance du projet principal sur le module, en spĂ©cifiant ce dernier dans la section Binaires incorporĂ©s. S'il existe plusieurs cibles dans le projet, alors le module devra ĂȘtre inclus dans la section Binaires incorporĂ©s de chaque cible qui en dĂ©pend.



Je n'ajouterai qu'un seul commentaire de ma part: ne vous précipitez pas.



Savez-vous ce qui sera placĂ© dans ce module, sur quelle base les modules seront-ils rĂ©partis? Dans ma version, cela aurait dĂ» ĂȘtreUIViewControllerpour discuter avec le tableau et les cellules. Les cocoapods avec un chat doivent ĂȘtre attachĂ©s au module. Mais il s'est avĂ©rĂ© un peu diffĂ©rent. J'ai dĂ» reporter la mise en Ɠuvre du chat, car UIViewControllerson prĂ©sentateur et mĂȘme la cellule Ă©taient basĂ©s sur des classes de base et des protocoles dont le nouveau module ne savait rien.



Comment mettre en Ă©vidence un module? L'approche la plus logique - sur "ficham" ( fonctionnalitĂ©s ), c'est-Ă -dire pour certaines tĂąches utilisateur. Par exemple, chattez avec le support technique, Ă©crans d'inscription / de connexion, feuille du bas avec les paramĂštres de l'Ă©cran principal. De plus, vous aurez trĂšs probablement besoin d'une sorte de fonctionnalitĂ© de base, qui n'est pas une fonctionnalitĂ©, mais seulement un ensemble d'Ă©lĂ©ments d'interface utilisateur, de classes de base, etc. Cette fonctionnalitĂ© doit ĂȘtre dĂ©placĂ©e dans un module commun similaire au cĂ©lĂšbre fichier Utils... N'ayez pas peur de diviser ce module aussi. Plus les cubes sont petits, plus il est facile de les insĂ©rer dans le bĂątiment principal. Il me semble que c'est ainsi que l'on peut formuler encore un des principes SOLID .



Il y a des conseils prĂȘts Ă  l'emploi pour diviser en modules, que je n'ai pas utilisĂ©s, c'est pourquoi j'ai cassĂ© autant de copies, et j'ai mĂȘme dĂ©cidĂ© de parler du douloureux. Cependant, cette approche - d'abord agir, puis rĂ©flĂ©chir - vient de m'ouvrir les yeux sur l'horreur du code dĂ©pendant dans un projet monolithique. Lorsque vous ĂȘtes au dĂ©but du voyage, vous avez du mal Ă  saisir toute la quantitĂ© de changements qui seront nĂ©cessaires pour Ă©liminer les dĂ©pendances.



Alors dĂ©placez simplement la classe d'un module Ă  un autre, voyez ce qui rougit dans Xcode et essayez de comprendre les dĂ©pendances. Xcode 10 est dĂ©licat: lorsque vous dĂ©placez des liens vers des fichiers d'un module Ă  un autre, il laisse les fichiers au mĂȘme endroit. Donc, la prochaine Ă©tape sera comme ça ...



4. DĂ©placez les fichiers dans le gestionnaire de fichiers, supprimez les anciens liens dans Xcode et rajoutez des fichiers dans le nouveau module. Faire cette classe Ă  la fois permettra de ne pas s'emmĂȘler dans les dĂ©pendances.



Pour rendre toutes les entités détachées disponibles depuis l'extérieur du module, vous devez prendre en compte les particularités de Swift et Objective-C.



5. Dans Swift, toutes les classes, Ă©numĂ©rations et protocoles doivent ĂȘtre marquĂ©s d'un modificateur d'accĂšspublicalors ils sont accessibles depuis l'extĂ©rieur du module. Si une classe de base est dĂ©placĂ©e vers un framework sĂ©parĂ©, elle doit ĂȘtre marquĂ©e avec un modificateur open, sinon cela ne fonctionnera pas pour crĂ©er une classe descendante Ă  partir de celle-ci.



Vous devez immédiatement vous rappeler (ou apprendre pour la premiÚre fois) quels sont les niveaux d'accÚs dans Swift et réaliser un profit!







Lors de la modification du niveau d'accĂšs pour la classe portĂ©e, Xcode vous demandera de modifier le niveau d'accĂšs de toutes les mĂ©thodes remplacĂ©es au mĂȘme niveau.







Ensuite, vous devez ajouter l'importation du nouveau cadre dans le fichier Swift, oĂč la fonctionnalitĂ© sĂ©lectionnĂ©e est utilisĂ©e, avec un UIKit. AprĂšs cela, il devrait y avoir moins d'erreurs dans Xcode.



import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {
//..
}


Avec Objective-C, la sĂ©quence est un peu plus compliquĂ©e. De plus, l'utilisation d'un en-tĂȘte de pontage pour importer des classes Objective-C dans Swift n'est pas prise en charge dans les frameworks.







Par consĂ©quent, le champ d'en-tĂȘte de pont Objective-C doit ĂȘtre vide dans les paramĂštres de l'infrastructure.







Il existe un moyen de sortir de cette situation, et pourquoi il en est ainsi fait l'objet d'une Ă©tude distincte.



6. Chaque framework a son propre fichier d'en- tĂȘte parapluie , Ă  travers lequel toutes les interfaces publiques Objective-C examineront le monde extĂ©rieur.



Si vous spĂ©cifiez l'importation de tous les autres fichiers d'en-tĂȘte dans cet en-tĂȘte parapluie, ils seront disponibles dans Swift.







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.
    }


En Objective-C, pour accĂ©der aux classes en dehors d'un module, il faut jouer avec ses paramĂštres: rendre publics les fichiers d'en-tĂȘte.







7. Lorsque tous les fichiers ont Ă©tĂ© transfĂ©rĂ©s un par un vers un module sĂ©parĂ©, n'oubliez pas les Cocoapods. Le Podfile doit ĂȘtre rĂ©organisĂ© si certaines fonctionnalitĂ©s se retrouvent dans un cadre distinct. C'Ă©tait comme ça pour moi: le pod avec des indicateurs graphiques a dĂ» ĂȘtre introduit dans le cadre gĂ©nĂ©ral, et le chat - le nouveau pod - a Ă©tĂ© inclus dans son propre cadre sĂ©parĂ©.



Il est nécessaire d'indiquer explicitement que le projet n'est plus simplement un projet, mais un espace de travail avec des sous-projets:



workspace 'myFrameworkTest'


Les dĂ©pendances communes aux frameworks doivent ĂȘtre dĂ©placĂ©es dans des variables sĂ©parĂ©es, par exemple, networkPodset uiPods:



def networkPods
     pod 'Alamofire'
end



 def uiPods
     pod 'GoogleMaps'
 end


Ensuite, les dépendances du projet principal seront décrites comme suit:



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


Les dépendances du framework avec chat - de cette façon:



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


Roches sous-marines



Probablement, cela pourrait ĂȘtre terminĂ©, mais j'ai dĂ©couvert plus tard plusieurs problĂšmes implicites, que je veux Ă©galement mentionner.



Toutes les dépendances courantes sont déplacées vers un framework séparé, chat - vers un autre, le code est devenu un peu plus propre, le projet est construit, mais il plante au démarrage.



Le premier problÚme était dans l'implémentation du chat. Dans l'immensité du réseau, le problÚme survient dans d'autres pods, il suffit de google " BibliothÚque non chargée: Raison: image non trouvée ". C'est avec ce message que la chute a eu lieu.



Je n'ai pas pu trouver de solution plus élégante et j'ai été obligé de dupliquer la connexion du pod avec le chat dans l'application principale:



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


Ainsi, Cocoapods permet à l'application de voir la bibliothÚque liée dynamiquement au démarrage et lorsque le projet est compilé.



Un autre problÚme était les ressources, que j'avais sûrement oubliées et que je n'avais jamais vu aucune mention de cet aspect à garder à l'esprit. L'application a planté lors de la tentative d'enregistrement du fichier xib de cellule: "Impossible de charger NIB dans le bundle" .



Le constructeur de init(nibName:bundle:)classe UINibpar défaut recherche une ressource dans le module d'application principal. Naturellement, vous ne savez rien à ce sujet lorsque le développement est effectué dans un projet monolithique.



La solution est de spĂ©cifier le bundle dans lequel la classe de ressources est dĂ©finie, ou de laisser le compilateur le faire lui-mĂȘme en utilisant le constructeur de init(for:)classeBundle... Et, bien sĂ»r, n'oubliez pas dĂ©sormais que les ressources peuvent dĂ©sormais ĂȘtre communes Ă  tous les modules ou spĂ©cifiques Ă  un module.



Si le module utilise xibs, alors Xcode proposera, comme d'habitude, des boutons et UIImageViewsĂ©lectionnera des ressources graphiques dans l'ensemble du projet, mais au moment de l'exĂ©cution, toutes les ressources situĂ©es dans d'autres modules ne seront pas chargĂ©es. J'ai chargĂ© des images dans le code en utilisant le constructeur de la init(named:in:compatibleWith:)classe UIImage, oĂč le deuxiĂšme paramĂštre Bundleest l'emplacement du fichier image.



Les cellules dans UITableViewet UICollectionViewdoivent maintenant Ă©galement s'inscrire de la mĂȘme maniĂšre. Et nous devons nous rappeler que les classes Swift dans la reprĂ©sentation sous forme de chaĂźne incluent Ă©galement le nom du module et que la mĂ©thode NSClassFromString()d'Objective-C renvoienil, donc je recommande d'enregistrer les cellules en spĂ©cifiant non pas une chaĂźne, mais une classe. Pour UITableViewvous pouvez utiliser la mĂ©thode d'assistance suivante:



@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)
    }
}


conclusions



Vous n'avez plus à vous inquiéter si une demande d'extraction contient des modifications dans la structure du projet effectuées dans différents modules, car chaque module a son propre fichier xcodeproj. Vous pouvez distribuer le travail afin de ne pas avoir à passer plusieurs heures à assembler le fichier de projet. Il est utile d'avoir une architecture modulaire dans des équipes importantes et réparties. En conséquence, la vitesse de développement devrait augmenter, mais l'inverse est également vrai. J'ai passé beaucoup plus de temps sur mon tout premier module que si je créerais un chat à l'intérieur d'un monolithe.



Parmi les avantages Ă©vidents qu'Apple souligne Ă©galement, - la possibilitĂ© de rĂ©utiliser le code. Si l'application a des cibles diffĂ©rentes (extensions d'application), il s'agit de l'approche la plus accessible. Le chat n'est peut-ĂȘtre pas le meilleur exemple. J'aurais dĂ» commencer par dĂ©finir la couche rĂ©seau, mais soyons honnĂȘtes avec nous-mĂȘmes, c'est une route trĂšs longue et dangereuse qu'il vaut mieux dĂ©composer en petites sections. Et comme au cours des deux derniĂšres annĂ©es il s'agissait de l'introduction d'un deuxiĂšme service d'organisation du support technique, j'ai voulu l'implĂ©menter sans l'introduire. OĂč sont les garanties que le troisiĂšme n'apparaĂźtra pas bientĂŽt?



Un effet non Ă©vident lors du dĂ©veloppement d'un module est des interfaces plus rĂ©flĂ©chies et plus propres. Le dĂ©veloppeur doit concevoir les classes de maniĂšre Ă  ce que certaines propriĂ©tĂ©s et mĂ©thodes soient accessibles de l'extĂ©rieur. InĂ©vitablement, vous devez rĂ©flĂ©chir Ă  ce qu'il faut cacher et Ă  la maniĂšre de crĂ©er le module pour qu'il puisse ĂȘtre facilement rĂ©utilisĂ©.



All Articles