I want to understand basic, abstract and correct architectural approach for networking applications in iOS
: il n'y a pas d' approche "la meilleure" ou "la plus correcte" pour construire une architecture d'application. C'est un travail très créatif. Vous devez toujours choisir l'architecture la plus simple et la plus extensible, qui sera claire pour tout développeur, qui commence à travailler sur votre projet ou pour d'autres développeurs de votre équipe, mais je suis d'accord, qu'il peut y avoir un "bon" et un "mauvais" " architecture.
Vous avez dit:, collect the most interesting approaches from experienced iOS developers
je ne pense pas que mon approche soit la plus intéressante ou la plus correcte, mais je l'ai utilisée dans plusieurs projets et j'en suis satisfaite. Il s'agit d'une approche hybride de celles que vous avez mentionnées ci-dessus, ainsi que des améliorations de mes propres efforts de recherche. Je m'intéresse aux problèmes de construction d'approches, qui combinent plusieurs modèles et idiomes bien connus. Je pense que beaucoup de modèles d'entreprise de Fowler peuvent être appliqués avec succès aux applications mobiles. Voici une liste des plus intéressantes, que nous pouvons appliquer pour créer une architecture d'application iOS ( à mon avis ): couche de service , unité de travail , façade distante , objet de transfert de données ,Passerelle , supertype de couche , cas spécial , modèle de domaine . Vous devez toujours concevoir correctement une couche de modèle et ne pas oublier la persistance (cela peut augmenter considérablement les performances de votre application). Vous pouvez utiliser Core Data
pour cela. Mais vous ne devez pas oublier, ce Core Data
n'est pas un ORM ou une base de données, mais un gestionnaire de graphes d'objets avec la persistance comme une bonne option. Ainsi, très souvent, cela Core Data
peut être trop lourd pour vos besoins et vous pouvez envisager de nouvelles solutions telles que Realm et Couchbase Lite , ou créer votre propre couche de mappage / persistance d'objets légers, basée sur SQLite brut ou LevelDB. Je vous conseille également de vous familiariser avec la conception pilotée par domaine et le CQRS .
Au début, je pense que nous devrions créer une autre couche pour la mise en réseau, car nous ne voulons pas de gros contrôleurs ou de modèles lourds et dépassés. Je ne crois pas à ces fat model, skinny controller
choses. Mais je crois en l' skinny everything
approche, car aucune classe ne devrait être grosse, jamais. Tout réseau peut être généralement résumé comme une logique métier, par conséquent, nous devrions avoir une autre couche, où nous pouvons la mettre. La couche de service est ce dont nous avons besoin:
It encapsulates the application's business logic, controlling transactions
and coordinating responses in the implementation of its operations.
Dans notre MVC
royaume Service Layer
est quelque chose comme un médiateur entre le modèle de domaine et les contrôleurs. Il existe une variante assez similaire de cette approche appelée MVCS où a Store
est en fait notre Service
couche. Store
instances de modèle vends et gère le réseau, la mise en cache , etc. Je veux mentionner que vous ne devriez pas écrire tout votre réseau et la logique métier dans votre couche de service. Cela peut également être considéré comme une mauvaise conception. Pour plus d'informations, consultez les modèles de domaine anémique et riche . Certaines méthodes de service et logique métier peuvent être gérées dans le modèle, ce sera donc un modèle "riche" (avec comportement).
J'utilise toujours largement deux bibliothèques: AFNetworking 2.0 et ReactiveCocoa . Je pense que c'est un must pour toute application moderne qui interagit avec le réseau et les services Web ou contient une logique d'interface utilisateur complexe.
ARCHITECTURE
Au début, je crée une APIClient
classe générale , qui est une sous-classe de AFHTTPSessionManager . Il s'agit d'un cheval de bataille de tous les réseaux de l'application: toutes les classes de service lui délèguent des demandes REST réelles. Il contient toutes les personnalisations du client HTTP, dont j'ai besoin dans l'application particulière: épinglage SSL, traitement des erreurs et création d' NSError
objets simples avec des raisons d'échec détaillées et des descriptions de toutes API
et des erreurs de connexion (dans ce cas, le contrôleur sera en mesure d'afficher les messages corrects pour l'utilisateur), définir des sérialiseurs de demande et de réponse, des en-têtes http et d'autres éléments liés au réseau. Ensuite , je divise logiquement toutes les demandes de l' API dans subservices ou, plus correctement, microservices : UserSerivces
, CommonServices
, SecurityServices
,FriendsServices
et ainsi de suite, conformément à la logique métier qu'ils mettent en œuvre. Chacun de ces microservices est une classe distincte. Ils forment ensemble un Service Layer
. Ces classes contiennent des méthodes pour chaque demande d'API, traitent les modèles de domaine et renvoient toujours un RACSignal
avec le modèle de réponse analysé ou NSError
à l'appelant.
Je veux mentionner que si vous avez une logique de sérialisation de modèle complexe - alors créez une autre couche pour cela: quelque chose comme Data Mapper mais plus général, par exemple JSON / XML -> Model mapper. Si vous avez un cache: créez-le également en tant que couche / service séparé (vous ne devez pas mélanger la logique métier avec la mise en cache). Pourquoi? Parce que la couche de mise en cache correcte peut être assez complexe avec ses propres accrochages. Les gens mettent en œuvre une logique complexe pour obtenir une mise en cache valide et prévisible comme par exemple la mise en cache monoïdale avec des projections basées sur des profuncteurs. Vous pouvez lire sur cette belle bibliothèque appelée Carlos pour en savoir plus. Et n'oubliez pas que Core Data peut vraiment vous aider avec tous les problèmes de mise en cache et vous permettra d'écrire moins de logique. De plus, si vous avez une logique entre NSManagedObjectContext
les modèles de demande de serveur et les modèles de serveur, vous pouvez utiliserModèle de référentiel , qui sépare la logique qui récupère les données et les mappe au modèle d'entité de la logique métier qui agit sur le modèle. Donc, je conseille d'utiliser le modèle de référentiel même lorsque vous avez une architecture basée sur les données de base. Référentiel peut des choses abstraites, comme NSFetchRequest
, NSEntityDescription
, NSPredicate
et ainsi de suite à des méthodes simples comme get
ou put
.
Après toutes ces actions dans la couche Service, l'appelant (contrôleur de vue) peut effectuer des tâches asynchrones complexes avec la réponse: manipulations de signaux, chaînage, mappage, etc. à l'aide de ReactiveCocoa
primitives, ou simplement vous y abonner et afficher les résultats dans la vue . J'injectent l' injection de dépendance dans toutes ces classes de service mes APIClient
, ce qui se traduira par un appel de service particulier en correspondant GET
, POST
, PUT
, DELETE
, etc. demande au point de terminaison REST. Dans ce cas APIClient
est passé implicitement à tous les contrôleurs, vous pouvez le rendre explicite avec un paramétré sur APIClient
les classes de service. Cela peut avoir un sens si vous souhaitez utiliser différentes personnalisations duAPIClient
pour des classes de service particulières, mais si, pour certaines raisons, vous ne voulez pas de copies supplémentaires ou si vous êtes sûr que vous utiliserez toujours une instance particulière (sans personnalisation) de APIClient
- faites-en un singleton, mais ne le faites pas, s'il vous plaît DON 'T faire des classes de service comme singletons.
Ensuite, chaque contrôleur de vue avec le DI injecte la classe de service dont il a besoin, appelle les méthodes de service appropriées et compose leurs résultats avec la logique de l'interface utilisateur. Pour l'injection de dépendance, j'aime utiliser BloodMagic ou un framework Typhoon plus puissant . Je n'utilise jamais de singletons, de cours de Dieu APIManagerWhatever
ou d'autres mauvaises choses. Parce que si vous appelez votre classe WhateverManager
, cela indique que vous ne connaissez pas son objectif et c'est un mauvais choix de conception . Les singletons sont également un anti-modèle, et dans la plupart des cas (sauf les rares), c'est une mauvaise solution. Le singleton ne devrait être envisagé que si les trois critères suivants sont satisfaits:
- La propriété de l'instance unique ne peut pas être raisonnablement attribuée;
- Une initialisation paresseuse est souhaitable;
- L'accès global n'est pas prévu autrement.
Dans notre cas, la propriété de l'instance unique n'est pas un problème et nous n'avons pas non plus besoin d'un accès global après avoir divisé notre gestionnaire de dieux en services, car maintenant un ou plusieurs contrôleurs dédiés ont besoin d'un service particulier (par exemple, UserProfile
les besoins des contrôleurs UserServices
, etc.) .
Nous devons toujours respecter le S
principe dans SOLID et utiliser la séparation des préoccupations , alors ne mettez pas toutes vos méthodes de service et vos appels réseaux dans une seule classe, car c'est fou, surtout si vous développez une application de grande entreprise. C'est pourquoi nous devrions considérer l'injection de dépendance et l'approche des services. Je considère cette approche comme moderne et post-OO . Dans ce cas, nous avons divisé notre application en deux parties: la logique de contrôle (contrôleurs et événements) et les paramètres.
Un type de paramètres serait des paramètres de «données» ordinaires. C'est ce que nous transmettons aux fonctions, manipulons, modifions, persistons, etc. Ce sont des entités, des agrégats, des collections, des classes de cas. L'autre type serait les paramètres de «service». Ce sont des classes qui encapsulent la logique métier, permettent de communiquer avec des systèmes externes, fournissent un accès aux données.
Voici un workflow général de mon architecture par exemple. Supposons que nous ayons un FriendsViewController
, qui affiche la liste des amis de l'utilisateur et que nous ayons une option à supprimer des amis. Je crée une méthode dans ma FriendsServices
classe appelée:
- (RACSignal *)removeFriend:(Friend * const)friend
où Friend
est un objet modèle / domaine (ou il peut être juste un User
objet s'ils ont des attributs similaires). Underhood cette méthode Parsis Friend
à NSDictionary
des paramètres JSON friend_id
, name
, surname
, friend_request_id
et ainsi de suite. J'utilise toujours la bibliothèque Mantle pour ce type de passe-partout et pour ma couche de modèle (analyse en avant et en avant, gestion des hiérarchies d'objets imbriquées dans JSON, etc.). Après l' analyse qu'il appelle APIClient
DELETE
méthode pour faire une demande REST réelle et retourne Response
en RACSignal
l'appelant ( FriendsViewController
dans notre cas) pour afficher un message approprié pour l'utilisateur ou autre.
Si notre application est très importante, nous devons séparer notre logique encore plus clairement. Par exemple, il n'est pas toujours bon de mélanger Repository
ou de modéliser la logique avec Service
une seule. Lorsque j'ai décrit mon approche, j'avais dit que la removeFriend
méthode devrait être dans la Service
couche, mais si nous allons être plus pédants, nous pouvons remarquer qu'elle appartient mieux Repository
. Rappelons-nous ce qu'est le référentiel. Eric Evans en a donné une description précise dans son livre [DDD]:
Un référentiel représente tous les objets d'un certain type comme un ensemble conceptuel. Il agit comme une collection, sauf avec une capacité d'interrogation plus élaborée.
Ainsi, a Repository
est essentiellement une façade qui utilise la sémantique de style Collection (Ajouter, Mettre à jour, Supprimer) pour fournir l'accès aux données / objets. Voilà pourquoi quand vous avez quelque chose comme: getFriendsList
, getUserGroups
, removeFriend
vous pouvez le placer dans le Repository
, parce que la collection comme la sémantique est assez clair. Et du code comme:
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
est certainement une logique métier, car elle va au-delà des CRUD
opérations de base et connecte deux objets de domaine ( Friend
et Request
), c'est pourquoi elle doit être placée dans la Service
couche. Je veux aussi remarquer: ne créez pas d'abstractions inutiles . Utilisez judicieusement toutes ces approches. Parce que si vous submergez votre application d'abstractions, cela augmentera sa complexité accidentelle, et la complexité causera plus de problèmes dans les systèmes logiciels qu'autre chose
Je vous décris un "ancien" exemple Objective-C mais cette approche peut être très facilement adaptée au langage Swift avec beaucoup plus d'améliorations, car elle a des fonctionnalités plus utiles et du sucre fonctionnel. Je recommande fortement d'utiliser cette bibliothèque: Moya . Il vous permet de créer une APIClient
couche plus élégante (notre cheval de bataille comme vous vous en souvenez). Désormais, notre APIClient
fournisseur sera un type de valeur (enum) avec des extensions conformes aux protocoles et exploitant la mise en correspondance des modèles de déstructuration. L'énumération rapide + la correspondance de motifs nous permet de créer des types de données algébriques comme dans la programmation fonctionnelle classique. Nos microservices utiliseront ce APIClient
fournisseur amélioré comme dans l'approche Objective-C habituelle. Pour la couche modèle au lieu de Mantle
vous pouvez utiliser la bibliothèque ObjectMapperou j'aime utiliser une bibliothèque Argo plus élégante et fonctionnelle .
J'ai donc décrit mon approche architecturale générale, qui peut être adaptée à n'importe quelle application, je pense. Il peut y avoir bien sûr beaucoup plus d'améliorations. Je vous conseille d'apprendre la programmation fonctionnelle, car vous pouvez en bénéficier beaucoup, mais n'allez pas trop loin avec. L'élimination d'un état mutable global, excessif, partagé, la création d'un modèle de domaine immuable ou la création de fonctions pures sans effets secondaires externes est, en général, une bonne pratique, et un nouveau Swift
langage encourage cela. Mais rappelez-vous toujours que surcharger votre code avec de lourds modèles fonctionnels purs, les approches théoriques par catégorie est une mauvaise idée, car d' autres développeurs liront et prendront en charge votre code, et ils peuvent être frustrés ou effrayants de laprismatic profunctors
et ce genre de choses dans votre modèle immuable. La même chose avec le ReactiveCocoa
: ne faites pas tropRACify
votre code , car il peut devenir illisible très rapidement, surtout pour les débutants. Utilisez-le quand il peut vraiment simplifier vos objectifs et votre logique.
Ainsi, read a lot, mix, experiment, and try to pick up the best from different architectural approaches
. C'est le meilleur conseil que je puisse vous donner.