Meilleures approches architecturales pour la création d'applications réseau iOS (clients REST)
Je suis un développeur iOS avec une certaine expérience et cette question est vraiment intéressante pour moi. J'ai vu beaucoup de ressources et de matériaux différents sur ce sujet, mais néanmoins je suis toujours confus. Quelle est la meilleure architecture pour une application en réseau iOS? Je veux dire un cadre abstrait de base, des modèles, qui s'adapteront à toutes les applications réseau, qu'il s'agisse d'une petite application qui n'a que quelques requêtes de serveur ou un client REST complexe. Apple recommande d'utiliser MVC
comme architecture de base approche pour toutes les applications iOS, mais ni {[0] } ni les modèles MVVM
plus modernes n'expliquent où mettre du code logique réseau et comment l'organiser en général.
Dois-je développer quelque chose comme MVCS
(S
pour Service
) et dans cette couche Service
mettre toutes les requêtes API
et autres logiques de réseau, ce qui en perspective peut être vraiment complexe? Après avoir fait quelques recherches, j'ai trouvé deux approches de base pour cela. ici Il a été recommandé de créer une classe distincte pour chaque demande de réseau au service web API
(comme LoginRequest
class ou PostCommentRequest
class et ainsi de suite) qui hérite de la classe abstraite de requête de base AbstractBaseRequest
et en plus de créer un gestionnaire de réseau global qui encapsule le code réseau commun et d'autres préférences (il peut s'agir de AFNetworking
personnalisation ou RestKit
tuning, si nous avons des mappages Mais cette approche semble une surcharge pour moi. Un autre l'approche consiste à avoir une classe singleton API
dispatcher ou manager comme dans la première approche, mais pas pour créer des classes pour chaque requête et encapsuler chaque requête en tant que méthode publique d'instance de cette classe manager comme: fetchContacts
, loginUser
méthodes, etc. Alors, quelle est la meilleure et correcte façon? Y a - t-il d'autres approches intéressantes que je ne connais pas encore?
Et devrais-je créer une autre couche pour toutes ces choses de réseau comme Service
, ou NetworkProvider
couche ou autre chose sur le dessus de mon architecture MVC
, ou cette couche devrait être intégrée (injectée) dans les couches MVC
existantes, par exemple Model
?
Je sais qu'il existe de belles approches, ou comment alors de tels monstres mobiles comme le client Facebook ou le client LinkedIn font face à une complexité exponentielle croissante de la logique de réseau?
Je sais qu'il n'y a pas de réponse exacte et formelle au problème. le but de cette question Est de recueillir les approches les plus intéressantes de développeurs iOS Expérimentés. Le la meilleure approche suggérée sera marquée comme acceptée et récompensée par une prime de réputation, d'autres seront upvoted. C'est surtout une question théorique et de recherche. Je veux comprendre l'approche architecturale de base, abstraite et correcte pour les applications réseau dans iOS. J'espère des explications détaillées de développeurs expérimentés.
11 réponses
I want to understand basic, abstract and correct architectural approach for networking applications in iOS
: Il n'y a pas " la meilleure " ou "la plus correcte" approche pour construire une architecture d'application. Il est un très travail de création. Vous devez toujours choisir l'architecture la plus simple et 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 une "bonne" et une "mauvaise" architecture.
Vous avez dit: collect the most interesting approaches from experienced iOS developers
, Je ne pense pas que mon approche soit la plus intéressant ou correct, mais je l'ai utilisé dans plusieurs projets et satisfait. C'est une approche hybride de ceux que vous avez mentionné ci-dessus, et aussi avec des améliorations de mes propres efforts de recherche. Je suis intéressant dans les 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éressants, que nous pouvons appliquer création d'une application iOS architecture (, à mon avis): la Couche de Service, Unité De Travail, Distance de la Façade, Objet de Transfert de Données, Passerelle, Couche Supertype, Cas Particulier, 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 devriez pas oublier, que Core Data
n'est pas un ORM ou une base de données, mais un gestionnaire de graphe d'objet avec persistance comme une bonne option de celui-ci. Ainsi, très souvent Core Data
peut être trop lourd pour vos besoins et vous pouvez regarder de nouvelles solutions telles que Realm et Couchbase Lite , ou construire votre propre couche de mappage/persistance d'objets légers, basée sur SQLite brut ou LevelDB. Aussi, je vous conseille de vous familiariser avec le Domain Driven Design et CQRS .
Au début, je pense que nous devrions créer une autre couche pour la mise en réseau, parce que nous ne voulons pas de contrôleurs fat ou de modèles lourds et dépassés. Je ne crois pas à ces fat model, skinny controller
choses. Mais je crois à l'approche skinny everything
, car aucune classe ne devrait être grosse, jamais. Tous les réseaux peuvent être généralement abstraits en tant que logique métier, par conséquent, nous devrions avoir une autre couche, où nous pouvons le mettre. Couche de Service est ce que nous besoin:
It encapsulates the application's business logic, controlling transactions
and coordinating responses in the implementation of its operations.
Dans notre domaine MVC
Service Layer
est quelque chose comme un médiateur entre le modèle de domaine et les contrôleurs. Il existe une variation assez similaire de cette approche appelée MVCS où un Store
est en fait notre couche Service
. Store
Vend des instances de modèle et gère la mise en réseau, la mise en cache, etc. Je tiens à mentionner que vous ne devriez pas écrire toute votre logique réseau et métier dans votre couche de service. Cela peut également être considéré comme une mauvaise conception. Pour plus d' info regarder le Anémique et Riches modèles de domaine. Certaines méthodes de service et la 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 doit avoir pour toute application moderne qui interagit avec le réseau et les services web ou contient une logique D'interface utilisateur complexe.
L'ARCHITECTURE
Au début, je crée une classe générale APIClient
, qui est une sous-classe de AFHTTPSessionManager . C'est un cheval de bataille de tous les réseaux de l'application: toutes les classes de service lui déléguent des requêtes 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'objets NSError
simples avec des raisons d'échec détaillées et des descriptions de tous API
et les erreurs de connexion (dans ce cas, le contrôleur sera en mesure d'afficher les messages corrects pour l'utilisateur), la configuration des sérialiseurs de requêtes et de réponses, des en-têtes http et d'autres éléments liés au réseau. Ensuite, je divise logiquement toutes les requêtes API en sous-Services ou, plus correctement, microservices: UserSerivces
, CommonServices
, SecurityServices
, FriendsServices
et ainsi de suite, en conséquence de la logique métier qu'ils implémentent. Chacun de ces microservices est une classe distincte. Ils forment ensemble un Service Layer
. Ces classes contiennent méthodes pour chaque demande D'API, traiter les modèles de domaine et renvoie 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 en tant que couche/service séparé (vous ne devriez 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 pièges. Les gens implémentent 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 comprendre plus. Et n'oubliez pas que les données de base peuvent vraiment vous aider avec tous les problèmes de mise en cache et vous permettront d'écrire moins de logique. De plus, si vous avez une logique entre NSManagedObjectContext
et les modèles de requêtes serveur, vous pouvez utiliserRepository pattern, 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 choses abstraites, comme NSFetchRequest
,NSEntityDescription
, NSPredicate
et ainsi de suite jusqu'à la plaine des méthodes comme get
ou put
.
Après toutes ces actions dans la couche de Service, l'appelant (contrôleur de vue) peut faire des choses asynchrones Complexes avec la réponse: manipulations de signal, chaînage, cartographie, etc. avec l'aide de ReactiveCocoa
primitives, ou simplement s'y abonner et afficher les résultats dans la vue. J'injecte avec l'injection de dépendance dans toutes ces classes de service my APIClient
, qui traduira 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 une classe de service paramétrée sur APIClient
. Cela peut avoir du sens si vous voulez pour utiliser différentes personnalisations du APIClient
pour des classes de service particulières, mais si vous ,pour certaines raisons, ne voulez pas de copies supplémentaires ou si vous êtes sûr que vous utiliserez toujours une instance particulière (sans personnalisations) du APIClient
- faites-en un singleton, mais ne le faites pas, ne faites pas de classes de service comme singletons.
Ensuite, chaque contrôleur de vue injecte à nouveau 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 injection de dépendance j'aime utiliser BloodMagic {[92] } ou un framework plus puissant Typhoon . Je n'utilise jamais de singletons, de classe Dieu APIManagerWhatever
ou d'autres mauvaises choses. Parce que si vous appelez votre classe WhateverManager
, cela indique que vous ne connaissez pas son but et il est un mauvais choix de conception. Singletons est également un anti-pattern, et dans la plupart des cas (sauf les rares) est une solution fausse. Singleton ne doit être considéré que si les trois critères suivants sont satisfaits:
- la propriété de l'instance unique ne peut être raisonnablement attribuée;
- l'initialisation paresseuse est souhaitable;
- l'accès Global n'est pas prévu par ailleurs.
Dans notre cas, la propriété de l'instance unique n'est pas un problème et nous n'avons pas besoin d'un accès global après avoir divisé notre gestionnaire de Dieu en services, car maintenant seulement un ou plusieurs contrôleurs dédiés ont besoin d'un service particulier (par exemple UserProfile
le contrôleur a besoin de UserServices
et ainsi de suite sur).
Nous devrions toujours respecter le principe S
dans SOLID et utiliser séparation des préoccupations , alors ne mettez pas toutes vos méthodes de service et vos appels de réseaux dans une classe, car c'est fou, surtout si vous développez une grande application d'entreprise. C'est pourquoi nous devrions envisager l'injection de dépendance et l'approche des services. Je considère cette approche comme moderne et post-OO. Dans ce cas, nous divisons notre application en deux parties: la logique de contrôle (contrôleurs et les événements) et les paramètres.
Un type de paramètres serait des paramètres "données" ordinaires. C'est ce que nous passons autour des 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 "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. Nous allons 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 classe FriendsServices
appelée:
- (RACSignal *)removeFriend:(Friend * const)friend
Où Friend
est un objet modèle / domaine (ou il peut s'agir simplement d'un objet User
s'ils ont des attributs similaires). Cette méthode analyse Friend
à {[51] } des paramètres JSONfriend_id
, name
, surname
, friend_request_id
et ainsi de suite. J'utilise toujours la bibliothèque Mantle pour ce genre de passe-partout et pour ma couche de modèle (analyse en arrière et en avant, la gestion des hiérarchies d'objets imbriqués dans JSON et ainsi de suite). Après l'analyse il appelle APIClient
DELETE
méthode pour faire une demande REST réelle et renvoie Response
dans 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, ce n'est pas toujours bon de mélanger Repository
ou la logique du modèle avec Service
un. Lorsque j'ai décrit mon approche, j'avais dit que removeFriend
la méthode devrait être dans la couche Service
, mais si nous sommes plus pédants, nous pouvons remarquer qu'elle appartient mieux à Repository
. Souvenons-nous de ce Qu'est le référentiel. Eric Evans lui a donné une description précise dans son livre [DDD]:
Un Référentiel représente tous les objets d'un certain type conceptuel défini. Il agit comme une collection, sauf avec une capacité d'interrogation plus élaborée.
Donc, un Repository
est essentiellement une façade qui utilise la sémantique de style de Collection (Add, Update, Supprimer) pour fournir l'accès aux données / objets. C'est pourquoi quand vous avez quelque chose comme: getFriendsList
, getUserGroups
, removeFriend
vous pouvez le placer dans le Repository
, car la sémantique de type collection est assez claire ici. Et le code comme:
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
Est définitivement une logique métier, car elle dépasse les opérations de base CRUD
et connecte deux objets de domaine (Friend
et Request
), c'est pourquoi elle doit être placée dans la couche Service
. Aussi, je veux remarquer: ne créez pas d'abstractions inutiles . Utiliser tous les ces approches judicieusement. Parce que si vous submergez votre application avec des abstractions, cela augmentera sa complexité accidentelle, et la complexité cause plus de problèmes dans les systèmes logiciels que toute autre chose
Je vous décris un "vieux" exemple Objective-C mais cette approche peut être très facile à adapter au langage Swift avec beaucoup plus d'améliorations, car elle a plus de fonctionnalités utiles et de sucre fonctionnel. Je recommande fortement d'utiliser cette bibliothèque: Moya. Il vous permet de créer un calque APIClient
plus élégant (notre cheval de bataille comme vous vous en souvenez). Maintenant, notre fournisseur APIClient
sera un type de valeur (enum) avec des extensions conformes aux protocoles et tirant parti de la correspondance de motifs de déstructuration. SWIFT enums + pattern matching nous permet de créer types de données algébriques comme dans la programmation fonctionnelle classique. Nos microservices utiliseront ce fournisseur APIClient
amélioré comme dans l'approche Objective-C. Pour la couche de modèle au lieu de Mantle
, vous pouvez utiliser ObjectMapper bibliothèque ou je voudrais utiliser plus élégante et fonctionnelle Argo bibliothèque.
Donc, j'ai décrit mon approche architecturale générale, qui peut être adaptée à n'importe quelle application, je pense. Il peut y avoir beaucoup plus d'améliorations, bien sûr. Je vous conseille d'apprendre la programmation fonctionnelle, car vous pouvez en bénéficier beaucoup, mais n'allez pas trop loin avec elle aussi. Éliminer l'état mutable excessif, partagé et global, créer un modèle de domaineimmuable ou créer des fonctions pures sans effets secondaires externes est généralement une bonne pratique, et le nouveau langage Swift
l'encourage. Mais rappelez-vous toujours que surcharger votre code avec de lourds Modèles fonctionnels purs, des approches théoriques par catégorie est une mauvaise idée, parce que les autres développeurs liront et soutiendront votre code, et ils peuvent être frustrés ou effrayants du prismatic profunctors
et ce genre de choses dans votre modèle immuable. La même chose avec le ReactiveCocoa
: ne RACify
votre code trop , car il peut devenir illisible très vite, surtout pour les débutants. Utilisez-le quand il peut vraiment simplifier vos objectifs et votre logique.
Donc, 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.
Selon le but de cette question, j'aimerais décrire notre approche architecturale.
Approche de l'Architecture
L'architecture de notre application iOS générale repose sur les modèles suivants: couches de Service, MVVM, liaison de données UI, injection de dépendance ; etprogrammation réactive fonctionnelle paradigme.
Nous pouvons découper une application de consommateur typique en suivant logique couches:
- Assemblée
- Modèle
- Services de
- Stockage
- Gestionnaires
- Coordonnateurs
- UI
- Infrastructure
La couche D'assemblage est un point d'amorçage de notre application. Il contient un conteneur D'Injection de dépendances et des déclarations des objets de l'application et de leurs dépendances. Cette couche peut également contenir la configuration de l'application (URL, clés de services tiers, etc.). À cette fin nous utilisons Typhon bibliothèque.
La couche de Modèle contient des classes de modèles de domaine, des validations, des mappages. Nous utilisons la bibliothèque Mantle pour mapper nos modèles: elle prend en charge la sérialisation/désérialisation au format JSON
et aux modèles NSManagedObject
. Pour la validation et la représentation de nos modèles, nous utilisons les bibliothèques FXForms et fxmodelvalidation.
La couche Services déclare les services que nous utilisons pour interagir avec des systèmes externes dans afin d'envoyer ou de recevoir des données qui sont représentées dans notre modèle de domaine. Nous avons donc généralement des services de communication avec les API serveur (par entité), des services de messagerie (comme PubNub), des services de stockage (comme Amazon S3), etc. Fondamentalement, les services enveloppent les objets fournis par les SDK (par exemple PubNub SDK) ou implémentent leur propre logique de communication. Pour la mise en réseau générale, nous utilisonsAFNetworking bibliothèque.
Le but de la couche de stockage est d'organiser le stockage de données local sur l'appareil. Nous utilisons des données de base ou Realm pour cela (les deux ont des avantages et des inconvénients, la décision de ce qu'il faut utiliser est basée sur des spécifications concrètes). Pour la configuration des données de base, nous utilisons mdmcoredata bibliothèque et tas de classes - stockages - (similaires aux services) qui fournissent un accès au stockage local pour chaque entité. Pour Realm, nous utilisons simplement des stockages similaires pour avoir accès au stockage local.
Managers layer {[42] } est un endroit où vivent nos abstractions/wrappers.
Dans un gestionnaire le rôle pourrait être:
- Gestionnaire D'informations d'identification avec ses différentes implémentations (keychain, NSDefaults,...)
- Gestionnaire de Session en cours qui sait comment conserver et fournir la session utilisateur en cours
- Pipeline de Capture qui permet d'accéder aux périphériques multimédias (enregistrement vidéo, audio, prise de photos)
- gestionnaire BLE qui donne accès aux services et périphériques bluetooth
- Gestionnaire De Géolocalisation
- ...
Donc, dans le rôle de gestionnaire pourrait être n'importe quel objet qui implémente la logique d'un aspect ou d'une préoccupation particulière nécessaire au fonctionnement de l'application.
, Nous essayons d'éviter les Singletons, mais cette couche est un endroit où ils vivent, s'ils sont nécessaires.
La couche Coordinators fournit des objets qui dépendent des objets d'autres couches (Service, Stockage, modèle) afin de combiner leur logique en une séquence de travail nécessaire pour certains modules (fonctionnalité, écran, histoire d'utilisateur ou expérience utilisateur). Il chaînes habituellement opérations asynchrones et sait comment réagir sur leurs cas de réussite et d'échec. À titre d'exemple, vous pouvez imaginer une fonctionnalité de messagerie et un objet MessagingCoordinator
correspondant. La gestion de l'opération d'envoi de message peut ressembler à ceci:
- valider le message (couche modèle)
- Enregistrer le message localement (stockage des messages)
- Télécharger la pièce jointe du message (service amazon S3)
- mettre à jour l'état du message et les URL des pièces jointes et enregistrer le message localement (stockage des messages)
- sérialiser le message au format JSON (couche de modèle)
- publier un message à PubNub (service PubNub)
- mettez à jour l'état et les attributs du message et enregistrez-le localement (stockage des messages)
Sur chacune des étapes ci-dessus, une erreur est traitée en conséquence.
La couche D'interface utilisateur se compose des sous-couches suivantes:
- Viewmodel
- ViewControllers
- Vues
Afin d'éviter les contrôleurs de vue massifs, nous utilisons MVVM pattern et implémenter la logique nécessaire pour la présentation de L'interface utilisateur dans ViewModels. Un ViewModel a généralement des coordinateurs et des gestionnaires comme dépendances. ViewModels utilisés par ViewControllers et certains types de vues (par exemple, les cellules de vue de table). La colle entre ViewControllers et ViewModels est la liaison de données et le modèle de commande. Afin de permettre d'avoir cette colle, nous utilisonsreactivecocoa library.
Nous utilisons également ReactiveCocoa et son concept RACSignal
comme interface et valeur de retour type de tous les coordinateurs, services, méthodes de stockage. Cela nous permet d'enchaîner les opérations, de les exécuter en parallèle ou en série, et de nombreuses autres choses utiles fournies par ReactiveCocoa.
Nous essayons d'implémenter notre comportement D'interface utilisateur de manière déclarative. La liaison de données et la mise en page automatique aident beaucoup à atteindre cet objectif.
La couche D'Infrastructure contient toutes les aides, extensions et utilitaires nécessaires au travail de l'application.
Cette approche fonctionne bien pour nous et pour types d'applications que nous construisons habituellement. Mais vous devriez comprendre, que c'est juste une approche subjective qui devrait être adaptée/changée pour le but concret de l'équipe.
J'espère que cela vous aidera!
Vous pouvez également trouver plus d'informations sur le processus de développement iOS dans cet article de blog développement iOS en tant que Service
Parce que toutes les applications iOS sont différentes, je pense qu'il y a différentes approches ici à considérer, mais je vais généralement de cette façon:
Créez une classe de gestionnaire central (singleton) pour gérer toutes les demandes D'API (généralement nommées APICommunicator) et chaque méthode d'instance est un appel D'API. Et il y a une méthode centrale (non publique):
-
(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;
Pour mémoire, j'utilise 2 bibliothèques/frameworks majeurs, ReactiveCocoa et AFNetworking. ReactiveCocoa gère le réseau asynchrone réponses parfaitement, vous pouvez faire (sendNext:, sendError:, etc.).
Cette méthode appelle L'API, obtient les résultats et les envoie via RAC au format ' raw '(comme NSArray what AFNetworking returns).
Ensuite, une méthode comme getStuffList:
qui a appelé la méthode ci-dessus s'abonne à son signal, analyse les données brutes en objets (avec quelque chose comme Motis) et envoie les objets un par un à l'appelant (getStuffList:
et des méthodes similaires renvoient également un signal auquel le contrôleur peut s'abonner).
Le le contrôleur souscrit reçoit les objets par le bloc de subscribeNext:
et les gère.
J'ai essayé de nombreuses façons dans différentes applications, mais celui-ci a fonctionné le mieux de tous, donc je l'ai utilisé dans quelques applications récemment, il s'adapte à la fois aux petits et aux grands projets et il est facile d'étendre et de maintenir si quelque chose doit être modifié.
J'espère que cela aide, j'aimerais entendre les opinions des autres sur mon approche et peut-être comment les autres pensent que cela pourrait être amélioré.
Dans ma situation, j'utilise généralement la bibliothèque ResKit pour configurer la couche réseau. Il fournit une analyse facile à utiliser. Cela réduit mes efforts pour configurer le mappage pour différentes réponses et autres choses.
J'ajoute seulement du code pour configurer le mappage automatiquement. Je définis la classe de base pour mes modèles (pas le protocole en raison de beaucoup de code pour vérifier si une méthode est implémentée ou non, et moins de code dans les modèles eux-mêmes):
MappableEntry.h
@interface MappableEntity : NSObject
+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;
@end
MappableEntry.m
@implementation MappableEntity
+(NSArray*)pathPatterns {
return @[];
}
+(NSArray*)keyPathes {
return nil;
}
+(NSArray*)fieldsArrayForMapping {
return @[];
}
+(NSDictionary*)fieldsDictionaryForMapping {
return @{};
}
+(NSArray*)relationships {
return @[];
}
@end
Les relations sont des objets qui représentent des objets imbriqués en réponse:
RelationshipObject.h
@interface RelationshipObject : NSObject
@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;
+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;
@end
RelationshipObject.m
@implementation RelationshipObject
+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
RelationshipObject* object = [[RelationshipObject alloc] init];
object.source = key;
object.destination = key;
object.mappingClass = mappingClass;
return object;
}
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
RelationshipObject* object = [[RelationshipObject alloc] init];
object.source = source;
object.destination = destination;
object.mappingClass = mappingClass;
return object;
}
@end
Ensuite, je configure le mappage pour RestKit comme ceci:
ObjectMappingInitializer.h
@interface ObjectMappingInitializer : NSObject
+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;
@end
ObjectMappingInitializer.m
@interface ObjectMappingInitializer (Private)
+ (NSArray*)mappableClasses;
@end
@implementation ObjectMappingInitializer
+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {
NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];
// Creating mappings for classes
for (Class mappableClass in [self mappableClasses]) {
RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
[newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
[newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
[mappingObjects setObject:newMapping forKey:[mappableClass description]];
}
// Creating relations for mappings
for (Class mappableClass in [self mappableClasses]) {
RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
for (RelationshipObject *relation in [mappableClass relationships]) {
[mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
}
}
// Creating response descriptors with mappings
for (Class mappableClass in [self mappableClasses]) {
for (NSString* pathPattern in [mappableClass pathPatterns]) {
if ([mappableClass keyPathes]) {
for (NSString* keyPath in [mappableClass keyPathes]) {
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
}
} else {
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
}
}
}
// Error Mapping
RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
[errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
for (NSString *pathPattern in Error.pathPatterns) {
[[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
}
}
@end
@implementation ObjectMappingInitializer (Private)
+ (NSArray*)mappableClasses {
return @[
[FruiosPaginationResults class],
[FruioItem class],
[Pagination class],
[ContactInfo class],
[Credentials class],
[User class]
];
}
@end
Quelques exemples de mappableentry implémentation:
de l'Utilisateur.h
@interface User : MappableEntity
@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;
- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;
- (NSDictionary*)registrationData;
@end
de l'Utilisateur.m
@implementation User
- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
if (self = [super init]) {
self.username = username;
self.email = email;
self.password = password;
}
return self;
}
- (NSDictionary*)registrationData {
return @{
@"username": self.username,
@"email": self.email,
@"password": self.password
};
}
+ (NSArray*)pathPatterns {
return @[
[NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
];
}
+ (NSArray*)fieldsArrayForMapping {
return @[ @"username", @"email", @"password", @"token" ];
}
+ (NSDictionary*)fieldsDictionaryForMapping {
return @{ @"id": @"userId" };
}
@end
Maintenant à propos de L'emballage des requêtes:
J'ai un fichier d'en-tête avec une définition de blocs, pour réduire la longueur de ligne dans toutes les classes APIRequest:
APICallbacks.h
typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);
Et exemple de ma classe APIRequest que j'utilise:
LoginAPI.h
@interface LoginAPI : NSObject
- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;
@end
LoginAPI.m
@implementation LoginAPI
- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
[[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
onSuccess(mappingResult.array);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
onError(error);
}];
}
@end
Et tout ce dont vous avez besoin pour faire dans le code, initialisez simplement L'objet API et appelez-le chaque fois que vous en avez besoin:
SomeViewController.m
@implementation SomeViewController {
LoginAPI *_loginAPI;
// ...
}
- (void)viewDidLoad {
[super viewDidLoad];
_loginAPI = [[LoginAPI alloc] init];
// ...
}
// ...
- (IBAction)signIn:(id)sender {
[_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
// Success Block
} onError:^(NSError *error) {
// Error Block
}];
}
// ...
@end
Mon code n'est pas parfait, mais il est facile à définir une fois et à utiliser pour différents projets. Si c'est intéressant pour tout le monde, mb je pourrais passer du temps et faire une solution universelle quelque part sur GitHub et CocoaPods.
À mon avis, toute l'architecture logicielle est motivée par le besoin. Si cela est à des fins d'apprentissage ou personnelles, puis décider de l'objectif principal et ont qui conduisent l'architecture. Si c'est un travail à louer, alors le besoin d'affaires est primordial. L'astuce est de ne pas laisser les choses brillantes vous distraire des besoins réels. Je trouve cela difficile à faire. Il y a toujours de nouvelles choses brillantes qui apparaissent dans cette entreprise et beaucoup d'entre elles ne sont pas utiles, mais vous ne pouvez pas toujours le dire à l'avance. Se concentrer sur le besoin et être prêt à abandonner mauvais choix si vous le pouvez.
Par exemple, j'ai récemment fait un prototype rapide d'une application de partage de photos pour une entreprise locale. Puisque le besoin commercial était de faire quelque chose de rapide et sale, l'architecture a fini par être un code iOS pour faire apparaître une caméra et un code réseau attaché à un bouton D'envoi qui a téléchargé l'image dans un magasin S3 et a écrit à un domaine SimpleDB. Le code était trivial et le coût minime et le client dispose d'une collection de photos évolutive accessible sur le web avec des appels REST. Bon marché et stupide, l'application avait beaucoup de défauts et verrouillerait l'interface utilisateur à l'occasion, mais ce serait un gaspillage de faire plus pour un prototype et cela leur permet de déployer leur personnel et de générer des milliers d'images de test facilement sans problèmes de performance ou d'évolutivité. Architecture merdique, mais il correspond parfaitement au besoin et au coût.
Un autre projet impliquait la mise en œuvre d'une base de données sécurisée locale qui se synchronise avec le système de l'entreprise en arrière-plan lorsque le réseau est disponible. J'ai créé un synchroniseur d'arrière-plan qui utilisait RestKit car il semblait avoir tout ce dont j'avais besoin. Mais j'ai dû écrire tellement de code personnalisé pour RestKit pour traiter le JSON idiosyncratique que j'aurais pu le faire plus rapidement en écrivant mon propre JSON en transformations CoreData. Cependant, le client voulait apporter cette application en interne et je sentais que RestKit serait similaire aux frameworks qu'ils utilisaient sur d'autres plates-formes. J'attends pour voir si c'était une bonne décision.
Encore une fois, le le problème pour moi est de se concentrer sur le besoin et de laisser cela déterminer l'architecture. J'essaie comme l'enfer d'éviter d'utiliser des paquets tiers car ils apportent des coûts qui n'apparaissent qu'après que l'application a été sur le terrain pendant un certain temps. J'essaie d'éviter de faire des hiérarchies de classe car elles rapportent rarement. Si je peux écrire quelque chose dans un délai raisonnable au lieu d'adopter un paquet qui ne correspond pas parfaitement, alors je le fais. Mon code est bien structuré pour le débogage et commenté de manière appropriée, mais tiers les paquets sont rarement. Cela dit, je trouve le réseautage AF trop utile pour ignorer et bien structuré, bien commenté, et maintenu et je l'utilise beaucoup! RestKit couvre beaucoup de cas courants, mais j'ai l'impression de me battre quand je l'utilise, et la plupart des sources de données que je rencontre sont pleines de bizarreries et de problèmes qui sont mieux gérés avec du code personnalisé. Dans mes dernières applications, j'utilise simplement les convertisseurs JSON intégrés et j'écris quelques méthodes utilitaires.
Un modèle que j'utilise toujours est d'obtenir le le réseau appelle le thread principal. Les 4-5 dernières applications que j'ai faites ont mis en place une tâche de minuteur en arrière-plan à l'aide de dispatch_source_create qui se réveille de temps en temps et effectue des tâches réseau au besoin. Vous devez effectuer un travail de sécurité des threads et vous assurer que le code de modification de L'interface utilisateur est envoyé au thread principal. Cela aide également à faire votre intégration / initialisation de telle sorte que l'utilisateur ne se sente pas accablé ou retardé. Jusqu'à présent, ce travail a été plutôt bien. Je suggère de regarder dans ces chose.
Enfin, je pense qu'à mesure que nous travaillons davantage et que le système d'exploitation évolue, nous avons tendance à développer de meilleures solutions. Il m'a fallu des années pour surmonter ma conviction que je dois suivre des modèles et des conceptions que d'autres personnes prétendent être obligatoires. Si je travaille dans un contexte où cela fait partie de la religion locale, ahem, je veux dire les meilleures pratiques d'ingénierie du ministère, alors je suis les coutumes à la lettre, c'est ce qu'ils me paient. Mais je trouve rarement que suivant des conceptions plus anciennes et patterns est la solution optimale. J'essaie toujours de regarder la solution à travers le prisme des besoins de l'entreprise et de construire l'architecture pour le match et de garder les choses aussi simples qu'elles peuvent être. Quand j'ai l'impression qu'il n'y en a pas assez, mais que tout fonctionne correctement, alors je suis sur la bonne voie.
J'utilise l'approche que j'ai obtenu à partir d'ici: https://github.com/Constantine-Fry/Foursquare-API-v2. J'ai réécrit cette bibliothèque rapide et vous pouvez voir l'approche architecturale de ces parties de code:
typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()
class Foursquare{
var authorizationCallback: OperationCallback?
var operationQueue: NSOperationQueue
var callbackQueue: dispatch_queue_t?
init(){
operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 7;
callbackQueue = dispatch_get_main_queue();
}
func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
let parameters: Dictionary <String, String> = [
"venueId":venueID,
"shout":shout,
"broadcast":"public"]
return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
}
func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
let url = self.constructURL(path, parameters: parameters)
var request = NSMutableURLRequest(URL: url)
request.HTTPMethod = httpMethod
let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
self.operationQueue.addOperation(operation)
return operation
}
func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
var parametersString = kFSBaseURL+path
var firstItem = true
for key in parameters.keys {
let string = parameters[key]
let mark = (firstItem ? "?" : "&")
parametersString += "\(mark)\(key)=\(string)"
firstItem = false
}
return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
}
}
class Operation: NSOperation {
var callbackBlock: OpertaionCallback
var request: NSURLRequest
var callbackQueue: dispatch_queue_t
init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
self.request = request
self.callbackBlock = callbackBlock
self.callbackQueue = callbackQueue
}
override func main() {
var error: NSError?
var result: AnyObject?
var response: NSURLResponse?
var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)
if self.cancelled {return}
if recievedData{
result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
if result != nil {
if result!.isKindOfClass(NSClassFromString("NSError")){
error = result as? NSError
}
}
if self.cancelled {return}
dispatch_async(self.callbackQueue, {
if (error) {
self.callbackBlock(success: false, result: error!);
} else {
self.callbackBlock(success: true, result: result!);
}
})
}
override var concurrent:Bool {get {return true}}
}
Fondamentalement, il existe une sous-classe NSOperation qui effectue NSURLRequest, analyse la réponse JSON et ajoute le bloc de rappel avec le résultat à la file d'attente. La classe API principale construit NSURLRequest, initialise cette sous-classe NSOperation et l'ajoute à la file.
Nous utilisons quelques approches en fonction de la situation. Pour la plupart des choses, AFNetworking est l'approche la plus simple et la plus robuste en ce sens que vous pouvez définir des en-têtes, télécharger des données en plusieurs parties, utiliser GET, POST, PUT & DELETE et il y a un tas de catégories supplémentaires pour UIKit qui vous permettent par exemple de définir une image à partir d'une url. Dans une application complexe avec beaucoup d'appels, nous résumons parfois cela à une méthode de commodité qui serait quelque chose comme:
-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
Il y en a quelques-uns les situations où AFNetworking n'est pas approprié, par exemple lorsque vous créez un framework ou un autre composant de bibliothèque, car AFNetworking peut déjà se trouver dans une autre base de code. Dans cette situation, vous utiliseriez un NSMutableURLRequest en ligne si vous effectuez un seul appel ou abstrait dans une classe request / response.
J'évite les singletons lors de la conception de mes applications. Ils sont typiques pour beaucoup de gens, mais je pense que vous pouvez trouver des solutions plus élégantes ailleurs. Typiquement, ce que je fais est de construire mes entités dans CoreData, puis de mettre mon code REST dans une catégorie NSManagedObject. Si par exemple je voulais créer et poster un nouvel utilisateur, je ferais ceci:
User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];
J'utilise RESTKit pour le mappage d'objets et l'initialise au démarrage. Je trouve que le routage de tous vos appels via un singleton pour être un perte de temps et ajoute beaucoup de passe-partout qui n'est pas nécessaire.
Dans NSManagedObject + Extensions.m:
+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}
Dans NSManagedObject + Networking.m:
- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
[[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
[self handleInputBlocking:blockInput];
}
Pourquoi ajouter des classes d'aide supplémentaires lorsque vous pouvez étendre la fonctionnalité d'une classe de base commune à travers les catégories?
Si vous êtes intéressé par des informations plus détaillées sur ma solution faites le moi savoir. Je suis heureux de partager.
Essayez https://github.com/kevin0571/STNetTaskQueue
Crée des requêtes API dans des classes séparées.
STNetTaskQueue traitera le threading et le delegate / callback.
Extensible pour différents protocoles.
D'un point de vue purement de conception de classe, vous aurez généralement quelque chose comme ceci:
- Vos contrôleurs de vue contrôlant une ou plusieurs vues
-
Classe de modèle de données - Cela dépend vraiment du nombre d'entités distinctes réelles avec lesquelles vous traitez, et de la façon dont elles sont liées.
Par exemple, si vous avez un tableau d'éléments à afficher dans quatre représentations différentes (list, chart, graph etc), vous aurez une classe de modèle de données pour list of articles, un de plus pour un article. La liste de la classe d'élément sera partagée par quatre contrôleurs de vue - tous les enfants d'un contrôleur de barre d'onglets ou d'un contrôleur de navigation.
Les classes de modèle de données seront utiles non seulement pour afficher les données, mais aussi pour les sérialiser, chacune d'elles pouvant exposer son propre format de sérialisation via des méthodes d'exportation JSON / XML / CSV (ou toute autre chose).
-
Il est important de comprendre que vous avez également besoin de API request builder classes cette carte directement avec vos points de terminaison D'API REST. Disons que vous avez une API qui connecte l'utilisateur - donc votre classe login API builder va créer post JSON payload for login api. Dans un autre exemple, une classe de générateur de requête API pour la liste des éléments de catalogue API créera une chaîne de requête GET pour l'api correspondante et déclenchera la requête GET REST.
Ces classes de générateur de requêtes API reçoivent généralement des données des contrôleurs de vue et transmettent également les mêmes données aux contrôleurs de vue pour la mise à jour de L'interface utilisateur / d'autres opérations. Les contrôleurs de vue décideront alors comment mettre à jour les objets du modèle de données avec ces données.
-
Enfin, le cœur du client REST - classe de récupération de données API ce qui est inconscient de toutes sortes de demandes D'API faites par votre application. Cette classe sera plus probablement un singleton, mais comme d'autres l'ont souligné, il ne doit pas nécessairement être un singleton.
Notez que le lien est juste une implémentation typique et ne prend pas en considération des scénarios comme la session, les cookies, etc., mais il suffit de vous lancer sans utiliser de frameworks tiers.
Cette question a déjà beaucoup de réponses excellentes et étendues, mais je pense que je dois le mentionner puisque personne d'autre ne l'a fait.
Alamofire pour Swift. https://github.com/Alamofire/Alamofire
Il est créé par les mêmes personnes que AFNetworking, mais est plus directement conçu avec Swift à l'esprit.