Trunk Based Development (TBD) vs Gitflow

La principale différence entre Gitflow et le TBD est que les premières branches ont une durée de vie plus longue et des commits plus importants.

Introduction

Aujourd'hui, comment parler de développement logiciel sans parler de Git ? Un bon système de gestion des versions est essentiel pour assurer un flux de travail efficace. Git est l'outil de gestion de versions par excellence et est le plus populaire. Néanmoins, avec Git, il s'est développé différentes stratégies pour structurer et gérer le flux de modifications de la codebase. Parmi toutes ces stratégies, aujourd'hui, deux vont nous intéresser : le Trunk-Based Development (TBD) et Git Flow.

D'un côté on a le TBD, une approche minimaliste qui préconise de travailler directement sur un tronc commun, autrement dit la branche principale. Tandis que Git Flow, lui, propose une structure plus complexe, avec des branches dédiées à des fonctionnalités, des corrections, des versions, etc.

Les deux approches ont des avantages et des inconvénients, leurs propres complexités et simplicités et c'est ce que nous allons voir maintenant.

L'objectif de cet article n'est pas simplement de fournir une explication de ces deux stratégies de gestion des versions, mais plutôt de démontrer pourquoi, dans de nombreux contextes, le TBD peut s'avérer une approche plus optimale que Git Flow.


Git Flow vs Trunk-Based Development

La stratégie Git Flow

Git Flow est une stratégie de gestion de versions populaire qui a été conçue pour aider les équipes à gérer les développements complexes, en tirant parti de la puissance et de la flexibilité des branches Git. Elle propose une structure organisée qui facilite le développement parallèle de différentes fonctionnalités et la gestion des versions.

Organisation

Avec Git Flow on organise nos branches de la manière suivante :

  • main, la branche principale qui représente l'état actuel de la production
  • develop, la branche où se trouve toutes les fonctionnalités, corrections et autres de la prochaine version prévue
  • feature/xxx, les branches créées à partir de develop où se trouve le code d'une fonctionnalité qui sera fusionné avec develop une fois le développement terminé
  • release/xxx, les branches créées à partir de develop où se trouve le code d'une nouvelle version du logiciel, potentiellement affiné avant le déploiement
  • hotfix/xxx, les branches créées à partir de main pour des corrections critiques découvertes en production. Ces branches sont fusionnées dans main dès que le correctif est prêt et sont également fusionnées avec develop pour s'assurer que la correction perdurera dans la codebase

Les problèmes de Git Flow

La force de Git Flow réside dans sa structure qui permet de gérer facilement des tâches parallèles et suivre d'évolution du code à travers le temps.

Cette stratégie entraîne néanmoins un lot d'inconvénients :

  • Complexité - Cette stratégie nécessite beaucoup de manipulation manuelle des branches qui demande une maîtrise totale de Git et de ce processus
  • Intégration continue compliquée - L'intégration continue est plus complexe à mettre en place en raison du développement parallèle sur plusieurs branches
  • Déploiement fréquent coûteux - Le déploiement continus/fréquents n'est pas impossible mais demande beaucoup plus de temps et d'énergie
  • Problèmes de fusion - Le nombre parallèle de branches important entraîne de nombreux problèmes potentiels de merge
  • Revues de code difficiles - La taille des pull requests a tendance à être plus importante avec Git Flow parce qu'elles contiennent des fonctionnalités complètes. La branche de feature a tendance à vivre trop longtemps, nécessitant des fusions fréquentes avec develop ce qui entraîne des retards dans le processus de livraison

La stratégie Trunk-Based Development

La stratégie Trunk-Based Development est une approche bien plus minimaliste dont le but est de simplifier le flux de travail en minimisant la fragmentation du code et en facilitant l'intégration continue. Pour cela on ne va travailler que sur une seule branche principale (main ou trunk) autrement appelé : le tronc commun.

Une seule source de vérité

Avec le TBD, cela signifie donc que toutes les modifications du code sont introduites et fusionnées directement dans la branche principale. Chaque développeur doit donc fusionner régulièrement ses modifications, plusieurs fois par jour. En conséquence, les versions sont gérées directement à partir de la branche principale, chaque développeur est constamment à jour et les problèmes de fusions sont considérablement réduits. Le cycle de développement est plus rapide et alimente l'intégration et le déploiements continus.

Quid des branches ?

Travailler avec l'approche TBD ne signifie pas qu'on a plus du tout de branche en plus de la branche principale. En effet, les branches peuvent encore être utilisées mais elles se doivent d'être de très courte durée et fusionnées dès que le travail est terminé.

Quid des revues de code ?

Quel que soit la stratégie adoptée, le processus de revue de code persiste et demeure une composante essentielle pour assurer la qualité du code qui est fusionné dans le tronc commun. La subtilité entre les deux stratégie est que avec le TBD, les modifications étant fréquemment fusionnées, elles sont généralement plus petites. Et si les modifications s'avèrent importante alors le TBD souhaite mettre en avant la collaboration et demanderait de faire ces modifications en pair ou en mob programming. Enfin, les revues de code se doivent d'être traitées rapidement (dans la demi-journée) et doivent durer que quelques minutes (15 maximum à peu près).

Quid des modifications importantes ?

Le TBD n'empêche pas le développement de fonctionnalités importantes qui demandent donc de lourdes modifications du code. En revanche, cette stratégie va favoriser la collaboration via du pair ou du mob programming mais ce n'est pas la seule solution. Il existe également les Feature Flags.

Un Feature Flag est une technique de développement logiciel permettant de masquer, activer ou désactiver une fonctionnalité dans un environnement de production sans avoir à redéployer le code. Cette technique offre un contrôle en temps réel des fonctionnalités, représente une sécurité contre les problèmes potentiels de nouvelles fonctionnalités et, surtout, permet de travailler sur de nouvelles fonctionnalités directement dans la branche principale sans interrompre le fonctionnement normal de l'application.

Dans le cadre de l'approche TBD, où toutes les modifications sont effectuées directement sur la branche principale, l'utilisation de Feature Flags de fusionner le code pour de nouvelles fonctionnalités qui ne sont pas encore terminées ou testées. La fonctionnalité peut être développée et fusionnée dans le tronc sans être exposée aux utilisateurs jusqu'à ce qu'elle soit prête, où le Feature Flag à ce moment-là, peut être activé.

