Pourquoi VIPER est un mauvais choix pour votre prochaine application

Il y a beaucoup de battage autour de VIPER au cours de la dernière année, et tout le monde est tellement inspiré par lui. La plupart de ces articles sont biaisés à son sujet et essaient de démontrer à quel point il est cool. Ce n’est pas le cas. Il présente au moins la même quantité de problèmes, si ce n’est plus, que les autres modèles d’architecture. Dans cet article, je veux expliquer pourquoi VIPER n’est pas aussi bon qu’il est annoncé et ne convient pas à la plupart de vos applications.

Certains articles sur la comparaison des architectures prétendent généralement que VIPER est un modèle entièrement différent de toutes les architectures basées sur MVC. Cette affirmation n’est pas vraie : c’est juste un MVC normal, où nous séparons le contrôleur en deux parties : l’interacteur et le présentateur. La vue reste la même, mais le modèle est renommé en entité. Le routeur mérite des mots particuliers : Oui, c’est vrai, d’autres architectures ne mettent pas en avant cette partie dans leur abréviation, mais elle existe toujours, de manière implicite (oui, quand vous appelez pushViewController vous écrivez un simple routeur) ou plus explicite (par exemple les FlowCoordinators).

Maintenant je veux parler des avantages que VIPER vous offre. Je vais utiliser ce livre comme une référence complète sur VIPER. Commençons par le deuxième objectif, se conformer au SRP (Single Responsibility Principle). C’est un peu rude, mais quel esprit étrange peut appeler cela un avantage ? Vous êtes payé pour résoudre des tâches, pas pour vous conformer à un mot à la mode. Oui, vous utilisez toujours TDD, BDD, les tests unitaires, Realm ou SQLite, l’injection de dépendance, et tout ce dont vous pouvez vous souvenir, mais vous l’utilisez pour résoudre le problème du client, pas pour simplement l’utiliser.

Un autre objectif est beaucoup plus intéressant. La testabilité est une préoccupation très importante. Elle mérite un article à part entière puisque beaucoup de gens en parlent, mais seuls quelques-uns testent vraiment leurs apps et encore moins le font correctement.

L’une des principales raisons de cela est le manque de bons exemples. Il y a beaucoup d’articles sur la création d’un test unitaire avec assert 2 + 2 == 4 mais pas d’exemples réels (btw, Artsy fait un excellent travail en open sourcing leurs apps, vous devriez jeter un coup d’œil à leurs projets).

VIPER suggère de séparer toute la logique dans un tas de petites classes avec des responsabilités séparées. Cela peut faciliter les tests, mais pas toujours. Oui, écrire un unit-test pour une classe simple est facile, mais la plupart de ces tests ne testent rien. Par exemple, regardons la plupart des méthodes d’un présentateur, ils sont juste un proxy entre une vue et d’autres composants. Vous pouvez écrire des tests pour ce proxy, cela augmentera la couverture de test de votre code, mais ces tests sont inutiles. Vous avez également un effet secondaire : vous devez mettre à jour ces tests inutiles après chaque modification du code principal.

La bonne approche des tests devrait inclure le test de l’interacteur et du présentateur en même temps, car ces deux parties sont profondément intégrées. En outre, parce que nous séparons entre deux classes, nous avons besoin de beaucoup plus de tests par rapport à un cas où nous avons juste une classe. C’est une simple сombinatorique : la classe A a 4 états possibles, la classe B a 6 états possibles, donc une combinaison de A et B a 20 états, et vous devriez tout tester.

La bonne approche pour simplifier les tests est d’introduire la pureté dans le code au lieu de simplement séparer l’état compliqué dans un tas de classes.

Cela peut sembler étrange, mais tester des vues est plus facile que de tester un certain code métier. Une vue expose l’état comme un ensemble de propriétés et hérite de l’apparence visuelle de ces propriétés. Ensuite, vous pouvez utiliser FBSnapshotTestCase pour faire correspondre l’état avec l’apparence. Cela ne gère toujours pas certains cas limites comme les transitions personnalisées, mais combien de fois l’implémentez-vous ?

Conception excessive

VIPER est ce qui arrive quand d’anciens programmeurs Java d’entreprise envahissent le monde iOS. -n0damage, commentaire reddit

Honnêtement, quelqu’un peut-il regarder ça et dire : « oui, ces classes et protocoles supplémentaires ont vraiment amélioré ma capacité à comprendre ce qui se passe avec mon application ».

