+ 33 6 68 40 27 75 contact@odeven.fr
Sélectionner une page

Chose promise, chose due, voici mon retour d’expérience sur la conception de l’application mobile SwipEat !

Je suis Vincent MARIUS, Architecte Logiciel et Directeur Technique dans la société Odeven. Et je vais vous raconter l’histoire qui se cache derrière SwipEat, espérant que ce retour d’expérience puisse bénéficier à un maximum de personnes (que vous soyez Développeur, Architecte, ou encore Product Owner).

SwipEat est une application mobile permettant aux utilisateurs de publier leurs propres recettes de cuisine, de les partager avec la communauté, et bien sûr de consulter lesdites recettes afin de pouvoir les cuisiner. Mais ce qui rend cette application unique, c’est surtout son système de « Scroll & Swipe », permettant à chacun (même les plus fainéants) de parcourir et de « sauvegarder » une recette en swipant à droite.

Introduction

Je vais vous parler ici de nombreux sujets, en essayant de ne pas trop entrer dans les détails :

  • L’approche « agile » du projet
  • La conception
  • Les technologies
  • L’architecture
  • L’automatisation des tâches

Le but de ce projet était principalement de faire monter en compétence notre développeur mobile, alors en contrat d’alternance, et d’attaquer le marché du développement mobile avec une première application. Notre équipe étant principalement orientée développement Web, il y avait tout de même un gap à remplir. Dans tous les cas, hors de question de dédier un budget important pour ce projet.

Pourquoi agile ?

Un des principes qui me plaît le plus, c’est le fait qu’en méthode agile, on drive un projet en se basant principalement sur la valeur (vous trouverez d’ailleurs un article très intéressant à ce sujet ici).

C’est-à-dire qu’on ne navigue pas dans un périmètre stricte, avec des objectifs et des délais précis, mais plutôt « au jour le jour », en remettant en question systématiquement les choix que nous avons fait, afin de toujours privilégier les fonctionnalités ayant le plus de valeur ajoutée, et de s’adapter continuellement.

C’est une approche qu’on aime particulièrement à Odeven : on commence petit, et on fait grossir au fur-et-à-mesure, sans faire de plans sur la comète, tout en testant le marché. Bien sûr, il est important de définir une trame de base. Avant de coder, il faut savoir à minima où on va.

Quelles technologies pour l’application mobile ?

Pour l’application mobile, nous avons choisi React Native. Pourquoi ? Parce que notre équipe est essentiellement composée de développeurs Web. React Native est certes un peu différent de React, mais lorsqu’on connaît Javascript (ou Typescript), pas besoin de se former sur un nouveau langage.

Et puis React Native est un Framework qui n’a plus besoin de faire ses preuves. Qu’on aime ou pas, il est soutenu par une grande communauté et des acteurs importants du marché. De plus, de nombreuses applications connues ont été développées avec (Facebook, Discord, et bien d’autres).

Bref, React Native est un Framework parfaitement adapté au développement mobile, et qui a l’avantage de permettre de coder l’essentielle de l’application en une base de code unique.

Si vous êtes également issu du développement Web, vous ne devriez pas avoir trop de mal à passer sur React Native 😉

Et pour le Backend ?

Et oui, pour pouvoir fonctionner, notre application a besoin d’un Backend (création des comptes utilisateur, stockage des recettes, des photos, etc…).

Nous avons fait le choix de partir sur différentes technologies :

  • Symfony pour la plupart des microservices : Symfony est un Framework robuste et parfaitement adapté pour faire du microservice
  • Typescript et Node.js pour l’API-Gateway : afin de définir un schéma GraphQL « global » (mais on y reviendra)
  • MySQL et MongoDB : pour stocker les données (utilisateurs, recettes, etc…)
  • Minio pour le stockage des photos : permet de « scale » facilement si nécessaire
  • RabbitMQ pour les messages asynchrones : permet aux différents microservices de communiquer de manière asynchrone
  • Traefik en tant que Reverse Proxy : dans une architecture microservices, on a généralement plusieurs services Web

Quel protocole pour la communication synchrone ?

Ce que j’entend par « synchrone », c’est tout simplement qu’on n’attend un réponse lorsqu’on effectue une requête, pour pouvoir passer à la suite. Par exemple, pour l’authentification, il est indispensable d’attendre la réponse (le token d’authentification, ou l’erreur en cas d’échec de la connexion).

Beaucoup de développeurs choisissent REST pour échanger des informations via HTTP, principalement parce qu’il est simple. Mais nous, nous avons choisi GraphQL, parce qu’il permet d’aller beaucoup plus loin que REST, surtout en terme d’optimisation.