Le recours à des Feature Flags apporte une flexibilité considérable au processus de développement et constitue une composante essentielle pour atteindre un déploiement continu et un flux de travail efficace dans le TBD.

Mais à quoi ça ressemble concrètement ?

Ce sont ni-plus ni-moins des booléens :

Et à l'usage, par exemple pour du React mais le principe est le même pour n'importe quel environnement :

Il est tout à fait possible de mettre en place un système de Feature Flags contrôlable à distance via un backoffice ou des outils tout prêt à l'usage qui existe sur le marché comme Firebase Remote Config, PostHog ou Harness par exemple.

Pré-requis

Pour une implémentation efficace du TBD, plusieurs éléments sont généralement requis :

  • Intégration Continue (CI) : c'est une stratégie qui bénéficie grandement de l'utilisation de la CI étant donné quelle est souvent sollicité pour de petites modifications. La CI permet d'assurer que le tronc commun est toujours en état de fonctionner correctement et qu'elle continue à être deployable à tout moment.
  • Tests automatisés : les tests automatisés vont de pair avec la CI, ils assurent, si ils sont correctement mis en place, de la qualité du code.
  • Revues de code : comme mentionné précédemment, les revues de code sont une composante essentielle pour maintenir la qualité et le partage de connaissance.
  • Feature flag : comme expliqué précédemment, il est important de savoir mettre en place les features flag parce qu'ils sont souvent utilisés.
  • Culture de la collaboration : enfin, l'environnement de travail est très important, toute l'équipe doit être impliqué, connaître et appliquer ce processus. L'équipe doit également se responsabiliser et doit être prête à collaborer étroitement et à partager ses connaissances.

Résumons les bénéfices

Maintenant que nous avons expliqué le TBD comment il fonctionne et dans quel contexte, nous pouvons en ressortir les bénéfices suivants :

  • Intégration continue (CI) : grâce à l'intégration fréquente de petits changements, les problèmes sont détectés et résolus plus rapidement. De plus, cela limite les éventuels conflits de fusion.
  • Déploiements plus rapides : avec une seule branche principale toujours prête à être déployée, le TBD peut faciliter des déploiements plus rapides et plus réguliers.
  • Simplification du processus : la stratégie TBD supprime la nécessité de gérer de nombreuses branches à long terme, simplifiant le flux de travail de l'équipe.
  • Qualité du code : les revues de code régulières contribuent à maintenir la qualité du code et à anticiper les problèmes.
  • Flexibilité grâce aux Feature Flags : l'utilisation de Feature Flags permet de tester de nouvelles fonctionnalités en production sans les exposer aux utilisateurs finaux, contribuant à un lancement plus sûr et contrôlé des nouveautés.

Un dernier bénéfice que nous allons voir en détail dans le dernier point de cet article, c'est la facilité avec laquelle le TBD favorise l'atteinte de performances élevées selon les métriques DORA, un ensemble de mesures reconnues pour évaluer l'efficacité des équipes.

Et les inconvénients dans tout ça ?

La mise en œuvre de la stratégie TBD présente également certains défis ou inconvénients :

  • Gestion rigoureuses des fusions : les modifications doivent être fusionnées en continu dans le tronc commun, ce qui nécessite que les développeurs synchronisent fréquemment leur travail avec la branche principale pour éviter les conflits de fusion.
  • Culture : pour certaines équipes, l'adoption de cette stratégie peut nécessiter un changement significatif dans leurs pratiques de travail, notamment l'intégration continue et les revues de code constantes.
  • Déploiements risqués sans tests adéquats : sans une couverture de test adéquate, le risque d'introduction de bugs en production peut être plus élevé, car tout le code est fusionné directement dans la branche principale qui est déployée.
  • Complexité des Feature Flags : bien que les Feature Flags offrent plus de flexibilité, leur gestion peut ajouter une certaine complexité. Une mauvaise utilisation des feature flags peut entraîner de la dette technique.

En dépit de ces défis/inconvénients, il est globalement reconnu que les avantages valent les efforts nécessaires pour mettre en œuvre le TBD. Comme pour beaucoup de choses de la vie, il est important de déterminer si cette stratégie est adaptée au contexte spécifique de votre équipe et de votre projet.

DORA Metrics et Trunk-Based Development

Les DORA Metrics (ou DevOps Research and Assessment metrics), sont une série de mesures de performance pour les équipes de développement logiciel.

Ces mesures incluent :

  • Le temps de cycle de déploiement (Mean Lead Time for Changes - MLTC) : Le temps moyen nécessaire pour qu'un commit passe à la production.
  • La fréquence de déploiement (Deployment Frequency - DF) : À quelle fréquence une organisation déploie du code en production.
  • Le temps de rétablissement (Mean Time to Restore - MTTR) : Le temps nécessaire pour récupérer d'une panne ou d'un incident de production.
  • Le taux d'échec des modifications (Change Failure Rate - CFR) : La proportion de déploiements causant un incident de production ou un échec de service.

Le TBD est lié aux DORA Metrics car c'est une méthode de développement qui peut potentiellement améliorer ces mesures. Il encourage des cycles d'intégration et de déploiement plus courts, ce qui peut accélérer le délai de déploiement et augmenter la fréquence de déploiement.

  • MLTC et DF : La fusion fréquente de petites modifications permet de réduire le temps de cycle de déploiement et d'augmenter la fréquence de déploiement, car la branche principale est toujours dans un état deployable.
  • CFR : Avec des revues de code régulières et des tests automatisés, on peut s'attendre à ce que le pourcentage de modifications ratées diminue, car les problèmes sont souvent découverts et corrigés avant le déploiement.
  • MTTR : Comme les problèmes sont généralement plus petits et plus localisés avec cette approche, il est généralement possible de corriger et de restaurer le service plus rapidement.

En résumé, l’approche Trunk-Based Development est bien alignée avec l’amélioration des métriques DORA, ce qui en fait une stratégie de choix pour les équipes axées sur le DevOps.

Ainsi, le développement basé sur la stratégie TBD peut contribuer à l'amélioration des DORA metrics.

Le mot de la fin

Cet article a examiné en profondeur la stratégie de Trunk-Based Development (TBD) en la comparant à Git Flow et en mettant en avant ses nombreux avantages. Nous avons analysé comment le TBD favorise des cycles de développement plus rapides, une meilleure qualité de code grâce aux revues de code constantes, et une plus grande flexibilité par l'utilisation de feature flags. Nous avons également expliqué comment le TBD facilite l'atteinte de performances élevées selon les DORA Metrics.