Imaginez une tâche simple : nous avons un bouton qui déclenche une mise à jour à partir d’un serveur et une vue avec des données provenant d’une réponse du serveur. Devinez combien de classes/protocoles seront affectés par ce changement ? Oui, au moins 3 classes et 4 protocoles vont être modifiés pour cette simple fonctionnalité. Personne ne se rappelle comment Spring a commencé avec quelques abstractions et a fini avec AbstractSingletonProxyFactoryBean ? Je veux toujours une « superclasse pratique de proxy factory bean pour les proxy factory beans qui ne créent que des singletons » dans mon code.

Composants redondants

Comme je l’ai déjà mentionné auparavant, un présentateur est généralement une classe très bête qui ne fait que proxier les appels de la vue vers l’interacteur (quelque chose comme ça). Oui, parfois il contient une logique compliquée, mais dans la plupart des cas, c’est juste un composant redondant.

Montant « DI-friendly » des protocoles

Il y a une confusion commune avec cette abréviation : VIPER met en œuvre les principes SOLID où DI signifie « inversion de dépendance », et non « injection ». L’injection de dépendance est un cas particulier du pattern Inversion de contrôle, qui est lié, mais différent de l’Inversion de dépendance.

L’Inversion de dépendance consiste à séparer les modules de différents niveaux en introduisant des abstractions entre eux. Par exemple, le module d’interface utilisateur ne devrait pas dépendre directement du module de réseau ou de persistance. L’inversion de contrôle est différente, c’est quand un module (généralement d’une bibliothèque que nous ne pouvons pas changer) délègue quelque chose à un autre module, qui est généralement fourni au premier module comme une dépendance. Oui, lorsque vous implémentez une source de données pour votre UITableView, vous utilisez le principe IoC. L’utilisation des mêmes caractéristiques du langage pour différents objectifs de haut niveau est une source de confusion ici.

Revenons à VIPER. Il y a beaucoup de protocoles (au moins 5) entre chaque classe à l’intérieur d’un module. Et ils ne sont pas du tout nécessaires. Presenter et Interactor ne sont pas des modules de différentes couches. L’application du principe IoC peut avoir du sens, mais posez-vous une question : combien de fois avez-vous mis en œuvre au moins deux présentateurs pour une vue ? Je pense que la plupart d’entre vous ont répondu zéro. Alors pourquoi il est nécessaire de créer ce tas de protocoles que nous n’utiliserons jamais ?

De plus, à cause de ces protocoles, vous ne pouvez pas facilement naviguer dans votre code avec un IDE, car cmd+clic vous dirigera vers un protocole, au lieu d’une véritable implémentation.

Problèmes de performance

C’est une chose cruciale et il y a beaucoup de gens qui ne s’en soucient pas, ou qui sous-estiment simplement un impact de mauvaises décisions d’architecture.

Je ne parlerai pas du framework Typhoon (qui est très populaire pour l’injection de dépendance dans le monde ObjC). Bien sûr, il a un certain impact sur les performances, en particulier lorsque vous utilisez l’auto-injection, mais VIPER ne vous oblige pas à l’utiliser. Au lieu de cela, je veux parler du temps d’exécution et du démarrage de l’application et de la façon dont VIPER ralentit votre application littéralement partout.

Temps de démarrage de l’application. Il est rarement discuté, mais c’est un sujet important, si votre app se lance très lentement, les utilisateurs éviteront de l’utiliser. Il y avait une session sur la dernière WWDC sur l’optimisation du temps de démarrage de l’app l’année dernière. TL ; DR : Le temps de démarrage de votre application dépend directement du nombre de classes que vous avez. Si vous avez 100 classes, tout va bien, personne ne le remarquera. Cependant, si votre application n’a que 100 classes, avez-vous vraiment besoin de cette architecture compliquée ? Mais si votre application est énorme, par exemple si vous travaillez sur une application Facebook (18 000 classes), l’impact est énorme, de l’ordre de 1 seconde selon la session mentionnée précédemment. Oui, le lancement à froid de votre application prendra 1 seconde juste pour charger toutes les métadonnées des classes et rien d’autre, vous avez bien lu.

