web-dev-qa-db-fra.com

Meilleures approches architecturales pour la création d'applications réseau iOS (clients REST)

Je suis un développeur iOS expérimenté et cette question m’intéresse beaucoup. J'ai vu beaucoup de ressources et de matériaux différents sur ce sujet, mais je suis toujours perplexe. Quelle est la meilleure architecture pour une application en réseau iOS? Je veux dire un cadre abstrait de base, des modèles, qui conviendront à toutes les applications de réseau, qu’il s’agisse d’une petite application ne disposant que de quelques requêtes de serveur ou d’un client REST complexe. Apple recommande d'utiliser MVC comme approche architecturale de base pour toutes les applications iOS, mais ni MVC ni les modèles MVVM plus modernes n'expliquent où placer le code de 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 demandes API et autres logiques de mise en réseau, ce qui peut être très complexe en perspective? Après quelques recherches, j'ai trouvé deux approches de base pour cela. Ici il a été recommandé de créer une classe distincte pour chaque requête réseau adressée au service Web API (telle que la classe LoginRequest ou la classe PostCommentRequest, etc.) héritant toutes de la classe de base abstraite de la demande de base AbstractBaseRequest et en plus de créer un gestionnaire de réseau global qui encapsule le code de réseau courant et d’autres préférences (il peut s’agir de la personnalisation AFNetworking ou du réglage RestKit, si nous avons des mappages et persistance d’objets complexes, ou même une implémentation de communication réseau propre avec une API standard). Mais cette approche me semble être une surcharge. Une autre approche consiste à avoir une classe de répartiteur ou de gestionnaire singleton API comme dans la première approche, mais non pour créer des classes pour chaque requête et pour encapsuler chaque requête en tant que méthode publique d'instance de cette classe de gestionnaire telle que: méthodes fetchContacts, loginUser, etc. Alors, quelle est la meilleure et correcte méthode? Existe-t-il d'autres approches intéressantes que je ne connais pas encore?

Et devrais-je créer une autre couche pour tous ces éléments de réseau, comme la couche Service ou NetworkProvider ou tout ce qui se trouve au-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 les clients Facebook ou les clients LinkedIn font face à une complexité de plus en plus grande de la logique de mise en 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 proposées par des développeurs iOS expérimentés.. La meilleure approche suggérée sera marquée comme acceptée et récompensée par une prime de réputation, les autres seront levées. C'est principalement une question théorique et de recherche. Je souhaite comprendre une approche architecturale de base, abstraite et correcte pour les applications réseau dans iOS. J'espère une explication détaillée de la part de développeurs expérimentés.

309

_I want to understand basic, abstract and correct architectural approach for networking applications in iOS_: il y a no "la meilleure" ou "la plus correcte" pour la construction d'une architecture d'application. Il s’agit d’un travail créatif very . 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 conviens 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. C’est une approche hybride de celles que vous avez mentionnées ci-dessus, mais également des améliorations apportées par mes propres efforts de recherche. Je suis intéressé par les problèmes d’approches de construction, 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 , - nité de travail , façade distante , objet de transfert de données , passerelle , super-type de couche , - Cas particulier , modèle de domaine . Vous devez toujours concevoir correctement une couche de modèle et ne jamais oublier la persistance (elle peut augmenter considérablement les performances de votre application). Vous pouvez utiliser _Core Data_ pour cela. Mais vous ne devriez pas == oubliez que _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. Donc, très souvent, _Core Data_ peut être trop lourd pour vos besoins et vous pouvez chercher de nouvelles solutions telles que Realm et Couchbase Lite , ou créer votre propre objet léger. couche de mappage/persistance, basée sur du SQLite brut ou LevelDB . Aussi, je vous conseille de vous familiariser avec les Domain Driven Design et CQRS .