Cependant, nous avons également souligné que le TBD n'est pas sans défis. Il nécessite une gestion rigoureuse des fusions, un changement culturel significatif dans certaines équipes, une bonne couverture de tests pour minimiser les risques associés au déploiement constant.

Pour conclure, le TBD est un modèle puissant qui peut accélérer la livraison de valeur, améliorer la qualité du code et favoriser l'optimisation continue des performances de l'équipe. Cependant, comme pour toute stratégie, son adoption doit être précédée d’une évaluation approfondie des besoins, contextes et capacités spécifiques de l’équipe.

Les dédicaces

Sommaire
Nos autres catégories
Partager sur :
Nos autres catégories
Partager sur :

Ne manquez rien
Abonnez-vous à notre newsletter

Notre newsletter tous les mois :
Je m'abonne
Merci ! C'est dans la boîte :)
Oops! Something went wrong while submitting the form.

Nos experts vous parlent
Le décodeur

L’Architecture Hexagonale sur un projet Web + Mobile (Partie 2 sur 5)
28/2/2024

Dans l'article précédent nous avons initialisé notre monorepo, la CI, le framework de test et préparé la structure de notre projet et plus précisément de notre Architecture Hexagonale pour la lib core.

Dans ce nouvel article de la série notre objectif va être de mettre en place l'Architecture Hexagonale et de montrer comment grâce à elle nous allons pouvoir développer et créer de la logique métier sans UI (donc sans ouvrir le navigateur ou l'app mobile). Pour cela nous allons travailler en TDD (Test-Driven Development, vous pouvez voir mon article à ce sujet) et utiliser le feedback des tests.

L'Architecture Hexagonale

La structure cible

Pour rappel, voici la structure que l'on va mettre en place à l'issue de cet article :

- src
    - wallet
       - __ tests __
         - wallet.service.test.ts
       - domain
         - wallet.ts
         - wallet.repository.ts
         - wallet.service.ts
       - infrastructure
         - in-memory-wallet.repository.ts
         - local-storage-wallet.repository.ts
         - mmkv-wallet.repository.ts
       - user-interface
         - wallet.store.ts

Chose promise, chose due ! Nous allons maintenant rentrer dans le détail de chaque fichier, à quoi ils servent et ce qu'ils contient.

Développer en TDD

Lorsqu'on travaille en TDD on commence par le test et ce test va nous guider vers un objectif. Il va nous assurer qu'on suit le bon chemin à l'aide de la boucle de feedback régulière qu'on obtient à l'aide des tests. Pour en savoir plus sur la méthodologie à suivre pour faire du TDD je vous invite à nouveau à lire mon article à ce sujet.

Nous allons commencer par travailler sur l'entité Wallet qui correspond à un portefeuille qui a un solde négatif ou positif (par exemple on peut avoir le portefeuille "Compte Principal Julien" qui a un solde positif de 1000€).

Voici les tests mis en place pour cette entité :

describe('Wallet Service', () => {
	let service: WalletService

	beforeEach(() => {
		const repository = new InMemoryWalletRepository()
		service = new WalletService(repository)
	})

	test('getAll > should retrieve all wallets', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallets = await service.getAll()

		expect(retrievedWallets).toEqual([newWallet])
	})

	test('get > should retrieve a wallet according to an id', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)

		expect(retrievedWallet).toEqual(newWallet)
	})

	test('create > shoudl create a wallet', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		const createdWallet = await service.create(newWallet)
		const retrievedWallets = await service.getAll()
		const retrievedWallet = await service.get(createdWallet.id)

		expect(createdWallet).toEqual(newWallet)
		expect(retrievedWallets).toEqual([newWallet])
		expect(retrievedWallet).toEqual(newWallet)
	})

	test('update > should update the specified wallet', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }
		const updatedWallet = { id: '1', name: 'Wallet 1', balance: 100 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)
		const modifiedWallet = await service.update(updatedWallet)
		const retrievedModifiedWallet = await service.get(modifiedWallet.id)

		expect(retrievedWallet).toEqual(newWallet)
		expect(modifiedWallet).toEqual(updatedWallet)
		expect(retrievedModifiedWallet).toEqual(updatedWallet)
	})

	test('delete > should delete a wallet according to an id', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)
		await service.delete(newWallet.id)
		const retrievedWallets = await service.getAll()

		expect(retrievedWallet).toEqual(newWallet)
		expect(retrievedWallets).toEqual([])
	})
})

On peut comprendre via ces tests que les cas d'utilisations de notre entité sont :

  • getAll, récupération de tous les portefeuilles
  • get, récupération d'un portefeuille en particulier
  • create, création d'un portefeuille
  • update, mise à jour d'un portefeuille
  • delete, suppression d'un portefeuille

Nous allons voir maintenant comme réussir à mettre en place ces tests.

Domain

Nous allons commencé par créer le contenu de la partie Domain. Dans cette partie nous allons retrouver tout ce qui représente le problème à résoudre (problème métier). C'est une partie qui doit être totalement indépendante.

L'entité

Commençons par créer notre entité Wallet correspondant à un portefeuille.

type Wallet = {
	// un identifiant unique (ex: 4d0c2e72-be1a-4e2c-a189-2f321fcdc3a4)
	id: string

	// un nom (ex: Compte Principal Julien)
	name: string

	// un nombre positif ou négatif pour le solde (ex: +1000€)
	balance: number
}


Le repository

Maintenant que notre entité est définie, nous allons définir une interface que l'on appelle également port qui va préciser comment interagir avec cette entité. Nous utilisons ici un modèle de conception d'inversion de dépendances qui nous permet de rester totalement libre sur les outils à utiliser pour respecter cette interface. Nous pourrons très bien implémenté cette interface en utilisant une base de données, une API ou un localStorage par exemple, le domaine s'en fiche.

interface WalletRepository {
	getAll(): Promise
	get(walletId: string): Promise
	create(wallet: Wallet): Promise
	update(wallet: Wallet): Promise
	delete(walletId: string): Promise
}

Le service