Si vous ne connaissez pas GraphQL, ça vaut le coup d’œil. L’avantage principale réside dans le fait que le client peut préciser quelles informations il souhaite récupérer, en une seule requête. Autrement dit, le client peut demander à récupérer uniquement certains champs, et non la totalité d’un objet (et ce en plus de pouvoir récupérer des informations diverses en une seule requête).

Côté Back, ce qui est intéressant, c’est qu’une fois l’API GraphQL créée, on n’a généralement plus besoin de revenir dessus. En effet, si on la réfléchit bien dès le début, les clients auront à leur disposition tous les éléments leur permettant d’optimiser eux-mêmes les requêtes.

A noter également que l’application mobile n’est pas la seule à pouvoir exploiter l’API GraphQL d’un microservice. Tant qu’à en mettre une en place, autant l’exploiter au maximum. Ainsi, nos différents microservices peuvent utiliser ces APIs lorsqu’ils ont besoin de communiquer de manière synchrone les uns avec les autres.

Attention : GraphQL est simple et rapide à comprendre et à mettre en place côté client. Mais côté serveur, ça demande un peu plus de formation, et un certain temps d’adaptation.

Quel protocole pour la communication asynchrone ?

L’un des plus gros défaut de l’architecture microservice, c’est la complexité du système, surtout concernant les informations qui sont réparties dans différentes bases de données. En plus d’avoir chacun sa propre base (du moins, je vous le conseille), certains microservices peuvent utiliser des systèmes de base de données complètement différents (MySQL d’un côté, MongoDB d’un autre, InfluxDB pour des statistiques, etc…).

Le but d’une telle architecture est de séparer l’application en blocs distincts. Ce n’est pas pour autant qu’il ne doivent pas coopérer, bien au contraire !

Si un utilisateur supprime son compte, qu’advient-il de ses recettes ? Si tout est stocké dans une même base de données, on pourrait appliquer un traitement en cascade par exemple, afin que ses recettes soient automatiquement supprimées. Mais si les recettes sont stockées ailleurs, comment le service qui en est responsable peut-il les supprimer ?

Il y a deux manières de gérer ça :

  1. Lors de la suppression d’un utilisateur, le service A exécute une requête synchrone vers le service B (et attend la fin du traitement)
  2. Lors de la suppression d’un utilisateur, le service A envoie un message asynchrone, consommé « plus tard » par le service B

La deuxième solution est à privilégier. Imaginez que l’utilisateur possède des milliers de recette. La suppression de ces recettes pourrait être très longue. L’utilisateur n’aimerait probablement pas se retrouver devant un écran de chargement de plusieurs secondes (voir minutes), juste pour valider la suppression de son compte.

Bien sûr, tout dépend du cas d’utilisation. Il existe différents patterns (ex: SAGA) permettant de gérer tous ces échanges de manières asynchrone. Mais nous n’avons pas besoin d’aller aussi loin dans le cas présent.

Pour SwipEat, nous avons opté pour le protocole AMQP (avec un serveur RabbitMQ). Simple et efficace, il permet de définir des « queues », dans lesquelles sont stockés différents messages. Ce qui est sympa dans ce système, c’est que celui qui envoi un message ne sait pas qui va le recevoir. Ce sont les « consommateurs » qui « s’abonnent » à un type de message précis.

Dans l’exemple ci-dessus, on pourrait avoir un service « Security », qui enverrait un message « event.user.deleted », et un service « Recipes » qui s’abonnerait à ce type de message, afin d’effectuer un traitement précis à chaque fois qu’un utilisateur est supprimé. Attention bien sûr à gérer le délais entre la suppression d’un compte utilisateur, et des recettes associées. Encore une fois, il serait tout à fait possible d’utiliser le patron SAGA, afin de ne supprimer réellement le compte utilisateur qu’une fois toutes les recettes réellement supprimées.

L’architecture

Ce qui est important, c’est de savoir qu’une architecture n’est pas fixe, et qu’elle doit évoluer avec le projet. Sinon, aucun intérêt à mettre en place une architecture microservices…

Alors voici les microservices que nous avons imaginés :

  • Microservice « security » : Responsable des comptes utilisateurs (inscription, authentification, droits, etc…)
  • Microservice « recipes » : Responsable des recettes de cuisine
  • Microservice « preferences » : Responsable des « likes/dislikes » et des liens entre les utilisateurs (followers)
  • Microservice « drive » : Responsable des fichiers (photos des recettes, photos de profil, etc…)
  • Microservice « mailer » : Responsable de l’envoi des emails
  • Microservice « api-gateway » : Responsable de la mise en place d’un « super schéma » GraphQL