Au début, je pense devrait == créer une autre couche pour la mise en réseau, car nous ne voulons pas de gros contrôleurs ni de modèles lourds et surchargés. Je ne crois pas en ces _fat model, skinny controller_ choses. Mais je pense à l'approche _skinny everything_, car aucune classe ne doit être grosse, jamais. Tous les réseaux peuvent être généralement résumés en tant que logique métier. Par conséquent, nous devrions avoir une autre couche, où nous pouvons le mettre. couche de service c'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 domaine _Service Layer_, il s’agit d’un médiateur entre le modèle de domaine et les contrôleurs. Il existe une variante assez similaire de cette approche appelée MVCSStore est en réalité 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 devez pas écrire toute votre logique de mise en réseau et de gestion dans votre couche service. Cela peut aussi être considéré comme une mauvaise conception. Pour plus d'informations, consultez les modèles de domaine Anemic et Rich . Certaines méthodes de service et certaines logiques d’entreprise peuvent être gérées dans le modèle, ce sera donc un modèle "riche" (avec comportement).

J'utilise toujours intensément deux bibliothèques: AFNetworking 2. 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 qui contient une logique d’interface utilisateur complexe.

ARCHITECTURE

Au début, je crée une classe générale APIClient, 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 services délèguent des éléments réels REST aux requêtes correspondantes. Il contient toutes les personnalisations du client HTTP, dont j'ai besoin dans une 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 toutes les erreurs API et de connexion (dans ce cas, le contrôleur pourra afficher les informations correctes). messages pour l’utilisateur), paramétrer les sérialiseurs de demande et de réponse, les en-têtes http et d’autres éléments liés au réseau. Ensuite, je divise logiquement toutes les demandes d'API en sous-services ou, plus correctement, microservices : UserSerivces, CommonServices, SecurityServices, FriendsServices et ainsi de suite, conformément à la logique métier implémentée. Chacun de ces microservices est une classe séparée. Ensemble, ils forment un _Service Layer_. Ces classes contiennent des méthodes pour chaque demande d'API, traitent des modèles de domaine et renvoient toujours un RACSignal avec le modèle de réponse analysé ou NSError à l'appelant.

Je tiens à mentionner que si vous avez une logique de sérialisation de modèle complexe, créez une autre couche: quelque chose comme Data Mapper mais, par exemple, de manière plus générale. JSON/XML -> Modèle mappeur. Si vous avez un cache: créez-le également en tant que couche/service distinct (vous ne devez pas mélanger la logique métier à la mise en cache). Pourquoi? Parce que la bonne couche de mise en cache peut être assez complexe avec ses propres pièges. Les personnes implémentent une logique complexe pour obtenir une mise en cache valide et prévisible, comme par exemple. mise en cache monoïdale avec projections basées sur les 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 même, si vous avez une certaine logique entre les modèles NSManagedObjectContext et les demandes de serveur, vous pouvez utiliser le motif Repository , qui sépare la logique qui extrait 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. Le référentiel peut faire abstraction d'éléments tels que NSFetchRequest, NSEntityDescription, NSPredicate et ainsi de suite pour des méthodes simples telles que get ou put.

Après toutes ces actions dans la couche Service, l’appelant (contrôleur de vue) peut effectuer des opérations asynchrones complexes avec la réponse: manipulations de signal, chaînage, mappage, etc. à l’aide des primitives ReactiveCocoa, ou simplement s’y abonner et afficher les résultats dans vue. J'injecte avec le injection de dépendance dans toutes ces classes de service ma demande APIClient, qui traduira un appel de service particulier en demande correspondante GET, POST, PUT, DELETE, etc., à REST point final. Dans ce cas, APIClient est transmis implicitement à tous les contrôleurs. Vous pouvez le rendre explicite avec un service paramétré sur APIClient. Cela peut sembler judicieux si vous souhaitez utiliser différentes personnalisations de APIClient pour des classes de service particulières, mais si, pour certaines raisons, vous ne souhaitez pas de copies supplémentaires ou si vous êtes sûr de toujours utiliser une instance particulière (sans personnalisations) de APIClient - faites-en un singleton, mais NE PAS FAIRE, veuillez NE PAS FAIRE de classes de service en 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 plus puissant Typhoon . Je n'utilise jamais de singletons, de classe APIManagerWhatever de Dieu ou d'autres mauvaises choses. Parce que si vous appelez votre classe WhateverManager, cela indique que vous ne connaissez pas son objectif et qu’il s’agit d’un mauvais choix de conception . Singletons est également un anti-modèle, et dans la plupart des cas (sauf rares) est un faux Solution. Singleton ne devrait être envisagé que si les trois critères suivants sont remplis:

  1. La propriété de l'instance unique ne peut être raisonnablement attribuée.
  2. Une initialisation lente est souhaitable.
  3. L'accès global n'est pas prévu ailleurs.

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 dieu manager en services, car à présent un ou plusieurs contrôleurs dédiés ont besoin d'un service particulier (par exemple, le contrôleur UserProfile a besoin de UserServices et ainsi de suite. sur).

