Je veux comprendre l'approche architecturale de base, abstraite et correcte des applications de mise en réseau dans iOS.
Il y a pas de "la meilleure", ou "la plus correcte" approche pour construire une architecture d'application. Il s'agit d'une très emploi créatif. Vous devez toujours choisir l'architecture la plus simple et la plus extensible, qui sera claire pour tout développeur qui commence à travailler sur votre projet ou pour les autres développeurs de votre équipe, mais je suis d'accord qu'il peut y avoir une "bonne" et une "mauvaise" architecture.
Tu as dit :
recueillir les approches les plus intéressantes de développeurs iOS expérimentés
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 satisfait. Il s'agit d'une approche hybride de celles que vous avez mentionnées ci-dessus, et aussi d'améliorations issues de mes propres efforts de recherche. Je m'intéresse aux problèmes de construction d'approches qui combinent plusieurs modèles et idiomes bien connus. Je pense que beaucoup de Les modèles d'entreprise de Fowler peut être appliquée avec succès aux applications mobiles. Voici une liste des plus intéressantes, que nous pouvons appliquer pour créer une architecture d'application iOS ( à mon avis ) : Couche de service , Unité de travail , Façade distante , Objet de transfert de données , Passerelle , Supertype de couche , Cas particulier , Modèle de domaine . Vous devez toujours concevoir correctement une couche de modèle et ne pas oublier la persistance (elle peut augmenter considérablement les performances de votre application). Vous pouvez utiliser Core Data
pour ça. Mais vous ne devrait pas oublier, que Core Data
n'est pas un ORM ou une base de données, mais un gestionnaire de graphes d'objets dont la persistance est une bonne option. Ainsi, très souvent Core Data
peuvent être trop lourdes pour vos besoins et vous pouvez envisager de nouvelles solutions comme le Realm y Couchbase Lite Vous pouvez également créer votre propre couche légère de mappage et de persistance des objets, basée sur des données SQLite brutes ou des données de base. LevelDB . Je vous conseille également de vous familiariser avec les Conception pilotée par le domaine y CQRS .
Au début, je pense, nous devrait créer une autre couche pour la mise en réseau, car nous ne voulons pas de gros contrôleurs ou de modèles lourds et surchargés. Je ne crois pas en ces fat model, skinny controller
choses. Mais je croit en skinny everything
parce qu'aucune classe ne devrait être grosse, jamais. Toute la mise en réseau peut être généralement abstraite en tant que logique d'entreprise, par conséquent nous devrions avoir une autre couche, où nous pouvons la mettre. Couche de service est ce dont nous avons besoin :
Il encapsule la logique métier de l'application, contrôlant les transactions et coordonnant les réponses dans la mise en œuvre de ses opérations.
Dans notre MVC
domaine Service Layer
est une sorte de médiateur entre le modèle de domaine et les contrôleurs. Il existe une variante assez similaire de cette approche appelée MVCS où un Store
est en fait notre Service
couche. Store
vend des instances de modèles et gère la mise en réseau, la mise en cache, etc. Je tiens à mentionner que vous ne devrait pas écrire toute votre mise en réseau et votre logique commerciale dans votre couche service. Cela peut également être considéré comme une mauvaise conception. Pour plus d'informations, consultez le Anémique y Rich les modèles de domaine. Certaines méthodes de service et la logique commerciale peuvent être traitées dans le modèle, il s'agira donc d'un modèle "riche" (avec comportement).
J'utilise toujours de manière extensive deux bibliothèques : AFNetworking 2.0 y 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
Dans un premier temps, je crée un APIClient
qui est une sous-classe de AFHTTPSessionManager . C'est le cheval de bataille de tous les réseaux de l'application : toutes les classes de service lui délèguent les demandes REST réelles. Il contient toutes les personnalisations du client HTTP, dont j'ai besoin dans cette application particulière : Le pinning SSL, le traitement des erreurs et la création de liens simples. NSError
avec les raisons détaillées de l'échec et les descriptions de tous les API
et les erreurs de connexion (dans ce cas, le contrôleur sera en mesure d'afficher des messages corrects pour l'utilisateur), en définissant 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 exactement, microservices : UserSerivces
, CommonServices
, SecurityServices
, FriendsServices
et ainsi de suite, en fonction de la logique commerciale qu'ils mettent en œuvre. Chacun de ces microservices est une classe distincte. Ensemble, ils forment un Service Layer
. Ces classes contiennent des méthodes pour chaque demande d'API, traitent les modèles de domaine et renvoient toujours un message de type RACSignal
avec le modèle de réponse analysé ou NSError
à l'appelant.
Je tiens à mentionner que si vous avez une logique complexe de sérialisation de modèle - alors créez une autre couche pour cela : quelque chose comme Mappage de données mais plus général, par exemple JSON/XML -> mappeur de modèle. Si vous disposez d'un cache, créez-le également en tant que couche/service distinct (vous ne devez pas mélanger la logique commerciale avec le cache). Pourquoi ? Parce qu'une couche de mise en cache correcte peut être assez complexe avec ses propres problèmes. Les gens mettent en œuvre une logique complexe pour obtenir une mise en cache valide et prévisible, comme par exemple la mise en cache monoidale avec des projections basées sur des profuncteurs. Vous pouvez vous renseigner sur cette magnifique bibliothèque appelée Carlos pour en savoir plus. Et n'oubliez pas que Core Data peut vraiment vous aider à résoudre tous les problèmes de mise en cache et vous permettra d'écrire moins de logique. De plus, si vous avez une logique entre NSManagedObjectContext
et les modèles de demandes de serveur, vous pouvez utiliser Référentiel qui sépare la logique qui récupère les données et les mappe sur le modèle d'entité de la logique métier qui agit sur le modèle. Ainsi, je conseille d'utiliser le pattern Repository même lorsque vous avez une architecture basée sur Core Data. Repository peut abstraire des choses, comme NSFetchRequest
, NSEntityDescription
, NSPredicate
et ainsi de suite jusqu'aux méthodes simples comme get
o put
.
Après toutes ces actions dans la couche Service, l'appelant (contrôleur de vue) peut faire des choses asynchrones complexes avec la réponse : manipulations de signaux, chaînage, mappage, etc. à l'aide de la fonction ReactiveCocoa
ou simplement s'y abonner et afficher les résultats dans la vue. J'injecte avec le Injection de dépendances dans toutes ces classes de service mon APIClient
qui traduira un appel de service particulier en un service correspondant. GET
, POST
, PUT
, DELETE
etc. au point de terminaison REST. Dans ce cas APIClient
est passé implicitement à tous les contrôleurs, vous pouvez le rendre explicite avec un paramètre sur APIClient
les classes de service. Cela peut s'avérer utile si vous souhaitez utiliser différentes personnalisations de la classe de service APIClient
pour des classes de services particulières, mais si, pour certaines raisons, vous ne voulez pas de copies supplémentaires ou si vous êtes sûr de toujours utiliser une instance particulière (sans personnalisation) de l'interface utilisateur de l'interface utilisateur. APIClient
- faites-en un singleton, mais NE FAITES PAS, s'il vous plaît, NE FAITES PAS de classes de service des singletons.
Ensuite, chaque contrôleur de vue, à nouveau 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'IU. Pour l'injection de dépendances, j'aime utiliser BloodMagic ou un cadre plus puissant Typhon . Je n'utilise jamais de singletons, Dieu APIManagerWhatever
ou d'autres mauvaises choses. Parce que si vous appelez votre classe WhateverManager
cela indique que vous ne connaissez pas son utilité et que c'est un mauvais choix de conception . Les singletons sont également un anti-modèle, et en le plus cas (sauf rares) est un mauvais solution. Le singleton ne doit être envisagé que si les trois critères suivants sont satisfaits :
- La propriété de l'instance unique ne peut être raisonnablement attribuée ;
- Une 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 non plus besoin d'un accès global après avoir divisé notre gestionnaire de dieu en services, car maintenant, seuls un ou plusieurs contrôleurs dédiés ont besoin d'un service particulier (par ex. UserProfile
besoins du contrôleur UserServices
et ainsi de suite).
Nous devons toujours respecter S
principe dans SOLIDE et utiliser séparation des préoccupations Ne mettez donc pas toutes vos méthodes de service et vos appels de réseaux dans une seule classe, car c'est de la folie, surtout si vous développez une grande application d'entreprise. C'est pourquoi nous devrions envisager l'injection de dépendances 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 événements) et les paramètres.
Un type de paramètres serait les paramètres ordinaires de "données". 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 de paramètres est celui des paramètres de "service". Ce sont des classes qui encapsulent la logique métier, permettent de communiquer avec des systèmes externes, fournissent un accès aux données.
Voici par exemple un flux de travail général de mon architecture. Supposons que nous ayons un FriendsViewController
qui affiche la liste des amis de l'utilisateur et nous avons une option pour les supprimer. Je crée une méthode dans mon FriendsServices
classe appelée :
- (RACSignal *)removeFriend:(Friend * const)friend
donde Friend
est un objet de modèle/domaine (ou peut être simplement un User
s'ils ont des attributs similaires). En dessous, cette méthode analyse Friend
a NSDictionary
de paramètres JSON friend_id
, name
, surname
, friend_request_id
et ainsi de suite. J'utilise toujours Manteau pour ce genre de gabarit et pour ma couche modèle (analyse syntaxique en amont et en aval, gestion des hiérarchies d'objets imbriqués en JSON, etc.) Après l'analyse, il appelle APIClient
DELETE
pour effectuer une demande REST réelle et renvoie la méthode Response
en RACSignal
à l'appelant ( FriendsViewController
dans notre cas) pour afficher un message approprié pour l'utilisateur ou autre.
Si notre application est très grande, nous devons séparer notre logique de manière encore plus claire. Par exemple, il n'est pas toujours bon de mélanger la logique du `Repository` ou du modèle avec celle du `Service`. Quand j'ai décrit mon approche, j'ai dit que la méthode `removeFriend` devrait être dans la couche `Service`, mais si nous sommes plus pédants, nous pouvons remarquer qu'elle appartient plutôt au `Repository`. Rappelons-nous ce qu'est un Repository. Eric Evans en a donné une description précise dans son livre [DDD] :
Un référentiel représente tous les objets d'un certain type comme un ensemble conceptuel. Il agit comme une collection, mais avec une capacité d'interrogation plus élaborée.
Donc, un Repository
est essentiellement une façade qui utilise la sémantique du style Collection (Add, Update, Remove) 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 le Repository
car la sémantique des collections est assez claire ici. Et du code comme :
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
est définitivement une logique d'entreprise, parce qu'elle va au-delà de l'élémentaire CRUD
et de connecter deux objets de domaine ( Friend
y Request
), c'est pourquoi il doit être placé dans la section Service
couche. Je veux aussi remarquer : ne pas créer d'abstractions inutiles . Utilisez toutes ces approches à bon escient. Parce que si vous submergez votre application avec des abstractions, cela va augmentation de sa complexité accidentelle, et la complexité cause plus de problèmes dans les systèmes logiciels que tout autre chose
Je vous décris un "vieil" exemple Objective-C mais cette approche peut être très facilement adaptée au langage Swift avec beaucoup plus d'améliorations, car il a plus de fonctionnalités utiles et de sucre fonctionnel. Je vous recommande vivement d'utiliser cette bibliothèque : Moya . Il vous permet de créer une APIClient
couche (notre cheval de bataille, comme vous vous en souvenez). Maintenant, notre APIClient
Le fournisseur sera un type de valeur (enum) avec des extensions conformes aux protocoles et tirant parti du filtrage par déstructuration. Les enums Swift + le pattern matching nous permettent de créer des types de données algébriques comme dans la programmation fonctionnelle classique. Nos microservices utiliseront cette APIClient
comme dans l'approche Objective-C habituelle. Pour la couche modèle, au lieu de Mantle
vous pouvez utiliser Bibliothèque ObjectMapper ou j'aime utiliser un système plus élégant et fonctionnel Argo bibliothèque.
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 d'autres améliorations, bien sûr. Je vous conseille d'apprendre la programmation fonctionnelle, parce que vous pouvez en tirer un grand profit, mais n'allez pas trop loin avec elle aussi. Éliminer les états mutables globaux, partagés et excessifs, créer un système de gestion de l'information, etc. modèle de domaine immuable ou la création de fonctions pures sans effets secondaires externes est, en général, une bonne pratique, et les nouvelles Swift
La langue encourage cela. B mauvais l'idée, car autre les développeurs liront et soutiendront votre code, et ils peuvent être frustrés ou effrayés par la prismatic profunctors
et ce genre de choses dans votre modèle immuable. La même chose avec le ReactiveCocoa
Ne le faites pas. RACify
votre code trop car il peut devenir très vite illisible, surtout pour les débutants. Utilisez-le quand il peut vraiment simplifier vos objectifs et votre logique.
Donc, lisez beaucoup, mélangez, expérimentez, et essayez de tirer le meilleur de différentes approches architecturales. C'est le meilleur conseil que je puisse vous donner.
16 votes
N'est-ce pas une question de "liste de courses" ? Je viens d'avoir une question qui a été rejetée et fermée parce qu'il a été dit que les questions du type "quel est le meilleur" suscitent trop de débats non constructifs. Qu'est-ce qui fait de cette question de liste de courses une bonne question digne de votes positifs et d'une prime alors que d'autres sont fermées ?
1 votes
En général, la logique de réseau est intégrée au contrôleur, qui modifie un objet de modèle et notifie les délégués ou les observateurs.
1 votes
Des questions et des réponses très intéressantes. Après 4 ans de codage iOS, et en essayant de trouver la plus belle façon d'ajouter une couche réseau à l'application. Quelle classe devrait avoir la responsabilité de gérer une requête réseau ? Les réponses ci-dessous sont vraiment pertinentes. Merci
0 votes
@JoeBlow ce n'est pas vrai. L'industrie des applications mobiles repose encore beaucoup sur les communications serveur-client.