Nous avons notre entité et nous savons commencer interagir avec, maintenant nous allons créer un service qui va consumer une implémentation du de notre interface repository (partie suivante dans l'infrastructure).

class WalletService implements WalletRepository {
	constructor(private repository: WalletRepository) {}

	getAll() {
		return this.repository.getAll()
	}

	get(walletId: string) {
		return this.repository.get(walletId)
	}

	create(wallet: Wallet) {
		return this.repository.create(wallet)
	}

	update(wallet: Wallet) {
		return this.repository.update(wallet)
	}

	delete(walletId: string) {
		return this.repository.delete(walletId)
	}
}

Infrastructure

L'infrastructure est composée des différentes implémentations des ports du domaine, on les appelle également Adapters. Ici, nous aurons du code spécifique pour consommer une technologie concrète (une base de données, une API, etc.). C'est une partie qui ne doit dépendre uniquement du domaine.

L'implémentation du repository

Nous allons maintenant voir l'une des implémentation possible de notre WalletRepository. Pour commencer nous allons faire du in-memory, pratique notamment pour la mise en place des premiers tests de nos cas d'utilisations.

class InMemoryWalletRepository implements WalletRepository {
	private wallets: Wallet[] = []

	getAll() {
		return Promise.resolve(this.wallets)
	}

	get(walletId: string) {
		return Promise.resolve(this.wallets.find((wallet) => wallet.id === walletId))
	}

	create(wallet: Wallet) {
		this.wallets.push(wallet)
		return Promise.resolve(wallet)
	}

	update(wallet: Wallet) {
		const index = this.wallets.findIndex((w) => w.id === wallet.id)
		this.wallets[index] = wallet
		return Promise.resolve(wallet)
	}

	delete(walletId: string) {
		const index = this.wallets.findIndex((w) => w.id === walletId)
		this.wallets.splice(index, 1)
		return Promise.resolve()
	}
}

Comment dis précédemment, il s'agit d'une des multiples implémentation possible de notre WalletRepository. Nous pouvons très bien imaginer plus tard mettre en place un LocalStorageWalletRepository ou bien un SupabaseWalletRepostory.

Vous pouvez consulter mon répertoire public de broney sur GitHub pour voir mon implémentation de ces 2 repository et notamment de comment j'ai adapté ma série de test pour garantir leur bon fonctionnement.

User Interface

La partie user interface est composée de tous les adaptateurs qui constituent les points d'entrée de l'application. Les utilisateurs utilisent ces adaptateurs pour pouvoir interagir avec le coeur de l'application. Dans notre cas nous allons régulièrement utiliser des stores en utilisant la libraire Zustand. Il s'agit d'une libraire JS minimaliste pour la gestion d'états (une solution plus complexe serait par exemple Redux).

Voyons voir comment articuler notre store Zustand pour permettre à l'utilisateur d'interagir avec le coeur de l'application.

import { createStore } from 'zustand/vanilla'
import { InMemoryWalletRepository } from '../infrastructure/in-memory-wallet.repository'
import { WalletService } from '../domain/wallet.service'
import { Wallet } from '../domain/wallet'

const repository = new InMemoryWalletRepository()
const service = new WalletService(repository)

type States = {
	wallets: Wallet[]
	currentWallet: Wallet | undefined
}

type Actions = {
	load: () => void
	setCurrentWallet: (wallet: Wallet) => void
	getWallet: (walletId: string) => void
	createWallet: (wallet: Wallet) => void
	updateWallet: (wallet: Wallet) => void
	deleteWallet: (walletId: string) => void
}

export const walletStore = createStore()((set) => ({
	wallets: [],
	currentWallet: undefined,

	load: async () => {
		const allWallets = await service.getAll()
		set({ wallets: allWallets })
	},

	setCurrentWallet: (wallet) => set({ currentWallet: wallet }),

	getWallet: async (walletId: string) => {
		const wallet = await service.get(walletId)
		set({ currentWallet: wallet })
	},

	createWallet: async (wallet: Wallet) => {
		const newWallet = await service.create(wallet)
		set((state) => ({ wallets: [...state.wallets, newWallet] }))
	},

	updateWallet: async (wallet: Wallet) => {
		const updatedWallet = await service.update(wallet)
		set((state) => ({
			wallets: state.wallets.map((w) => (w.id === updatedWallet.id ? updatedWallet : w)),
			currentWallet: state.currentWallet?.id === updatedWallet.id ? updatedWallet : state.currentWallet,
		}))
	},

	deleteWallet: async (walletId: string) => {
		await service.delete(walletId)
		set((state) => ({
			wallets: state.wallets.filter((w) => w.id !== walletId),
			currentWallet: state.currentWallet?.id === walletId ? undefined : state.currentWallet,
		}))
	},
}))


Avec ce store on remarque qu'on va pouvoir facilement, dans n'importe quel environnement JavaScript, charger, définir, récupérer, créer, mettre à jour et supprimer des portefeuilles, tout en maintenant un état global pour l'ensemble des portefeuilles et du portefeuille courant.

Conclusion

Nous avons maintenant terminé ce deuxième article de cette série sur le développement d'une application web et mobile avec l'Architecture Hexagonale et le partage de la logique métier et des composants UI.

Dans cette deuxième partie nous avons vu comment travailler en TDD et surtout comment écrire de la logique métier sans avoir à ouvrir une quelque interface à l'exception du terminal pour les retours de tests.

Nous avons également eu un aperçu de comment nous allons interagir avec nos applications avec le coeur de l'application, via notre store Zustand. Nous irons plus loin à ce sujet dans le prochain article, la troisième partie : Partager de la logique métier et des composants entre le Web et le Mobile.

Vue.js vs React.js : quel Framework pour son projet ?
4/12/2023

À l'ère du développement web, choisir le bon Framework frontend peut définir le succès de votre projet. Aujourd'hui, plongeons dans le débat Vue.js vs React.js. Quel géant du JavaScript convient le mieux à votre vision ? C'est la question à laquelle nous répondrons dans cette comparaison détaillée. Explorez les fondamentaux, découvrez les différences clés, et faites un choix éclairé pour votre prochaine aventure digitale.

Les fondamentaux de Vue.js et React.js

Entamons notre exploration en mettant en lumière les caractéristiques distinctives de Vue.js et React.js, deux incontournables du développement JavaScript.

Vue.js : une approche progressive

Vue.js se démarque par son approche incrémentielle. Son système de composants facilite l'intégration avec des projets existants, offrant une simplicité appréciée. L'outil Vue CLI permet un démarrage rapide, un atout pour les projets de toutes envergures.

La partie écrite en HTML propose une balise div avec l'id app, qui est la zone d'application que Vue.js cible.

À l'intérieur de cette balise, nous avons un bouton avec un événement @click qui déclenche la méthode showMessage() lorsque le bouton est cliqué. Le paragraphe avec la directive v-if  s'affiche uniquement lorsque la propriété messageVisible est true.

Enfin, la partie script JavaScript est écrite en Vue.js. Elle créée une nouvelle instance de Vue avec les données et les méthodes nécessaires. Lorsque le bouton est cliqué, la méthode showMessage() est appelée pour rendre le message visible.

En résumé, ce code Vue.js crée une application basique avec un bouton qui, une fois cliqué, fait apparaître un message dans un paragraphe. La logique de rendu conditionnel est gérée par les propriétés réactives de Vue.js (messageVisible, dans ce cas).

React.js : la puissance du Virtual DOM

React.js brille avec le concept de Virtual DOM, améliorant significativement les performances. Sa flexibilité s'étend au-delà des applications web, permettant le développement d'applications mobiles avec React Native. Create React App offre, quant à lui, une entrée en matière rapide pour les nouveaux projets.

Une image contenant texte, capture d’écran, logiciel, ordinateurDescription générée automatiquement

Dans cet exemple, nous utilisons la fonction useState() de React pour déclarer et initialiser l'état du composant fonction App. Sa syntaxe est simplifiée grâce à l'utilisation d’une fonction fléchée.

Pour des projets réels, il serait préférable d'utiliser des outils comme Create React App ou Nextjs pour une configuration plus complète et une structure de dossier organisée.

Comparaison directe

Plongeons maintenant dans une analyse détaillée des différences clés entre Vue.js et React.js, mettant en lumière les points qui pourraient orienter votre choix.

Performance et DOM virtuel

En matière de performances, Vue.js excelle avec son système de rendu réactif. React.js, quant à lui, mise sur la puissance du Virtual DOM pour des applications fluides et réactives. Un point crucial à considérer selon les exigences de votre projet.

Facilité d'apprentissage et courbe d'apprentissage

Vue.js se distingue par sa courbe d'apprentissage douce, idéale pour les débutants. À l'inverse, React.js offre une courbe d'apprentissage plus prononcée, mais son écosystème robuste attire les développeurs expérimentés. Un choix à faire en fonction de votre équipe et de vos délais.

Composants et modularité

Vue.js brille avec son système de composants intuitif, favorisant la réutilisabilité. React.js propose également une approche modulaire, mais dans un écosystème plus vaste. Choisissez en fonction de la complexité de votre application et de vos besoins en modularité.

Choisir en fonction de vos besoins

Maintenant que nous avons disséqué les différences, concentrons-nous sur la prise de décision. Comment choisir entre Vue.js et React.js en fonction de vos besoins spécifiques ? C'est ce que nous allons explorer dans cette section.

Vue.js excelle dans le développement d'applications web, offrant simplicité et efficacité. À l'inverse, React.js élargit son spectre en permettant également le développement d'applications mobiles grâce à React Native. La nature de votre projet guidera ce choix crucial.

Vue.js se distingue par sa simplicité d'intégration avec HTML et CSS, facilitant la transition pour les développeurs. React.js adopte une approche JSX, plus proche du JavaScript pur. La préférence pour l'une ou l'autre dépendra de votre équipe et de vos préférences de codage.

Communautés actives et support

Entrons maintenant dans le monde des communautés, un aspect essentiel pour le succès continu d'un Framework. Voyons comment Vue.js et React.js se positionnent en termes de soutien et de ressources.

La communauté Vue.js connaît une croissance significative. Des forums actifs, des tutoriels, et un soutien en ligne abondant font de Vue.js une option attrayante pour ceux qui apprécient une communauté en pleine expansion.

Vue.js dispose d'un forum officiel où les développeurs de tous niveaux peuvent poser des questions, partager leurs expériences et discuter des meilleures pratiques. La communauté est connue pour être accueillante et réactive.

Les meetups et les conférences dédiés à Vue.js sont organisés régulièrement dans le monde entier. Ils offrent une excellente occasion de rencontrer d'autres développeurs, de partager des idées et d'en apprendre davantage sur les nouvelles fonctionnalités et les meilleures pratiques.

La documentation officielle de Vue.js est très complète et bien organisée. Elle propose des guides, des exemples concrets et une référence exhaustive pour aider les développeurs à comprendre et à utiliser Vue.js de manière efficace.

Vue Mastery propose des cours en ligne avancés pour les développeurs Vue.js de tous niveaux. Ces cours couvrent une variété de sujets, de l'apprentissage des bases à des sujets avancés tels que la gestion d'état avancée.

React.js, avec l'une des plus grandes communautés, offre une richesse de ressources et de forums de discussion. La stabilité et la maturité de la communauté React.js en font un choix rassurant pour ceux qui recherchent une base solide de soutien.

Le répertoire GitHub de React est un centre actif de collaboration. Les développeurs peuvent signaler des problèmes, proposer des fonctionnalités, et contribuer directement au développement du Framework.

Reactiflux est une communauté React sur Discord où les développeurs peuvent discuter en temps réel, poser des questions et partager leurs expériences.

La documentation officielle de React est exhaustive et mise à jour régulièrement. Elle couvre tout, de l'installation à des sujets avancés tels que les Hooks, les Context API, et la gestion d'état.

La communauté React est très active sur Stack Overflow. Les développeurs peuvent poser des questions et obtenir des réponses rapides de la part de la communauté.

Nos conseils pour optimiser votre choix

Nous arrivons à la phase cruciale de la prise de décision. Comment optimiser votre choix entre Vue.js et React.js ? Découvrons des conseils pratiques pour guider cette étape.

Vue.js se démarque par son accent sur une expérience utilisateur fluide. Ses fonctionnalités réactives et sa simplicité d'utilisation en font un choix idéal pour des applications offrant une expérience utilisateur exceptionnelle.

Quel que soit le choix, misez sur la qualité. Évitez des refontes coûteuses en privilégiant la qualité dès le début. Des applications bien conçues offrent une base solide pour le succès à long terme, que vous optiez pour Vue.js ou React.js.

En fin de compte, le choix entre Vue.js et React.js est personnel, dépendant de vos besoins spécifiques, de la nature de votre projet, et de vos préférences. Dans la prochaine section, rappelons l'essentiel de cette comparaison et offrons une vision claire pour vous aider à faire le choix éclairé qui propulsera votre projet vers le succès.

En conclusion, le choix entre Vue.js et React.js est une décision cruciale, dépendante de la nature unique de votre projet. Que vous soyez séduit par l'approche progressive de Vue.js ou par la puissance du Virtual DOM de React.js, l'essentiel est de faire un choix éclairé.

À travers cette comparaison, nous avons exploré les fondamentaux, examiné les différences clés, et offert des conseils pratiques pour guider votre décision.

N'oubliez pas : la clé du succès réside souvent dans la simplicité. Optez pour le Framework qui s'aligne le mieux avec vos besoins, tout en privilégiant la qualité dès le départ. Que vous choisissiez Vue.js ou React.js, transformons ensemble votre vision en une réalité digitale.

Si des questions persistent ou si vous avez besoin d'un accompagnement plus approfondi, n'hésitez pas à nous contacter. Chez Yield Studio, nous sommes là pour concrétiser vos projets avec expertise et engagement.

Design Pattern : Compound Components
25/6/2024

Introduction

En tant que développeurs, nous savons que les projets évoluent constamment : les besoins changent, les designs se métamorphosent et les spécifications initiales peuvent rapidement devenir obsolètes.

Face à cet environnement mouvant, nos composants traditionnels montrent parfois leurs limites. Ils ne sont pas tous adaptés pour faire face de manière flexible et robuste à cette évolution constante.

Qui n'a jamais été frustré par un composant trop rigide pour s'accommoder d'un changement de maquette ou d'une mise à jour des exigences du projet ?

Examinons ensemble, deux exemples, pour illustrer le Design Pattern : Compound Components.

Exemple d’un composant d’UI simple ✏️

Supposons que nous devons créer un composant Card tout ce qu’il y a de plus classique. On a besoin d’affiche un title, une description et un thumbnail.

Voilà une implementation simple de ce que pourrait être ce composant :

// card.tsx

type CardProps = {
	title: string;
	description: string;
	thumbnail: string;
}

function Card({ title, description, thumbnail }: CardProps) {
	return (
		<View>
			<Image source={{ uri: thumbnail }} />
			<Text>{title}</Text>
			<Text>{description}</Text>
		</View>
	)
}

Ainsi que son usage :

// home.tsx

function HomeScreen() {
	return (
		<View>
			<Card
				title="Lorem ipsum"
				description="Quis enim aliqua ad et consectetur laboris reprehenderit ea anim occaecat adipisicing duis exercitation magna cupidatat."
				thumbnail="https://picsum.photos/200/300"
			/>
		</View>
	)
}

Jusque là, tout va bien, notre composant est simple à développer, simple à utiliser et facile à relire.

Maintenant, comme dans tous les projets, le besoin évolue et le design de nos composants avec. Admettons, que notre besoin a évolué de façon à ce qu’on ai besoin d’ajouter un bouton sur notre composant Card. Mais, ce bouton ne doit pas apparaître à tous les endroits de mon application.

Ce que l’on va retrouver dans la plupart des projets professionnels aujourd’hui, c’est une surcharge de propriétés sur le composant. Le plus souvent, notre composant serait comme suit :

// card.tsx

type CardProps = {
	title: string;
	description: string;
	thumbnail: string;
	buttonLabel?: string;
	showButton?: boolean;
	onPressButton?: () => void;
}

function Card({ 
	title, 
	description, 
	thumbnail,
	buttonLabel,
	showButton = false,
	onPressButton
}: CardProps) {
	return (
		<View>
			<Image source={{ uri: thumbnail }} />
			<Text>{title}</Text>
			<Text>{description}</Text>
			{showButton && <Button label={buttonLabel} onPress={onPressButton} />}
		</View>
	)
}


NB : c’est volontairement exagéré pour mettre en avant le problème. Même sans Compound Components que l’on verra après, on pourra avoir un composant bien plus propre !

Et l’usage du composant serait comme suit :

// home.tsx

function HomeScreen() {
	return (
		<View>
			<Card
				title="Lorem ipsum"
				description="Quis enim aliqua ad et consectetur laboris reprehenderit ea anim occaecat adipisicing duis exercitation magna cupidatat."
				thumbnail="https://picsum.photos/200/300"
			/>
			<Card
				title="Lorem ipsum"
				description="Quis enim aliqua ad et consectetur laboris reprehenderit ea anim occaecat adipisicing duis exercitation magna cupidatat."
				thumbnail="https://picsum.photos/200/300"
				buttonLabel="Lorem"
				onPressButton={() => { /* ... */}}
				showButton
			/>
		</View>
	)
}


Observations

Que peux-tu observer sur cet exemple de composant “traditionnel” qui ne représente que de l’UI ?

  1. Structure rigide
    Le composant Card a une structure définie, il contient toujours une image, un titre, une description et un bouton. Il n’y a flexibilité pour changer la structure d’un composant en fonction des besoins.
  2. Passage de props
    Toutes les données dont le composant Card a besoin sont passées via les props. Cela peut devenir encombrant et difficile à maintenir à mesure que nous ajoutons plus de props au composant.
  3. Moins de réutilisabilité
    Les sous-composants ne peuvent pas être réutilisés indépendamment. Par exemple, si nous voulons utiliser seulement le bouton ou l'image de la carte dans un autre composant, cela ne serait pas possible.
  4. Peu extensible
    Ajouter de nouvelles fonctionnalités à la carte nécessite une modification de l'implémentation de la carte elle-même, augmentant potentiellement le risque de créer des bugs non liés.
  5. Simplicité
    Cependant, dans certains cas, cette approche peut être préférable pour sa simplicité. Si votre composant est très simple et n'a pas besoin des avantages offerts par le pattern de Compound Components, le surcoût en complexité peut ne pas en valoir la peine.

Exemple d’un composant plus complexe ✏️

Supposons maintenant que nous devons créer un composant plus complexe, des composants mêmes, qui ont besoin de travailler ensuite pour mettre en oeuvre une fonctionnalité de Todo-list.

Pour cela, nous allons avoir les composants TodoList (pour afficher une liste d’item de todo), TodoItem (qui représente un item de todo), TodoForm (qui représente le formulaire d’un item de todo) et TodoStats (qui affiche des statistiques pour une liste de todo donnée).

Voilà une implementation de ce que pourrait être ces composants :

// todo-list.tsx

type TodoListProps = {
	todos: Array<{ id: string; content: string }>;
	onPressDelete: (id: string) => void;
}

function TodoList({ todos, onPressDelete }: TodoListProps) {
	return (
		<View>
			{todos.map((todo) => (
				<TodoItem 
					key={todo.id} 
					id={todo.id} 
					content={todo.content}
					onPressDelete={onPressDelete}
				/>
			)}
		</View>
	)
}

// todo-item.tsx

type TodoItemProps = {
	id: string;
	content: string;
	onPressDelete: (id: string) => void;
}

function TodoItem({ id, content onPressDelete }: TodoListItemProps) {
	return (
		<View>
			<Text>{content}</Text>
			<Button label="Delete" onPress={() => onPressDelete(id)} />
		</View>
	)
}

// todo-form.tsx

type TodoFormProps = {
	onPressSubmit: (content: string) => void;
}

function TodoForm({ onPressSubmit }: TodoFormProps) {
	const [value, setValue] = useState<string>('')
	
	return (
		<View>
			<TextInput value={value} onChangeText={setValue} />
			<Button label="Add" onPress={() => onPressSubmit(value)} />
		</View>
	)
}

// todo-stats.tsx

type TodoStatsProps = {
	todos: Array<{ id: string; content: string }>;
}

function TodoStats({ todos }: TodoStatsProps) {
	return (
		<View>
			<Text>Sum of todos: {todos.length}</Text>
			{/* average number of characters */}
			{/* ... */}
		</View>
	)
}


Ainsi que l’usage de ces composants :

// home.tsx

function HomeScreen() {
	const [todos, setTodos] = useState([])
	
	return (
		<View>
			<TodoList 
				todos={todos} 
				onPressDelete={(id) => 
					setTodos((state) => state.filter(todo) => todo.id !== id
				}
			/>
			<TodoStats todo={todos} />
			<TodoForm 
				onPressSubmit={(content) =>
					setTodos((state) => [...state.todos, { id: uuid(), content }])
				} 
			/>
		</View>
	)
}


Observations

Que peux-tu observer sur cet exemple de composant “traditionnel” qui ne représente cette fois une fonctionnalité plus complexe ?

  1. Rigidité
    Dans l'état actuel, la structure est assez rigide. Par exemple, si vous voulez une autre variante de TodoItem qui a un bouton pour marquer une tâche comme terminée, ou peut-être une variante de TodoForm qui a des champs supplémentaires, l'adaptation de ces composants à ces scénarios serait plus complexe.
  2. Passage de props
    Les fonctions de suppression et d'ajout sont transmises en tant que props aux composants enfants TodoItem et TodoForm depuis le composant parent HomeScreen. Cela peut devenir compliqué à gérer à mesure que l'application s'agrandit, car chaque fois que vous voulez utiliser ces fonctions, vous devez les transmettre à travers tous les composants intermédiaires.
  3. Manque d’encapsulation
    Les composants TodoItem et TodoForm exposent trop de détails d'implémentation. Par exemple, TodoItem a besoin de connaître non seulement le contenu de la tâche, mais aussi son id et comment traiter une action de suppression. De même, TodoForm doit gérer son propre état et savoir comment gérer une action de soumission. Cela pourrait être évité avec une version composée qui masquerait ces détails.
  4. Peu extensible et peu lisible
    Ce point est suffisamment explicite je pense !

Le Design Pattern : Compound Components 👀

Le Design Pattern : Compound Components s’applique à n’importe quel langage fonctionnant avec des composants et de la gestion d’états. Il s’agit d’une approche qui offre :

  1. Structure
    Le terme "Compound Components" décrit une relation "a un" entre les composants. Un composant comporte plusieurs sous-composants qui travaillent ensemble pour former une unité cohérente. Le composant parent sert de composant de mise en page tandis que les sous-composants déterminent le contenu.
  2. Flexibilité
    Les Compound Components offrent une grande flexibilité dans l'arrangement des sous-composants. Les utilisateurs de cette API de composant peuvent contrôler l'organisation, la structure et la présentation d'un composant.
  3. Abstraction
    Ils permettent une bonne séparation des préoccupations car chaque sous-composant traite une fonctionnalité particulière. Cela permet une meilleure réutilisation des composants et simplifie le test et la maintenance.
  4. Pas de passage de props
    Un avantage majeur du modèle de Compound Components est l'évitement du prop-drilling, qui est un problème où des props doivent être passés à travers de nombreux niveaux de composants. Les Compound Components résolvent ce problème en utilisant le contexte React pour partager la valeur entre les composants.
  5. Encapsulation
    Avec les Compound Components, nous pouvons exposer ce qui est nécessaire et masquer les détails d'implémentation spécifiques. Cela aide à produire un code plus clair et plus facile à maintenir.

Mise en pratique

Maintenant, voyons ensemble un refactor de nos composants précédents en version Compound Components.

Le composant d’UI simple en Compound Components

Reprenons notre composant d’UI simple et convertissons les props title, description, etc. en sous-composants pour en faire une composition comme suit :

// card.tsx

type CardProps = PropsWithChildren

function Card({ children }: CardProps) {
	return <View>{children}</View>
}

Card.Title = CardTitle
Card.Description = CardDescription
Card.Thumbnail = CardThumbnail
Card.Button = CardButton

// card-title.tsx

type CardTitleProps = {	title: string }

function CardTitle({ title }: CardTitleProps) {
	return <Text>{title}</Text>
}

// card-description.tsx

type CardDescriptionProps = {	description: string }

function CardDescription({ description }: CardDescriptionProps) {
	return <Text>{description}</Text>
}

// card-thumbnail.tsx

type CardThumbnailProps = {	source: string }

function Card({ source }: CardThumbnailProps) {
	return <Image source={{ uri: thumbnail }} />
}

// card-button.tsx

type CardButtonProps = {
	label: string;
	onPress: () => void;
}

function CardButton({ label, onPress }: CardButtonProps) {
	return <Button label={label} onPress={onPress} />
}


Et maintenant l’usage :

// home.tsx

function HomeScreen() {
	return (
		<View>
			<Card>
				<Card.Thumbnail source="https://picsum.photos/200/300" />
				<Card.Title title="Lorem ipsum" />
				<Card.Description 
					description="Quis enim aliqua ad et consectetur laboris reprehenderit ea anim occaecat adipisicing duis exercitation magna cupidatat."
				/>
			</Card>
			<Card>
				<Card.Thumbnail source="https://picsum.photos/200/300" />
				<Card.Title title="Lorem ipsum" />
				<Card.Description 
					description="Quis enim aliqua ad et consectetur laboris reprehenderit ea anim occaecat adipisicing duis exercitation magna cupidatat."
				/>
				<Card.Button label="Lorem" onPress={() => { /* ... */}} />
			</Card>
		</View>
	)
}


Observations

Quelles observations peux-tu faire cette fois ci ?

  1. Flexibilité
    Le Compound Components donne un plus grand contrôle sur l'organisation des éléments dans le rendu. Dans le deuxième exemple d'utilisation, nous avons de l'information supplémentaire et une absence de bouton, ce qui ne serait pas possible avec une version non composée du composant qui limiterait strictement la structure.
  2. Réutilisabilité
    Les sous-composants, comme CardTitle, CardImage, et CardContent peuvent être réutilisés et réarrangés librement. Cette approche réduit la duplication du code et accroît la maintenabilité.
  3. Lisibilité
    Le code est plus facile à comprendre. Alors qu'un composant non composé pourrait avoir un grand nombre de props, ce qui pourrait rendre le code plus difficile à suivre, chaque sous-composant sait clairement quel est son rôle dans le composant de carte.
  4. Isolation
    Les sous-composants (comme CardButton ou CardImage) peuvent être mis à jour indépendamment des autres sous-composants, évitant ainsi les effets de bord inattendus.
  5. Scalabilité
    Les nouveaux sous-composants peuvent être ajoutés facilement en suivant cette approche, permettant au composant de s'adapter et de se développer avec le temps. Par exemple, un sous-composant CardFooter pourrait être ajouté si besoin.

Le composant complexe en Compound Components

Enfin, passons au plus intéressant, le groupe de composant qui représente la fonctionnalité de Todo-list, voilà la version Compound Components :

// home.tsx

function HomeScreen() {
	return (
		<View>
			<Card>
				<Card.Thumbnail source="https://picsum.photos/200/300" />
				<Card.Title title="Lorem ipsum" />
				<Card.Description 
					description="Quis enim aliqua ad et consectetur laboris reprehenderit ea anim occaecat adipisicing duis exercitation magna cupidatat."
				/>
			</Card>
			<Card>
				<Card.Thumbnail source="https://picsum.photos/200/300" />
				<Card.Title title="Lorem ipsum" />
				<Card.Description 
					description="Quis enim aliqua ad et consectetur laboris reprehenderit ea anim occaecat adipisicing duis exercitation magna cupidatat."
				/>
				<Card.Button label="Lorem" onPress={() => { /* ... */}} />
			</Card>
		</View>
	)
}

Ainsi que son usage, drastiquement simplifiée :

// home.tsx

function HomeScreen() {
	return (
		<View>
			<Todos todos={[]}>
				<Todos.List />
				<Todos.Stats />
				<Todos.Form />
			</Todos>
		</View>
	)
}


Observations

Que peux t-on observer sur cette dernière partie ?

  1. Flexibilité d’affichage
    Avec l'approche de Compound Components, la disposition des composants est beaucoup plus flexible. Vous pouvez choisir de rendre Todos.List, Todos.Form, et Todos.Stats dans n'importe quel ordre ou même de ne pas les afficher en fonction des spécificités des spécifications ou des besoins de votre application.
  2. Utilisation du Contexte
    Grâce à l'utilisation de React Context (TodosContext), vous pouvez facilement partager des données (todos) et des fonctions (add, remove) entre tous les composants enfants. Cela permet d'éviter le problème de prop-drilling propre à l'approche non compound components.
  3. Hook personnalisé
    Ils utilisent un hook personnalisé useTodosContext pour obtenir les valeurs du contexte. Ce hook rend le code plus lisible et plus facile à utiliser.
  4. Réutilisabilité accrue
    Les composants sont désormais plus indépendants et peuvent être facilement réutilisés ailleurs dans l'application. Par exemple, Todos.List pourra être utilisé dans un autre écran ou dans une sidebar sans avoir besoin de passer d'informations supplémentaires via les props.
  5. Extensibilité
    Avec cette approche, vous pouvez également étendre facilement le composant Todos en ajoutant des sous-composants supplémentaires sans bouleverser l'architecture existante. Par exemple, si vous voulez ajouter une fonctionnalité pour marquer les tâches comme faites, vous pourriez créer un nouveau sous-composant Todos.Checkbox.

Le mot de la fin 👋

De manière générale, le composant “traditionnel” est plus simple, mais il offre moins de flexibilité et de potentiel de réutilisation que le Compound Components. Le choix entre les deux approches dépend des besoins spécifiques du projet. Mais de mon experience, partir direct sur du Compound Components est rarement une mauvaise idée !

Les inconvénients potentiels de cette approche sont qu'elle est plus complexe et qu'elle nécessite une compréhension plus approfondie des concepts de React (pour le cas de React), tels que le Contexte et les Compound Components eux-mêmes. De plus, il est important de noter que bien que le Context puisse sembler être une solution à tous les problèmes, il doit être utilisé avec parcimonie pour éviter un couplage excessif entre les composants de votre application.

L'adoption du pattern Compound Components dans la conception d'interfaces utilisateur peut sembler déroutante au début, mais les avantages qu'elle offre en termes de modularité, de flexibilité et de réutilisabilité sont indéniables. Ainsi, en décomposant intelligemment les composants en des sous-éléments logiques, nous pouvons produire des systèmes d'UI flexibles, réutilisables et gérables.

Vous pouvez retrouvez cet article au format vidéo sur YouTube en suivant ce lien.

Échangeons sur votre projet !

Développement web
Application mobile
Design & Product
Nous contacter

Simulateur

Bienvenue dans le
simulateur d’estimation

Sélectionnez
vos besoins

Sélectionnez un ou plusieurs choix

Définissez les
fonctionnalités

Sélectionnez un ou plusieurs choix

Dernière
étape !

Renseignez votre adresse mail pour recevoir l’estimation !
Obtenez l’estimation
Précédent
Suivant

Bravo ! Vous avez terminé
l’estimation de votre future app !

Vous recevrez dans votre boite mail l’estimation personnalisé. Une estimation vous offre la possibilité de vous projeter dans un budget, vous permettant ainsi de planifier en toute confiance. Néanmoins, chez Yield, nous adoptons une approche agile, prêts à remettre en question et ajuster nos évaluations en fonction de l'évolution de vos besoins et des spécificités de votre projet.
Retour au site
Oops! Something went wrong while submitting the form.