Nous devons toujours respecter le principe S dans SOLID et utiliser séparation des problèmes , ne mettez donc pas toutes vos méthodes de service et vos appels réseau dans une seule et même classe, parce que c'est dingue, surtout si vous développez une application de grande entreprise. C'est pourquoi nous devrions envisager une approche fondée sur l'injection de dépendance et les 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 événements) et les paramètres.

Un type de paramètres serait les paramètres "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 la communication avec des systèmes externes, fournissent un accès aux données.

Voici un flux de travail général de mon architecture par exemple. Supposons que nous ayons un FriendsViewController, qui affiche la liste des amis de l'utilisateur et une option pour le supprimer. Je crée une méthode dans ma classe FriendsServices appelée:

_- (RACSignal *)removeFriend:(Friend * const)friend
_

Friend est un objet modèle/domaine (ou ce peut être simplement un objet User s'ils ont des attributs similaires). Sous le capot, cette méthode analyse Friend en 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 modèle standard et pour ma couche de modèle (analyse en arrière et en aval, gestion des hiérarchies d'objets imbriquées dans JSON, etc.). Après l'analyse, la méthode APIClientDELETE est créée pour créer une demande réelle REST et renvoyer Response dans RACSignal à l'appelant (FriendsViewController dans notre cas) pour afficher le message approprié pour l'utilisateur ou autre.

Si notre application est très volumineuse, nous devons séparer encore plus notre logique. Par exemple. ce n'est pas toujours il est bon de mélanger Repository ou de modéliser la logique avec Service un. Lorsque j’ai décrit mon approche, j’avais dit que la méthode removeFriend devrait figurer dans la couche Service, mais si nous serions plus pédants, nous pourrions remarquer qu’elle appartenait 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 en tant qu'ensemble conceptuel. Il agit comme une collection, sauf avec une capacité d'interrogation plus élaborée.

Donc, Repository est essentiellement une façade qui utilise la sémantique du style Collection (Ajouter, Mettre à jour, Supprimer) pour fournir un accès aux données/objets. C'est pourquoi, lorsque vous avez quelque chose comme: getFriendsList, getUserGroups, removeFriend, vous pouvez le placer dans Repository, car la sémantique de type collection est assez claire ici. Et un code comme:

_- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
_

est certainement une logique métier, car elle va au-delà des opérations de base CRUD et connecte deux objets de domaine (Friend et Request). C’est pourquoi elle devrait être placée dans la couche Service. Aussi, je veux noter: ne crée pas d'abstractions inutiles . Utilisez toutes ces approches à bon escient. Parce que si vous allez submerger votre application d'abstractions, ceci augmente sa complexité accidentelle et sa complexité cause plus de problèmes dans les systèmes logiciels que tout autre chose