Et les différents middlewares que nous utilisons :

  • MySQL : pour stocker les données « relationnelles »
  • MongoDB : pour stocker les données « non relationnelles »
  • Minio : pour stocker les fichiers
  • RabbitMQ : pour gérer les échanges AMQP
  • Traefik : pour rediriger le trafic vers le bon microservice

Il est très difficile (voir impossible) d’identifier parfaitement les composantes d’une application, et de trouver le découpage optimal. Le but est de résoudre les problèmes d’une architecture monolithique, mais il ne faudrait pas non plus tomber dans le « nanoservice » (tout dépend de ce que vous cherchez à accomplir).

L’idéal est de s’adapter aux évolutions au fur et à mesure. Si vous êtes capable d’anticiper, c’est parfait. Mais attention à ne pas trop en faire « au cas où… », au risque de coder inutilement.

Les bundles Symfony

N’importe qui peut créer un bundle avec Symfony. Il s’agit ni plus ni moins d’un package, avec un zeste de Symfony par dessus (permettant une meilleur intégration avec le Framework).

Pourquoi je vous parle des bundles ? Et bien parce qu’ils peuvent vous faire gagner un temps précieux !

En microservice, on a tendance à coder plusieurs fois les mêmes choses, dans des microservices différents. Avec le système de bundle, on peut par exemple créer un bundle pour chaque microservice, visant à simplifier la communication avec le microservice en question.

Voici un exemple de ce que nous avons fait dans SwipEat :

  • Notre microservice « security » est responsable de tout ce qui à trait aux comptes utilisateurs (connexion par exemple)
  • Notre microservice « recipes » a besoin de contacter le service « security » pour vérifier si le token d’authentification de l’utilisateur est valide, et de savoir à quel utilisateur il correspond (pour savoir si l’utilisateur a le droit de supprimer une recette par exemple)
  • Notre bundle « swipeat/security-bundle » apporte un service Symfony nommé « Security », permettant d’exécuter des méthodes telles que « isAuthenticated() », lesquelles s’occupent d’effectuer une requête GraphQL vers le microservice « security », gérer le cache, etc…

Avec ce système, on crée une seule fois des fonctions optimisées visant à pallier aux inconvénients de l’architecture microservice. Et on les fait évoluer progressivement, permettant à tous les microservices d’en bénéficier.

Si vous utilisez GitLab, vous pouvez également automatiser la génération du package Composer, et le stocker sur votre serveur GitLab. Quelques lignes de configuration dans le fichier « composer.json » de vos projets, et le tour est joué : Composer sera capable d’aller récupérer des packages privés.

L’API Gateway

C’est à mon avis grâce à l’API Gateway que GraphQL apporte le plus dans une architecture microservices.

Imaginez que vous avez besoin, dans une même page, d’afficher une recette (nom de la recettes, ingrédients, etc…), le pseudonyme de l’auteur de la recette, le nombre de likes, etc…

Le problème, c’est que :

  • La recette est stockée dans une base de données MongoDB, gérée par le microservice « recipes »
  • Le pseudonyme de l’utilisateur est stocké dans une base de données MySQL, gérée par le microservice « security »,
  • Le nombre de « like » est stocké dans une autre base de données, gérée par le microservice « preferences »
  • Etc…

Ca fait beaucoup de requête tout ça, pour simplement afficher une page. Si l’application mobile devait faire autant de requêtes HTTP pour récupérer toutes ces informations, ce serait lent…

Imaginez même qu’on souhaite afficher une liste d’utilisateurs (par exemple les 10 utilisateurs les plus célèbres), et pour chaque utilisateur, le nombre de likes cumulé, le nombre de recettes, etc…

L’API Gateway est formidable pour gérer ces cas particuliers. Son but est principalement :

  • D’unifier toutes les APIs (si jamais on utilisait une API externe par exemple, on pourrait définir des requêtes GraphQL permettant de faire tampon)
  • De faire des agrégations de données

Nous avons plusieurs microservices, chacun exposant son propre schéma GraphQL. Avec l’API Gateway, on va pouvoir faire comme si on n’avait qu’un seul et unique schéma, permettant de créer des liens entre un objet « Recipe » et un objet « User » par exemple.

Cela permet également d’exécuter plusieurs « sous-requêtes » vers des microservices différents, mais entre l’API Gateway, et les autres microservices (toutes ces requêtes sont donc exécutées en interne sur votre serveur, et donc indépendantes du réseau de l’utilisateur).

Il existe différentes approches en GraphQL. L’une d’elle m’intéresse beaucoup : Apollo Federation.

Cette approche permet à chaque microservice de définir les liens qu’il entretient avec les objets des autres schémas.

Malheureusement, avec Symfony le bundle le plus complet autour de GraphQL est OverblogGraphQLBundle. Et celui-ci ne gère pas cette approche à ce jour…

