web-dev-qa-db-fra.com

Questions sur VIPER - Architecture propre

J'ai lu sur Clean Architecture de Robert Martin et plus spécifiquement sur VIPER .

Ensuite, je suis tombé sur cet article/article Brigade’s Experience Using an MVC Alternative qui décrit à peu près ce que je fais actuellement.

Après avoir réellement essayé d'implémenter VIPER sur un nouveau projet iOS, je suis tombé sur quelques questions:

  • Le présentateur peut-il interroger des informations dans la vue ou les "informations qui passent" doivent-elles toujours partir de la vue? Par exemple, si la vue a déclenché une action dans le présentateur, mais qu'en fonction des paramètres passés par cette action, le présentateur peut avoir besoin de plus d'informations. Ce que je veux dire, c'est que l'utilisateur a tapé "doneWithState:", si state == "quelque chose", obtenir des informations de la vue pour créer une entité, si state == "autre chose", animer quelque chose dans la vue. Comment dois-je gérer ce type de scénario?
  • Disons qu'un "module" (groupe de composants VIPER) décide de présenter un autre module de façon modale. Qui devrait être chargé de décider si le deuxième module sera présenté de façon modale, le filaire du premier module ou le filaire du deuxième module?
  • Supposons également que la vue du deuxième module soit poussée dans un contrôleur de navigation, comment l'action "retour" doit-elle être gérée? Dois-je définir manuellement un bouton "retour" avec une action dans le contrôleur de vue du deuxième module, qui appelle le présentateur, qui appelle le filaire du deuxième module qui se ferme et indique au filaire du premier module qu'il a été fermé afin que le contrôleur de vue du premier module puisse voulez afficher quelque chose?
  • Les différents modules doivent-ils parler uniquement à travers le wireframe ou également via les délégués entre les présentateurs? Par exemple, si l'application a navigué vers un module différent, mais après cela, l'utilisateur a appuyé sur "annuler" ou "enregistrer" et ce choix doit revenir en arrière et changer quelque chose dans le premier module (peut-être afficher une animation qu'il a été enregistré ou supprimer quelque chose ).
  • Disons qu'une broche a été sélectionnée sur une carte, alors le PinEditViewController est affiché. Lors du retour, la couleur de la broche sélectionnée peut devoir changer en fonction des actions d'utilisation sur PinEditViewController. Qui doit conserver l'état de la broche actuellement sélectionnée, le MapViewController, le MapPresenter ou le MapWireframe pour que je sache, au retour, quelle broche doit changer de couleur?
37
Rodrigo Ruiz

1. Le présentateur peut-il interroger les informations de la vue

Pour répondre à votre satisfaction, nous avons besoin de plus de détails sur le cas particulier. Pourquoi la vue ne peut-elle pas fournir plus d'informations contextuelles directement lors du rappel?

Je vous suggère de passer au présentateur un objet Command afin que le présentateur n'ait pas à savoir quoi faire dans ce cas. Le présentateur peut exécuter la méthode de l'objet, en lui transmettant certaines informations si nécessaire, sans rien savoir de l'état de la vue (et donc en y introduisant un couplage élevé).

  • La vue est dans un état que vous appelez x (opposé à y et z ). Il connaît de toute façon son état.
  • L'utilisateur termine l'action. View informe son délégué (le présentateur) de la fin. Parce qu'il est tellement impliqué, il construit un objet de transfert de données pour contenir toutes les informations habituelles. L'un des attributs de ce DTO est un id<FollowUpCommand> followUpCommand. View crée un XFollowUpCommand (contrairement à YFollowUpCommand et ZFollowUpCommand) et définit ses paramètres en conséquence, puis le place dans le DTO.
  • Le présentateur reçoit l'appel de méthode. Il fait quelque chose avec les données, quel que soit le concret FollowUpCommand. Il exécute ensuite la seule méthode du protocole, followUpCommand.followUp. La mise en œuvre concrète saura quoi faire.

Si vous devez faire un switch-case/if-else sur une propriété, la plupart du temps cela aiderait à modéliser les options comme des objets héritant d'un protocole commun et à passer les objets au lieu de l'état.

2. Module modal

Le module de présentation ou le module présenté doit-il décider s'il est modal? - Le module présenté (le second) devrait décider tant qu'il est conçu pour être utilisé uniquement de manière modale. Mettre la connaissance d'une chose dans la chose elle-même . Si son mode de présentation dépend du contexte, eh bien, le module lui-même ne peut pas décider.

Le filaire du deuxième module recevra un message comme celui-ci:

[secondWireframe presentYourStuffIn:self.viewController]

Le paramètre est l'objet pour lequel la présentation doit avoir lieu. Vous pouvez également transmettre un paramètre asModal si le module est conçu pour être utilisé dans les deux sens. S'il n'y a qu'une seule façon de le faire, mettez ces informations dans le module affecté (celui présenté) lui-même.

Il fera alors quelque chose comme:

- (void)presentYourStuffIn:(UIViewController)viewController {
    // set up module2ViewController

    [self.presenter configureUserInterfaceForPresentation:module2ViewController];

    // Assuming the modal transition is set up in your Storyboard
    [viewController presentViewController:module2ViewController animated:YES completion:nil];

    self.presentingViewController = viewController;
}

Si vous utilisez Storyboard Segues, vous devrez faire les choses un peu différemment.

3. Hiérarchie de navigation

Supposons également que la vue du deuxième module soit poussée dans un contrôleur de navigation, comment l'action "retour" doit-elle être gérée?

Si vous passez "tous VIPER", oui, vous devez passer de la vue à son filaire et vous diriger vers un autre filaire.

Pour retransmettre les données du module présenté ("Second") au module de présentation ("First"), ajoutez SecondDelegate et implémentez-le dans FirstPresenter. Avant que le module présenté n'apparaisse, il envoie un message à SecondDelegate pour notifier le résultat.

"Ne combattez pas le cadre", cependant. Vous pouvez peut-être tirer parti de certaines des subtilités du contrôleur de navigation en sacrifiant la pureté de VIPER. Les séquences sont déjà un pas dans la direction d'un mécanisme de routage. Regardez VTDAddWireframe pour les méthodes UIViewControllerTransitioningDelegate dans un filaire qui introduisent des animations personnalisées. C'est peut-être utile:

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[VTDAddDismissalTransition alloc] init];
}


- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source
{
    return [[VTDAddPresentationTransition alloc] init];
}

J'ai d'abord pensé que vous auriez besoin de conserver une pile de wireframes similaire à la pile de navigation, et que toutes les wireframes du module "actif" sont liées les unes aux autres. Mais ce n'est pas le cas. Les wireframes gèrent le contenu du module, mais la pile de navigation est la seule pile en place représentant quel contrôleur de vue est visible.

4. Flux de messages

Les différents modules doivent-ils parler uniquement à travers le wireframe ou également via les délégués entre les présentateurs?

Si vous envoyez directement à un autre objet du module B un message du présentateur A, que devrait-il se passer alors?

La vue du récepteur n'étant pas visible, une animation ne peut pas démarrer, par exemple. Le présentateur doit encore attendre le filaire/routeur. Il doit donc mettre en file d'attente l'animation jusqu'à ce qu'elle redevienne active. Cela rend le présentateur plus dynamique, ce qui rend son travail plus difficile.

Côté architecture, pensez au rôle que jouent les modules. Dans l'architecture Ports/Adapters, à partir de laquelle Clean Architecture enfonce certains concepts, le problème est plus évident. Par analogie: un ordinateur possède de nombreux ports. Le port USB ne peut pas communiquer avec le port LAN. Chaque flux d'informations doit être acheminé à travers le noyau.

Qu'est-ce qui est au cœur de votre application?

Avez-vous un modèle de domaine? Avez-vous un ensemble de services interrogés à partir de différents modules? Les modules VIPER sont centrés sur la vue. Les modules de partage de choses, comme les mécanismes d'accès aux données, n'appartiennent pas à un module particulier. C'est ce que vous pouvez appeler le noyau. Là, vous devez effectuer des modifications de données. Si un autre module devient visible, il extrait les données modifiées.

À des fins d'animation, cependant, laissez le routeur savoir quoi faire et émettez une commande au présentateur en fonction du changement de module.

Dans l'exemple de code VIPER Todo:

  • La "Liste" est la vue racine.
  • Une vue "Ajouter" est présentée en haut de la vue liste.
  • ListPresenter implémente AddModuleDelegate. Si le module "Ajouter" est terminé, ListPresenter saura, pas son filaire car la vue est déjà dans la pile de navigation .

5. État de conservation

Qui doit conserver l'état de la broche actuellement sélectionnée, le MapViewController, le MapPresenter ou le MapWireframe pour que je sache, au retour, quelle broche doit changer de couleur?

Aucun. Évitez l'état dans vos services de module d'affichage pour réduire les coûts de maintenance de votre code. Au lieu de cela, essayez de déterminer si vous pouvez transmettre une représentation des changements de broches pendant les changements.