Je vous décris un "ancien" exemple Objective-C mais cette approche peut être très facilement adaptée pour Swift langue 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 une couche APIClient plus élégante (notre cheval de bataille à votre souvenir). Notre fournisseur APIClient sera désormais un type de valeur (enum) avec des extensions conformes aux protocoles et utilisant la correspondance de modèle 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 habituelle. Pour la couche de modèle au lieu de Mantle, vous pouvez utiliser bibliothèque ObjectMapper ou j'aime utiliser une bibliothèque plus élégante et fonctionnelle Argo .

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 beaucoup plus d'améliorations, bien sûr. Je vous conseille d'apprendre la programmation fonctionnelle, car vous pouvez en tirer beaucoup de bénéfices, mais n'allez pas trop loin avec cela. Éliminer un état mutable global excessif, partagé et global, créer un modèle de domaine immuable ou créer des fonctions pures sans effets secondaires externes est, en général, une bonne pratique, et le nouveau langage Swift l'encourage. Mais souvenez-vous toujours que, surchargeant votre code avec des schémas fonctionnels purs et lourds, les approches catégorielles sont une idée bad , car autre développeurs liront et soutiendront votre code, et ils peuvent être frustrés ou effrayés par le _prismatic profunctors_ et ce genre de choses dans votre modèle immuable. La même chose avec ReactiveCocoa: ne RACify pas votre code trop , car il peut devenir illisible très rapidement, en particulier pour les débutants. Utilisez-le lorsqu'il peut réellement 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.

316

Selon l'objectif de cette question, j'aimerais décrire notre approche de l'architecture.

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 =; et Programmation Réactive Fonctionnelle paradigme.

Nous pouvons découper une application client typique en couches logiques suivantes:

  • Assemblée
  • Modèle
  • Prestations de service
  • Espace de rangement
  • Gestionnaires
  • Coordinateurs
  • UI
  • Infrastructure

La couche d'assemblage est un bootstrap point de notre application. Il contient un conteneur d’injection de dépendance 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.). Pour cela, nous utilisons la bibliothèque Typhoon .

La couche modèle contient les classes de modèles de domaine, les validations et les mappages. Nous utilisons la bibliothèque Mantle pour mapper nos modèles: elle prend en charge la sérialisation/désérialisation dans les modèles JSON et NSManagedObject. Pour la validation et la représentation sous forme 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 afin d'envoyer ou de recevoir des données représentées dans notre modèle de domaine. Nous avons donc généralement des services de communication avec les API de serveur (par entité), les services de messagerie (tels que PubNub ), les services de stockage (tels que Amazon S3), etc. SDK) ou mettre en œuvre leur propre logique de communication. Pour les réseaux généraux, nous utilisons la bibliothèque AFNetworking .