Runtime dispatch. C’est plus compliqué, beaucoup plus difficile à profiler et surtout applicable pour le compilateur Swift seulement (parce qu’ObjC a des capacités d’exécution riches et un compilateur ne peut pas effectuer ces optimisations en toute sécurité). Parlons de ce qui se passe sous le capot lorsque vous appelez une méthode (j’utilise « appeler » au lieu de « envoyer un message » car le second terme n’est pas toujours correct pour Swift). Il existe 3 types de dispatch en Swift (du plus rapide au plus lent) : static, table dispatch et sending a message. Le dernier est le seul type qui est utilisé dans ObjC, et il est utilisé dans Swift lorsque l’interopérabilité avec le code ObjC est nécessaire, ou lorsque nous déclarons une méthode comme dynamic. Bien sûr, cette partie du runtime est énormément optimisée et écrite en assembleur pour toutes les plateformes. Mais que se passe-t-il si nous pouvons éviter cette surcharge parce que le compilateur a connaissance de ce qui va être appelé au moment de la compilation ? C’est exactement ce que fait le compilateur de Swift avec la répartition statique et la répartition par table. La répartition statique est rapide, mais le compilateur ne peut pas l’utiliser sans une confiance à 100% dans les types de l’expression. Mais lorsque le type de notre variable est un protocole, le compilateur est obligé d’utiliser la répartition via des tables de témoins de protocole. Ce n’est pas littéralement lent, mais 1ms ici, 1ms là, et maintenant le temps d’exécution total est supérieur de plus d’une seconde à ce que nous pourrions obtenir en utilisant du code Swift pur. Ce paragraphe est lié au précédent sur les protocoles, mais je pense qu’il est préférable de séparer les préoccupations concernant juste la quantité imprudente de protocoles inutilisés avec un véritable désordre avec le compilateur avec cela.

Séparation des abstractions faibles

Il devrait y avoir une – et de préférence une seule – façon évidente de le faire.

L’une des questions les plus populaires dans la communauté VIPER est « où je devrais mettre X ? ». Donc, d’un côté, il y a beaucoup de règles comment nous devrions faire les choses correctement, mais d’un autre côté, beaucoup de choses sont basées sur l’opinion. Il peut s’agir de cas compliqués, par exemple la gestion des CoreData avec NSFetchedResultsController, ou UIWebView. Mais même les cas communs, comme l’utilisation de UIAlertController, est un sujet de discussion. Jetons un coup d’œil, ici nous avons un routeur traitant l’alerte, mais là nous avons une vue présentant une alerte. Vous pouvez essayer de battre cela avec un argument qui est une alerte simple est un cas spécial pour l’alerte simple sans aucune action sauf la fermeture.

Les cas spéciaux ne sont pas assez spéciaux pour briser les règles.

Oui, mais pourquoi ici nous avons une usine pour créer ce genre d’alertes ? Donc, nous avons un désordre même avec un cas aussi simple avec UIAlertController. Le voulez-vous ?

Génération de code

La lisibilité compte.

Comment cela peut-il être un problème avec une architecture ? C’est juste générer un tas de classes de modèles au lieu de l’écrire par moi-même. Quel est le problème avec ça ? Le problème est que la plupart du temps, vous lisez du code (à moins que vous ne travailliez dans une entreprise d’externalisation jetable), et non pas vous l’écrivez. Vous lisez donc du code passe-partout mélangé à du code réel la plupart du temps. Est-ce que c’est bien ? Je ne le pense pas.

Conclusion

Je ne poursuis pas un objectif de vous décourager d’utiliser VIPER du tout. Il est encore peut être certaines applications qui peuvent bénéficier de tous. Cependant, avant de commencer à développer votre application, vous devriez vous poser quelques questions :

  1. Cette application va-t-elle avoir une longue durée de vie ?
  2. Les spécifications sont-elles assez stables ? Sinon, vous pouvez vous retrouver avec d’énormes refactorings sans fin, même pour de petits changements.
  3. Testez-vous vraiment vos applications ? Soyez honnête avec vous-même.

Ce n’est que si vous répondez « oui » à toutes les questions que VIPER peut être un bon choix pour votre application.

Enfin, la dernière : vous devriez utiliser votre propre cerveau pour vos décisions, ne faites pas aveuglément confiance à un gars de Medium ou à une conférence à laquelle vous avez assisté qui dit : « Utilisez X, X est cool. » Ces gars peuvent avoir tort aussi.