C’est pourquoi j’ai choisi d’utiliser l’approche dites « Schema Stitching« . Elle est un peu plus simple, et consiste à réunir tous les schémas dans notre API Gateway, et de configurer les différentes relations qui existent entre chacun.

Par exemple, l’objet « Recipe » défini par le microservice « Recipes » possède un attribut « userId ». Dans l’API Gateway, on peut ajouter un attribut « user » sur l’objet « Recipe », en lui expliquant comment récupérer l’utilisateur depuis le microservice « Security », via l’attribut « userId ».

J’ai fait le choix d’utiliser Javascript pour coder ce microservice, car la bibliothèque GraphQL Tools et très bien faites, et permet de gérer ces aspects simplement. Elle permet même de faire du « batch loading », et donc de charger plusieurs données à la fois, afin de réduire le nombre de « sous-requêtes ».

L’automatisation

A Odeven, comme dans de nombreuses boîtes de dev, on utilise GitLab pour héberger le code source de nos projets. Donc autant profiter de GitLab CI/CD pour automatiser certaines tâches. Dans SwipEat, nous avons défini plusieurs stages, dont certains très utiles pour une application mobile :

  • Évolution du numéro de version
  • Build de l’application pour Android/iOS
  • Déploiement sur les stores (Play Store, App Store)

Le fait d’automatiser ces tâches nous permet d’éviter de futures erreurs humaines (mauvais nommage de la version, déploiement de la mauvaise version sur les stores, etc…).

Et puis tant qu’à faire, autant en faire profiter tous les futurs projets !

Et oui, GitLab CI/CD permet de créer ses propres templates, réutilisables. C’est pourquoi nous avons défini des templates pour chaque type de technos que nous utilisons à Odeven :

  • Service Symfony
  • Bundle Symfony
  • Application React Native (Expo)
  • Application VueJS
  • Application ReactJS
  • Etc…

Du côté des projets, il n’y a plus qu’à créer le fameux fichier « .gitlab-ci.yml », et de le configurer de manière à pointer sur un template existant. Encore mieux, il est possible de définir des variables, et donc de personnaliser le template suivant si on souhaite que le projet soit exposé sur internet, associé à un serveur MySQL, MongoDB, etc… pour l’exécution des tests, et plus encore.

L’avantage principale de cette approche, c’est que le temps que vous passez aujourd’hui à concevoir vos templates, vous le récupèrerez très largement demain, pour chacun de vos futurs projets.

Conclusion

Un projet en entreprise, ce peut-être très simple, ou très complexe. Il y a une infinité de façon de faire un même projet, d’un point de vu « code », mais aussi « architecture », « gestion de projet », « outils », etc…

SwipEat représente une expérience intéressante pour notre équipe : l’occasion de s’essayer au développement mobile, de monter en compétence, et de rendre nos processus de développement encore plus performants.

SwipEat n’est pas parfait. Certains microservices par exemple, utilisent du MySQL alors que le nombre d’enregistrement pourrait potentiellement (un jour), représenter un réel problème en terme de performance. Mais ce n’est pas grave, car après tout avec une architecture microservices, l’avantage c’est qu’on peut faire évoluer simplement les différents composants. On peut même envisager de redévelopper entièrement un microservice. Et plutôt que de mettre des mois à implémenter une solution qui sera peut-être inutile (imaginez que le projet soit un échec complet), autant trouver le juste milieu (court terme + long terme), quitte à devoir retravailler certaines parties plus tard.

Ce n’est qu’un exemple parmi d’autres, car notre solution est imparfaite. En réalité, c’est même ce que l’on souhaite. Car après tout, une solution parfaite ça n’existe pas (surtout en développement). Un développeur devient de plus en plus compétant de jour en jour, alors il y a fort à parier que le code que vous avez fait il y a 6 mois, même si vous en étiez très fier à l’époque, vous le trouviez nul aujourd’hui (en tout cas c’est mon cas, même après 15 ans de code dans les doigts).

Ajoutez à ça le fait que le produit (et même la clientèle), va évoluer en même temps que votre projet, et vous aurez la preuve que jamais vous ne trouverez de solution parfaite. Ce n’est pas pour autant que vous devez faire n’importe quoi. Essayez au moins d’anticiper, sans forcément tout développer, mais plutôt en codant de manière à éviter que ce que vous faites maintenant ne soit pas un problème plus tard en cas d’évolution.

J’espère que ce partage d’expérience vous a inspiré. Il y a tellement de choses à dire, même sur une simple application de recette de cuisine, mais je vais éviter de vous endormir avec un roman. N’hésitez pas à laisser un commentaire si vous avez des questions ou même des suggestions. A bientôt !