La couche de stockage a pour objet d’organiser le stockage local des données sur le périphérique. Nous utilisons Core Data ou Realm pour cela (les deux ont des avantages et des inconvénients, la décision d'utilisation est basée sur des spécifications concrètes). Pour la configuration des données de base, nous utilisons MDMCoreData bibliothèque et un groupe 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.

La couche de gestionnaires est un endroit où vivent nos abstractions/wrappers.

Dans un rôle de gestionnaire pourrait être:

Ainsi, le rôle de gestionnaire peut être n'importe quel objet qui implémente la logique d’un aspect ou d’une préoccupation particulière nécessaire au fonctionnement d’une application.

Nous essayons d'éviter les singletons, mais cette couche est un endroit où ils vivent s'ils en ont besoin.

La couche Coordinators fournit des objets qui dépendent d'objets provenant d'autres couches (Service, Stockage, Modèle) afin de combiner leur logique en une seule séquence de travail nécessaire à certaines module (fonctionnalité, écran, user story ou expérience utilisateur). Il enchaîne généralement les opérations asynchrones et sait comment réagir à leurs cas de réussite et d’échec. A titre d'exemple, vous pouvez imaginer une fonction de messagerie et l'objet MessagingCoordinator correspondant. La gestion de l'envoi d'un message peut ressembler à ceci:

  1. Valider le message (couche modèle)
  2. Enregistrer le message localement (stockage des messages)
  3. Télécharger le message en pièce jointe (service Amazon s3)
  4. Mettre à jour le statut des messages et les URL des pièces jointes et enregistrer le message localement (stockage des messages)
  5. Sérialiser le message au format JSON (couche modèle)
  6. Publier un message sur PubNub (service PubNub)
  7. Mettre à jour le statut et les attributs des messages et les sauvegarder localement (stockage des messages)

A chacune des étapes ci-dessus, une erreur est traitée en conséquence.

La couche d'interface utilisateur comprend les sous-couches suivantes:

  1. ViewModels
  2. ViewControllers
  3. Des vues

Afin d'éviter les contrôleurs de vue massive, nous utilisons un modèle MVVM et implémentons la logique nécessaire à la présentation de l'interface utilisateur dans ViewModels. Un modèle de vue a généralement des coordinateurs et des gestionnaires en tant que dépendances. ViewModels utilisés par ViewControllers et certains types de vues (par exemple, les cellules de vue de tableau). Le lien entre ViewControllers et ViewModels est le modèle de liaison de données et de commande. Pour pouvoir utiliser cette colle, nous utilisons la bibliothèque --- (ReactiveCocoa .

Nous utilisons également ReactiveCocoa et son concept RACSignal comme interface et type de valeur renvoyée de tous les coordonnateurs, services et 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 de mettre en œuvre 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 les types d'applications que nous construisons habituellement. Mais vous devez comprendre que c’est juste une approche subjective qui devrait être adaptée/modifiée pour les besoins concrets 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

30
Alex Petropavlovsky

Parce que toutes les applications iOS sont différentes, je pense qu'il y a différentes approches à considérer ici, 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 appelée APICommunicator) et chaque méthode d'instance est un appel d'API. Et il existe 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 parfaitement les réponses réseau asynchrones (sendNext :, sendError :, etc.).
Cette méthode appelle l'API, récupère les résultats et les envoie via RAC au format "brut" (comme NSArray renvoyé par AFNetworking).
Ensuite, une méthode comme getStuffList: qui appelait 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 à un à l'appelant (getStuffList: et des méthodes similaires renvoient également un signal auquel le contrôleur peut souscrire).
Le contrôleur souscrit reçoit les objets par le bloc subscribeNext: et les traite.

J’ai essayé de nombreuses manières dans différentes applications, mais celle-ci fonctionnait au mieux. C’est pourquoi je l’utilise depuis peu dans plusieurs applications. Elle convient aux petits comme aux grands projets. doit être modifié.
Espérons que cela aide, j'aimerais entendre l'opinion des autres sur mon approche et peut-être comment d'autres pensent que cela pourrait peut-être être amélioré.

18
Rickye

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.

J'ajoute seulement du code pour configurer le mappage automatiquement. Je définis la classe de base pour mes modèles (pas de protocole à cause 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:

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

RelationObjet.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 d'implémentation MappableEntry:

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

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

Passons maintenant à l’emballage des demandes:

J'ai un fichier d'en-tête avec la définition des 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 la 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 que vous devez faire dans le code, initialisez simplement un 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 de le définir une fois et de l'utiliser pour différents projets. Si cela vous intéresse, mb, je pourrais passer un peu de temps et lui proposer une solution universelle quelque part sur GitHub et CocoaPods.

8
Andrew Cherkashyn

Selon moi, toute architecture logicielle est dictée par les besoins. S'il s'agit d'un apprentissage ou à des fins personnelles, déterminez l'objectif principal et indiquez-le dans l'architecture. S'il s'agit d'un travail à embaucher, le besoin commercial est primordial. Le truc 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 affaire et beaucoup d'entre elles ne sont pas utiles, mais vous ne pouvez pas toujours le dire à l'avance. Concentrez-vous sur le besoin et soyez prêt à abandonner les mauvais choix si vous le pouvez.

Par exemple, j'ai récemment réalisé un prototype rapide d'application de partage de photos pour une entreprise locale. Comme le besoin commercial était de faire quelque chose de rapide et de sale, l’architecture consistait en un code iOS permettant de faire apparaître une caméra et un code réseau associé à un bouton d’envoi qui téléchargeait l’image sur un magasin S3 et écrivait sur 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 présentait de nombreuses failles et bloquait parfois l'interface utilisateur, mais ce serait un gaspillage d'en faire plus pour un prototype et cela leur permettrait de se déployer auprès de leur personnel et de générer facilement des milliers d'images de test sans performances ni évolutivité. préoccupations. Architecture de merde, mais elle répond parfaitement aux besoins et coûte parfaitement.

Un autre projet impliquait la mise en place 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 je devais écrire tellement de code personnalisé pour RestKit afin de traiter du JSON idiosyncratique que j'aurais pu le faire plus rapidement en écrivant mon propre JSON dans les transformations CoreData. Cependant, le client souhaitait apporter cette application en interne et j’ai eu l’impression que RestKit serait similaire aux frameworks qu’ils utilisaient sur d’autres plates-formes. J'attends de voir si c'était une bonne décision.

Encore une fois, le problème pour moi est de mettre l’accent sur le besoin et de le laisser déterminer l’architecture. J'essaie comme un diable d'éviter d'utiliser des packages tiers, car ils génèrent des coûts qui n'apparaissent que lorsque l'application est sur le terrain depuis un certain temps. J'essaie d'éviter de créer des hiérarchies de classes, car elles rapportent rarement. Si je peux écrire quelque chose dans un délai raisonnable au lieu d'adopter un package qui ne convient pas parfaitement, je le fais. Mon code est bien structuré pour le débogage et commenté de manière appropriée, mais les packages tiers le sont rarement. Cela dit, je trouve AF Networking trop utile pour être ignoré et bien structuré, bien commenté et entretenu, et je l’utilise beaucoup! RestKit couvre un grand nombre de cas courants, mais je sens que je me suis battu quand je l'utilise, et la plupart des sources de données que je rencontre sont pleines d'exagérations et de problèmes qui sont mieux gérés avec du code personnalisé. Dans mes dernières applications, je viens d'utiliser les convertisseurs JSON intégrés et d'écrire quelques méthodes utilitaires.

Un modèle que j'utilise toujours est d'obtenir les appels réseau du thread principal. Les 4-5 dernières applications que j'ai réalisées ont configuré une tâche de minuterie en arrière-plan à l'aide de dispatch_source_create, qui se réveille de temps en temps et effectue les tâches réseau en fonction des besoins. Vous devez effectuer un travail sur la sécurité des threads et vous assurer que le code de modification de l'interface utilisateur est envoyé au thread principal. Il est également utile de procéder à l’installation/à l’initialisation de manière à ce que l’utilisateur ne se sente pas surchargé ou retardé. Jusqu'à présent, cela a plutôt bien fonctionné. Je suggère de regarder dans ces choses.

Enfin, je pense qu’au fur et à mesure que nous travaillons 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 devais suivre des schémas et des conceptions que d'autres personnes considèrent comme obligatoires. Si je travaille dans un contexte où cela fait partie de la religion locale, euh, je veux dire des meilleures pratiques d'ingénierie du ministère, alors je suis les coutumes à la lettre, c'est pour cela qu'elles me paient. Mais je trouve rarement que la solution optimale consiste à suivre des motifs et des modèles plus anciens. J'essaie toujours de regarder la solution à travers le prisme des besoins de l'entreprise et de construire l'architecture qui convient et de garder les choses aussi simples que possible. Quand j'ai l'impression qu'il n'y en a pas assez, mais que tout fonctionne correctement, je suis sur la bonne voie.

7
Fran K.

J'utilise l'approche que j'ai obtenue à partir d'ici: https://github.com/Constantine-Fry/Foursquare-API-v2 . J'ai réécrit cette bibliothèque dans Swift et vous pouvez voir l'approche architecturale à partir de ces parties du 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}}
}

En gros, il existe une sous-classe NSOperation qui crée NSURLRequest, analyse la réponse JSON et ajoute le bloc de rappel avec le résultat à la file d'attente. La classe d'API principale construit NSURLRequest, initialise cette sous-classe NSOperation et l'ajoute à la file d'attente.

4
bzz

Nous utilisons quelques approches en fonction de la situation. 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 qu'il existe de nombreuses catégories supplémentaires pour UIKit qui vous permettent, par exemple, de définir une image à partir de une url. Dans une application complexe avec beaucoup d'appels, nous résumons parfois cela à une méthode de commodité qui ressemble à celle-ci:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

Il existe quelques situations où AFNetworking ne convient pas, par exemple si vous créez un cadre ou un autre composant de bibliothèque, car AFNetworking peut déjà se trouver dans une autre base de code. Dans ce cas, vous utiliseriez NSMutableURLRequest en ligne si vous passez un seul appel ou abstraite dans une classe de requête/réponse.

3
Martin

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. En règle générale, je crée mes entités dans CoreData, puis place mon code REST dans une catégorie NSManagedObject. Si par exemple je voulais créer et POST un nouvel utilisateur, je procéderais ainsi:

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

J'utilise RESTKit pour le mappage d'objet et l'initialise au démarrage. Je trouve que l'acheminement de tous vos appels via un singleton est une perte de temps et ajoute beaucoup de passe-partout qui ne sont pas nécessaires.

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'assistance supplémentaires lorsque vous pouvez étendre les fonctionnalités d'une classe de base commune à travers des 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.

1
Sandy Chapman

Cette question contient déjà beaucoup de réponses excellentes et détaillées, mais j’ai le sentiment que je dois la mentionner, personne ne le faisant.

Alamofire pour Swift. https://github.com/Alamofire/Alamofire

Il a été créé par les mêmes personnes que AFNetworking, mais est plus directement conçu dans l’esprit Swift.

0

D'un point de vue purement conceptuel, 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 manière dont elles sont liées.

    Par exemple, si vous avez un tableau d'éléments à afficher dans quatre représentations différentes (liste, graphique, graphique, etc.), vous aurez une classe de modèle de données pour la liste d'éléments, une autre pour un élément. La liste de la classe d'éléments sera partagée par quatre contrôleurs de vue - tous les enfants d'un contrôleur de barre de tabulation ou d'un contrôleur de navigation.

    Les classes de modèles de données s'avéreront utiles non seulement pour l'affichage des données, mais également pour leur sérialisation, chacune pouvant exposer son propre format de sérialisation via des méthodes d'exportation JSON/XML/CSV (ou toute autre méthode).

  • Il est important de comprendre que vous avez également besoin des classes de générateur de demande d'API mappées directement avec vos points de terminaison REST API. Supposons que vous ayez une API qui enregistre l'utilisateur. Ainsi, votre classe de générateur d'API de connexion créera POST JSON payload pour l'API de connexion. Dans un autre exemple, une classe de générateur de requête d'API pour la liste d'éléments de catalogue, l'API créera une chaîne de requête GET pour l'API correspondante et déclenchera la requête REST GET.

    Ces classes de constructeur de requêtes d'API recevront généralement des données des contrôleurs de vue et transmettront 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 ensuite comment mettre à jour les objets du modèle de données avec ces données.

  • Enfin, le coeur du client REST - classe de récupérateur de données d'API = qui est inconscient de toutes sortes de requêtes API faites par votre application. Cette classe sera probablement un singleton, mais comme d'autres l'ont souligné, il n'est pas nécessaire que ce soit un singleton.

    Notez que le lien est simplement une implémentation typique et ne prend pas en compte des scénarios tels que la session, les cookies, etc., mais il suffit de vous lancer sans utiliser de frameworks tiers.

0
Nirav Bhatt

Essayez https://github.com/kevin0571/STNetTaskQueue

Créez des demandes d'API dans des classes séparées.

STNetTaskQueue traitera du threading et du délégué/rappel.

Extensible pour différents protocoles.

0
Kevin