Essayez d'atteindre les entités pour obtenir l'état (via le présentateur et l'interacteur et ainsi de suite).

Cela ne signifie pas que vous créez un objet Pin dans votre couche de vue, le passez d'un contrôleur de vue à un contrôleur de vue, modifiez ses propriétés, puis renvoyez-le pour refléter les modifications. Un NSDictionary avec des modifications sérialisées ferait-il l'affaire? Vous pouvez y mettre la nouvelle couleur et la renvoyer du PinEditViewController à son Presenter qui émet un changement dans le MapViewController.

Maintenant, j'ai triché: MapViewController doit avoir un état. Il doit connaître toutes les broches. Ensuite, je vous ai suggéré de passer un dictionnaire de modifications pour que MapViewController sache quoi faire.

Mais comment identifiez-vous la broche affectée?

Chaque broche peut avoir son propre identifiant. Peut-être que cet ID est juste son emplacement sur la carte. C'est peut-être son index dans un tableau de broches. Dans tous les cas, vous avez besoin d'une sorte d'identifiant. Ou vous créez un objet wrapper identifiable qui se maintient sur une broche elle-même pendant la durée de l'opération. (Cela semble trop ridicule pour changer la couleur, cependant.)

Envoi d'événements pour changer d'état

VIPER est très basé sur le service. Il y a beaucoup d'objets pour la plupart sans état liés entre eux pour transmettre des messages et transformer des données. Dans le post de Brigade Engineering, une approche centrée sur les données est également présentée.

Les entités sont dans une couche plutôt mince. À l'opposé du spectre que j'ai en tête se trouve un modèle de domaine . Ce modèle n'est pas nécessaire pour toutes les applications. Modéliser le cœur de votre application de manière similaire peut cependant être bénéfique pour répondre à certaines de vos questions.

Contrairement aux Entités en tant que conteneurs de données dans lesquels tout le monde peut accéder via des "gestionnaires de données", un Domaine protège ses Entités. Un domaine informera également les changements de manière proactive. (Par le biais de NSNotificationCenter, pour commencer. Moins par le biais d'appels de messages directs de type commande.)

Maintenant, cela pourrait également convenir à votre étui Pin:

  • PinEditViewController change la couleur des broches. Il s'agit d'un changement dans un composant d'interface utilisateur.
  • Le changement de composant d'interface utilisateur correspond à un changement de votre modèle sous-jacent. Vous effectuez les modifications via la pile de modules VIPER. (Conservez-vous les couleurs? Sinon, l'entité Pin est toujours de courte durée, mais c'est toujours une entité parce que son identité compte, pas seulement ses valeurs.)
  • Le Pin correspondant a changé de couleur et publie une notification via NSNotificationCenter.
  • Par hasard (c'est-à-dire que Pin ne sait pas), certains Interactor s'abonnent à ces notifications et modifient l'apparence de sa vue.

Bien que cela puisse fonctionner pour votre cas aussi, je pense que lier la modification

16
ctietze

Cette réponse peut être un peu sans rapport, mais je la mets ici pour référence. Le site Clean Swift est une excellente implémentation de " Clean Architecture " d'oncle Bob dans Swift. Le propriétaire l'appelle VIP (il contient quand même les "Entités" et le Routeur/filaire)).

Le site vous propose des modèles XCode. Disons que vous voulez créer une nouvelle scène (il appelle un module VIPER, "scènes"), tout ce que vous faites est File-> new-> sceneTemplate.

Ce modèle crée un lot de 7 fichiers contenant tout le mal de tête du code passe-partout pour votre projet. Il les configure également de sorte qu'ils fonctionnent hors de la boîte. Le site donne une explication assez approfondie de la façon dont tout se combine.

Avec tout le code de la plaque de la chaudière à l'écart, trouver des solutions aux questions que vous avez posées ci-dessus est un peu plus facile. En outre, les modèles permettent une cohérence à tous les niveaux.

[~ # ~] modifier [~ # ~] -> En ce qui concerne les commentaires ci-dessous, voici une explication de la raison pour laquelle je soutiens cette approche -> http://stringerstheory.net/the-clean-er-architecture-for-ios-apps/

Aussi celui-ci -> Le bon, le mauvais et le laid à propos de VIPER dans iOS

La plupart de vos questions trouvent une réponse sur ce post: https://www.ckl.io/blog/best-practices-viper-architecture (exemple de projet inclus). Je vous suggère de porter une attention particulière aux conseils d'initialisation/présentation des modules: c'est à la source Router de le faire.

En ce qui concerne les boutons de retour, vous pouvez use delegates pour déclencher ce message vers le module souhaité. Voici comment je le fais et cela fonctionne très bien (même après avoir inséré des notifications Push).

Et oui, les modules peuvent certainement se parler entre using delegates ainsi que. C'est un must pour les projets plus complexes.

2
Marcelo Gracietti