Test Driven Development (TDD) : Guide pour un Développement Efficace

Découvrez le Test Driven Development : avantages, meilleures pratiques et impact sur la qualité du code. Maîtrisez l'art du TDD aujourd'hui !

L'origine


Le Test-Driven Development plus communément appelé TDD n'a pas été inventé par une seule personne mais par plusieurs développeurs. Kent Beck est souvent la personne la plus affiliée au TDD parce qu'il est celui qui l'a popularisé lors de ses travaux sur la méthodologie Extreme Programming à la fin des années 90. Néanmoins il est important de rappeler qu'il n'est pas le créateur de l'outil qu'est le TDD, il a été mis en place pour tout un tas de personne, dont notamment Martin Fowler (co-auteur du Manifeste Agile) ou encore Ward Cunningham et Ron Jeffries qui ont fondé l'Extreme Programming avec Kent Beck.


Un outil de travail


L'outil


Le TDD est un outil de travail qui permet au développeur de coder en étant guidé par les tests. Il permet d'avancer étape par étape, petit pas par petit pas, pour avancer vers une solution toujours plus fiable.


Cette solution plus fiable est notamment due à sa forte proximité avec les principes SOLID. En effet, le TDD facilite la mise en place des principes SOLID. En écrivant d'abord des tests, il est plus naturel de concevoir un code simple et cohérent qui respecte les principes tels que l'encapsulation, le découplage, la gestion des dépendances et la séparation des responsabilités. On se retrouve plus naturellement avec des modules indépendants et l'injection de dépendances, ce qui conduit à du code plus propre.


Le TDD et les principes SOLID vont donc de pair pour une conception logicielle de qualité.



  1. Nous avons une première phase (rouge sur le schéma) où nous devons écrire un test qui échoue, basé sur une spécification. NB : on n'écrit pas de code de production (code final) tant que le test n'est pas en échec.
  2. La deuxième phase (verte sur le schéma) consiste à faire passer le test en succès en écrivant du code de production et tout en restant le plus simple possible. NB : on écrit uniquement le code nécessaire pour faire passer le test et sans retourner dans le code du test.
  3. La dernière phase (bleue sur le schéma) concerne le refactoring. On met à jour le code en appliquant les bonnes pratiques, le clean code, etc. Une fois que le refactoring est fait, si les tests passent toujours, on conserve le refactor et on passe aux prochaines spécifications, sinon, on annule le refactor et on essaye autre chose. NB : ne doit jamais effectuer de refactoring dans les autres phases, seulement ici.


L'approche TDD est un développement itératif et incremental qui met l'accent sur la qualité du code.


Le manifeste


Le TDD respecte certains principes qui sont :


  • Baby steps instead of large-scale changes

On avance petit pas par petit pas pour avoir une boucle de feedback rapide et régulière.


  • Continuous refactoring instead of late quality improvements

On améliore le code en continue, on s'en occupe tout de suite parce qu'un refactor annoncé pour plus tard n'arrive finalement en général jamais.


  • Evolutionary design instead of big design up front

On développe ce qui est nécessaire et suffisant et on évolue progressivement.


  • Executable documentation instead of static documents

Les tests mis en place pendant le TDD sont en réalité une documentation executable. L'idée est de lier la documentation avec le code pour s'assurer qu'elle est bien à jour et maintenue.


  • Minimalist code instead of gold-plated solution

Un code simple et qui fonctionne plutôt qu'une solution surdimensionnées avec un niveau de complexité bien trop élevé et pas nécessaire.




Le test propre

Given When Then


L'approche Given When Then est basée sur une convention développée dans le cadre du Behaviour-Driven Development autrement appelé BDD. Il s'agit d'une approche de développement axée sur la collaboration et la spécification du comportement à travers ds scénarios clairs et compréhensibles.


En utilisant cette convention on divise le test en trois parties :

  1. Given, la condition préalable au test
  2. When, l'exécution du système testé
  3. Then, le comportement attendu


Exemple : Given user is not logged in When user logs in Then user is logged in successfully


Should When


L'approche Should When est une convention de nommage facile à lire et plus largement utilisée.


En utilisant cette convention on divise le test en deux parties :

  1. When, la condition préalable au test
  2. Should, le comportement attendu


Exemple : Should have user logged in When user logs in


Arrange Act Assert


Le modèle Arrange Act Assert autrement appelé AAA est un pattern descriptif et révélateur des intentions pour structurer le test.


Le test est alors organisé de la manière suivante :

  1. La partie Arrange contient la logique de configuration du test. C'est ici qu'on initialise le test.
  2. La partie Act execute le système que l'on souhaite tester. C'est ici qu'on fait l'appel d'une fonction, d'un composant, d'un call API, etc.
  3. La partie Assert, vérifie que le système testé se comporte comme prévu. C'est ici qu'on vérifie que le résultat obtenu correspond au résultat attendu.


Exemple :




F.I.R.S.T.


Le principe F.I.R.S.T. est un ensemble de principes qui définissent les caractéristiques d'un test propre et de qualité.


Ces caractéristiques sont les suivantes :

  • Fast, un test doit être rapide et efficace de manière à pouvoir être exécuté fréquemment pour avoir un feedback régulier
  • Independent, les tests doivent être indépendants les uns des autres afin d'être exécutable individuellement et efficacement
  • Repeatable, un test doit être répétable dans n'importe quel environnement et à tout moment
  • Self-Validating, les tests doivent retourner un succès ou un échec afin de vérifier lui même si l'exécution du test a réussi ou échoué sans évaluation manuelle
  • Timely, les tests doivent être écrit avant ou en même temps que le code de production. ils doivent être maintenus et exécutés régulièrement


Quand l'utiliser ?


Quand ne pas l'utiliser plutôt !


Il n'est pas nécessaire et pas utile de faire du TDD quand on a pas de spécifications, quand les tests n'apportent rien, quand les tests sont trop lents, quand il n'y a pas de logique.


La démo


L'incontournable Fizz Buzz


Le Fizz Buzz est un exercice populaire qui permet d'appréhender la méthode TDD. Ce n'est qu'un échantillon et qu'un début de la méthode, mais ça reste intéressant.


Les spécifications sont les suivantes :

  • On commence à compter à partir de 1 jusqu'à 100
  • Lorsqu'on rencontre un nombre divisible par 3, on retourne "Fizz"
  • Lorsqu'on rencontre un nombre divisible par 5, on retourne "Buzz"
  • Lorsqu'on rencontre un nombre divisible par 3 et par 5, on retourne "Fizz-Buzz"


Nous allons utiliser le TypeScript pour mettre en place cet algorithme et le tester.


Première spécification


On créé un fichier test fizz-buzz.test.ts et on créé notre premier test qui va gérer la spécification où on teste le nombre 1.





Ici, le test ne passe pas parce qu'on a pas encore créé la fonction fizzBuzz() et encore moins l'algorithme associé. Nous sommes donc à la première phase du TDD, la phase rouge, celle où on écrit un test qui échoue.


On va maintenant passer à la deuxième phase du TDD, celle où on va faire passer le test au vert. Pour cela, on va créer le fichier fizz-buzz.ts et écrire le code permettant de gérer notre cas de spécification.




Ici, on pourrait avoir tendance à faire un return String(n) directement, mais TDD nous dit de commence par écrire le code le plus simple possible pour faire passer le test au vert, et en réalité le plus simple et rapide et de tout simplement retourner 1 directement.


On va passer au prochain test qui est de tester l'input 2.



Le test échoue parce qu'on a pas encore géré ce cas dans notre fonction. On retourne donc dans notre fonction et on essaye de résoudre ce cas de la manière la plus simple et rapide.




Ici, encore une fois, le plus rapide est de retourner 2 si on a 2 en input.


Maintenant on arrive à la troisième et dernière phase du TDD, celle du refactor. En effet, actuellement nos tests passent avec succès, le code est simple, mais il pourrait l'être encore plus en appliquant de bonnes pratiques.


On va donc revenir sur notre fonction fizzBuzz() sans modifier les tests.




Notre fonction est maintenant simple, propre et concise et les tests sont toujours verts. Notre refactor est donc réussit, on peut passer à la prochaine spécification.


Deuxième spécification


Nous devons maintenant faire en sorte de retourner Fizz si l'input est divisible par 3.



Le test échoue, on va maintenant gérer le cas du Fizz dans la fonction.




Le code le plus simple pour retourner  Fizz quand on a 3 et de tester si n === 3.


On va maintenant gérer un deuxième cas où on a un nombre divisible par 3.




Le test échoue, on met à jour le code.




Le code le plus simple pour retourner Fizzquand on a 3 ou 6 et de faire un ||, mais on se rend bien compte qu'on peut améliorer le code en utilisant le modulo de 3. Les tests sont tous verts, donc on peut se permettre de passer à la phase de refactor en modifiant uniquement le code de la production.



Refactor terminé, tous les tests sont verts, on peut passer à la prochaine spécification.


Troisième spécification


Nous devons maintenant faire en sorte de retourner Buzz si l'input est divisible par 5.


On va aller un peu plus vite, mais le procédé est le même, on va d'abord faire un test avec un input 5 puis 10, puis refactor.




Dernière spécification


Nous devons maintenant faire en sorte de retourner Fizz-Buzz si l'input est divisible par 3 et par 5.


Comme pour les spécifications précédentes, on va faire un test avec un input 15 puis 30 et enfin refactor.


Et voilà !


Le mot de la fin


Stop aux amalgames ! Le TDD n'est pas un outil pour avoir une bonne couverture de code avec les tests, ce n'est pas simplement "faire des tests". Le TDD est un outil dont le but est de guider le développeur vers un objectif. Il permet de donner un feedback régulier afin de s'assurer qu'on est toujours sur la bonne voie. Il faut le voir comme un GPS, qui nous donne des directions à suivre (tourner à droite, aller tout droit pendant 2km, etc.) jusqu'à atteindre un objectif.


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.

Comment envoyer des e-mails et SMS transactionnels avec Brevo sur Laravel ?
3/9/2024

Si vous développez une application SaaS, un réseau social, une marketplace ou encore une application métier, il est fort probable que vous aillez besoin d’envoyer des e-mails ou des SMS transactionnels à vos utilisateurs.

Si vous vous demandez ce qu’est un message transactionnel, c’est un message automatisé qui fournit aux utilisateurs des informations utiles qui visent à l’aider et améliorer son expérience. Parmi les exemples les plus courants, on retrouve les confirmations de commande, les rappels de rendez-vous et l'authentification à deux facteurs.

Exemple d’un e-mail et d’un SMS transactionnel

L’envoi d’e-mails et de SMS transactionnels avec Laravel est un jeu d’enfant, cette simplicité nous la devons au composant Notifications.

Laravel embarque le support de certains canaux : SMTP, Vonage et Slack. Les développeurs peuvent ensuite intégrer leurs propres canaux et les partager à la communauté sous forme de librairies.

C’est ce que nous avons fait chez Yield Studio en développant des packages pour Brevo, Mailjet et Expo !

Dans ce tutoriel, nous allons vous expliquer étape par étape comment envoyer des e-mails et des SMS transactionnels avec yieldstudio/laravel-brevo-notifier !

Étape 1 : Créer un compte Brevo

Pour commencer, vous devez vous créer un compte sur Brevo et vérifier votre adresse e-mail. Après ça vous pourrez vous rendre sur le tableau de bord.

Le plan Gratuit de Brevo vous permet d’envoyer jusqu’à 300 e-mails par jour, quant au SMS vous devrez charger votre compte avec des crédits SMS, à titre d’exemple le pack de 100 crédits SMS à destination de la France est facturé 4,5€.

Étape 2 : Installer Laravel Brevo Notifier

Après avoir créé votre compte Brevo, nous pouvons continuer l’installation.


composer require yieldstudio/laravel-brevo-notifier

Une fois l'installation terminée, nous pouvons éditer le fichier .env et ajouter les variables suivantes avec vos propres valeurs.


BREVO_KEY=your-brevo-key
BREVO_SMS_SENDER=Yield

# Laravel variables
MAIL_FROM_ADDRESS=hello@yieldstudio.fr
MAIL_FROM_NAME=Yield Studio

La valeur de BREVO_KEY peut être obtenue sur votre tableau de bord dans SMTP et API.

Créer une clé API sur Brevo

Quant à la valeur de BREVO_SMS_SENDER, elle est limitée à 11 caractères alphanumériques ou 15 caractères numériques. Il s’agit de l’expéditeur que verra votre utilisateur lors de l’envoi SMS (Yield, Amazon, IKEA)

Étape 3 : Authentifier vos expéditeurs

Avec Brevo comme avec de nombreux fournisseurs, les adresses e-mail qui servent à envoyer des e-mails doivent être une adresse authentifiée. Vous pouvez authentifier une adresse e-mail en particulier ou bien directement un nom de domaine.

Vous devez vous rendre dans Expéditeurs, domaines et IP dédiées et ajouter les adresses e-mails ou les domaines qui vous serviront à envoyer des e-mails.

Liste des expéditeurs autorisés sur Brevo

Étape 4 : Générer la Notification

Vous devez ensuite créer une classe pour votre notification, vous pouvez utiliser la commande Artisan ci-dessous, par exemple nous créons une notification OrderShipped


php artisan make:notification OrderShipped

Cette commande génère la classe dans le dossier App\Notifications. Si ce dossier n’existe pas encore, Laravel le créera pour nous.

Étape 5 : Envoyer un e-mail et un SMS

Assurez ensuite vous de changer le retour de la méthode via pour y ajouter notre BrevoSmsChannel  et/ou BrevoEmailChannel


<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use YieldStudio\LaravelBrevoNotifier\BrevoSmsChannel;
use YieldStudio\LaravelBrevoNotifier\BrevoEmailChannel;

class OrderShipped extends Notification
{
    public function via(): array
    {
        return [BrevoSmsChannel::class, BrevoEmailChannel::class];
    } 
}

Vous pouvez ensuite préparer votre notification en ajoutant la méthode toBrevoSms et/ou toBrevoEmail :


<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use YieldStudio\LaravelBrevoNotifier\BrevoSmsMessage;
use YieldStudio\LaravelBrevoNotifier\BrevoEmailMessage;

class OrderShipped extends Notification
{
    public function __construct(private Order $order) {
    }

    // ...
    
    public function toBrevoSms($notifiable): BrevoSmsMessage
    {
        return (new BrevoSmsMessage())
            ->to($notifiable->phone)
            ->content('La commande n°' . $this->order->number . ' a été expédiée.');
    }

    public function toBrevoEmail($notifiable): BrevoEmailMessage
    {
        return (new BrevoEmailMessage())
            ->templateId(1)
            ->to($notifiable->firstname, $notifiable->email)
            ->params(['order_number' => $this->order->number]);
    }
}

Pour envoyer un e-mail transactionnel avec Brevo, vous devez créer des Templates.

Vous pouvez maintenant envoyer votre notification depuis votre controller, un listener ou l’endroit le plus approprié dans votre cas :


<?php

namespace App\Http\Controllers;

use App\Notifications\SendSMSMessageNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

final class MarkOrderAsShipped extends Controller
{
    public function __invoke(Request $request, Order $order)
    {
        // ...
        $request->user->notify(new OrderShipped($order));
        // ...
    }
}

Et voilà vous avez envoyé votre première notification avec Brevo ⚡

Conclusion

Et voila, envoyer un e-mail ou un SMS transactionnel avec Brevo à partir d'une application Laravel c’est relativement simple. Créez d'abord un compte Brevo, installez la librairie yieldstudio/laravel-brevo-notifier, configurez et envoyez votre superbe notification.

Vous pouvez aller encore plus loin en ajoutant des destinataires en copie, des pièces jointes (une facture par exemple) et bien plus.

Sentez-vous libre de tester ce package et d’y contribuer sur Github.

L'Architecture hexagonale : Introduction
26/2/2024

Découvrons aujourd’hui l'univers captivant de l'architecture hexagonale. Cette approche du développement d'applications mobiles repose sur des principes fondamentaux, offrant une structure robuste et évolutive. Dans cet article, explorez les bases de l'architecture hexagonale, découvrez des exemples concrets sur GitHub et apprenez à l'intégrer avec Spring Boot. Optimisez votre code, maîtrisez l'inversion des dépendances et transformez votre façon de concevoir des applications mobiles.

Bienvenue dans le futur du développement logiciel !

Qu'est-ce que l'architecture hexagonale ?

L'architecture hexagonale redéfinit la conception des applications mobiles. À la base de cette approche novatrice se trouvent des principes clés, sculptant une structure en forme d'hexagone. Cette méthode se distingue par son agilité, son adaptabilité et son aptitude à créer des applications robustes. Découvrons les fondements de l'architecture hexagonale pour comprendre comment elle redéfinit le développement logiciel.

L'architecture hexagonale transcende les schémas conventionnels de développement logiciel. Imaginons-la comme une vue aérienne de votre application, où un hexagone représente le cœur, le noyau de votre système. Plongeons dans les détails de cette approche novatrice.

Au centre de cette structure, nous trouvons l'hexagone central. C'est le cœur, le noyau où réside la logique métier de votre application. Cet espace défini par l'hexagone représente l'essence même de ce que votre application offre à ses utilisateurs.

Les côtés de l'hexagone représentent les couches périphériques. Chacune de ces couches a un rôle spécifique dans l'interaction de l’application avec le monde extérieur. De la gestion des entrées/sorties à la persistance des données, ces couches entourent le noyau central, mais sans créer de dépendances directes avec lui.

Les interactions entre le cœur et les couches périphériques se font à travers des ports et adaptateurs. Les ports définissent des interfaces au sein du noyau, tandis que les adaptateurs fournissent des implémentations concrètes pour ces interfaces. Cette modularité offre une flexibilité essentielle, permettant à l'application de s'adapter sans altérer sa logique métier.

Enfin, l'inversion des dépendances est le principe qui gouverne l'architecture hexagonale. Plutôt que d'avoir des dépendances directes, le cœur de l'application dépend d'abstractions définies par les ports. Cette inversion crée un environnement flexible permettant des modifications sans impacter la stabilité du système.

Pour comprendre pleinement l'architecture hexagonale, explorons des exemples concrets. 

L'architecture hexagonale ne se contente pas de suivre les sentiers battus du développement logiciel. Imaginez-la comme une vue aérienne de votre application, où un hexagone représente le cœur, le noyau vibrant de votre système. Plongeons dans les détails pour rendre cette approche plus tangible.

Au cœur, l'hexagone central incarne la logique métier de votre application. C'est là que réside l'essence de ce que votre application offre à ses utilisateurs. Autour de ce noyau, les côtés de l'hexagone représentent les couches périphériques. De la gestion des entrées/sorties à la persistance des données, chaque couche a un rôle précis dans l'interaction de l'application avec le monde extérieur.

Exemples concrets

Imaginons une application de gestion de tâches où le cœur de l'hexagone représente la logique de gestion des tâches, des deadlines, etc. Les côtés de l'hexagone pourraient inclure une couche d'interface utilisateur, une couche de persistance des données, et une couche de services externes.

Les interactions entre le cœur et les couches périphériques s'effectuent à travers des ports et adaptateurs. Les ports définissent des interfaces au sein du noyau, tandis que les adaptateurs fournissent des implémentations concrètes pour ces interfaces. Cette modularité offre une flexibilité essentielle, permettant à l'application de s'adapter sans compromettre sa logique métier.

Supposons que le port "GestionTâchesPort" définisse les opérations nécessaires à la gestion des tâches. L'adaptateur "GestionTâchesAdapter" fournirait l'implémentation concrète de ces opérations, interagissant avec la base de données et les services externes.

Enfin, l'inversion des dépendances règne en maître dans l'architecture hexagonale. Plutôt que des dépendances directes, le cœur dépend d'abstractions définies par les ports. Cette inversion crée un environnement souple, permettant des modifications sans secouer la stabilité du système.

Plutôt que d'avoir des dépendances directes vers la base de données, le cœur dépendrait d'interfaces définies dans le port "PersistancePort", laissant les détails d'implémentation à l'adaptateur "PersistanceAdapter".

En résumé, l'architecture hexagonale offre une vision stratégique du développement logiciel. Elle place la logique métier au centre, entourée de couches flexibles facilitant l'interaction avec le monde extérieur. Cette approche, avec son hexagone central et ses principes fondamentaux, ouvre la voie à des applications mobiles robustes, adaptables et pérennes.

Les principes de l'architecture hexagonale

L'architecture hexagonale repose sur des fondements solides, formant une structure en forme d'hexagone pour créer des applications mobiles robustes. Décortiquons les principes clés qui définissent cette approche innovante :

  • L'architecture hexagonale est structurée en couches fondamentales. De la couche d'infrastructure à celle de persistance, chaque strate joue un rôle crucial. Cela offre une organisation claire, favorisant la stabilité et la modularité.
  • Le point d'entrée représente l'accès initial à l'application, tandis que la logique métier dicte son fonctionnement interne. Cette dualité assure une expérience utilisateur fluide, équilibrant l'interaction extérieure avec la logique interne.
  • Fondamentale à l'architecture hexagonale, l'inversion des dépendances renverse les schémas traditionnels. Cette approche permet à l'application de s'adapter aux changements sans compromettre sa stabilité. Elle crée un environnement où la logique métier guide les détails d'implémentation.

Mise en pratique de l'architecture hexagonale

Découvrons comment concrètement mettre en œuvre l'architecture hexagonale dans le développement d'applications mobiles. Plongeons dans des exemples tangibles pour comprendre son impact réel.

Explorez un exemple concret sur GitHub où le code source d'une application est dévoilé. Chaque composant, de la couche d'infrastructure à la couche de persistance, est clairement défini. Visualisez comment ces éléments s'entremêlent pour former une structure cohérente. Cette transparence simplifie le processus de développement, permettant une compréhension facile et une évolution efficace de l'application.

L'intégration pratique de l'architecture hexagonale est facilitée avec Spring Boot. Cette union offre une approche concrète pour développer des applications mobiles robustes. Elle simplifie la gestion des dépendances et maintient la flexibilité, permettant aux développeurs de se concentrer sur la création plutôt que sur des considérations techniques. 

L'architecture hexagonale dans le contexte du Domain-Driven Design

Plongeons dans la synergie puissante entre l'architecture hexagonale et les principes du Domain-Driven Design (DDD), développant ainsi des applications mobiles de plus grande qualité.

L'architecture hexagonale et le Domain-Driven Design (DDD) fusionnent harmonieusement pour définir des modèles de domaine robustes offrant ainsi une approche complète du développement logiciel.

Lorsque nous plongeons l'architecture hexagonale dans le contexte du Domain-Driven Design (DDD), une collaboration symbiotique émerge. Ces deux approches, axées sur la compréhension profonde du domaine métier, se renforcent mutuellement.

1. Collaboration Harmonieuse

Imaginons une application de commerce électronique. Dans l'architecture hexagonale, le cœur de l'hexagone représente la gestion des commandes, des transactions, et des stocks, constituant la logique métier centrale. Dans le contexte du DDD, ces entités deviennent les agrégats du domaine, chacun avec son propre cycle de vie et ses règles métier spécifiques. Ainsi, l'hexagone central et les agrégats du DDD collaborent harmonieusement pour façonner le modèle de domaine.

2. Réflexion sur le Contexte Limité

Poursuivons avec la réflexion sur le contexte limité du DDD. Supposons que notre application de commerce électronique ait également un module de gestion des utilisateurs. Dans l'architecture hexagonale, cela devient une autre zone centrale, avec ses propres ports et adaptateurs. Dans le contexte limité du DDD, ce module représente son propre sous-domaine avec ses règles métier distinctes. Cette approche permet une séparation claire des préoccupations et une meilleure compréhension du modèle de domaine, alignant ainsi l'architecture hexagonale avec les principes du DDD.

3. Alistair Cockburn et les Fondements

Alistair Cockburn, un pionnier du Domain-Driven Design, souligne l'importance de définir des interactions précises entre les entités du domaine. Dans l'architecture hexagonale, cela se traduit par la définition précise des ports et adaptateurs, offrant une interface bien définie pour chaque interaction. Cette synchronicité entre les principes de Cockburn et l'architecture hexagonale garantit une compréhension approfondie du domaine et une mise en œuvre logicielle qui reflète fidèlement la réalité métier.

L'architecture hexagonale et le Domain-Driven Design forment une alliance puissante. En utilisant des exemples concrets, nous avons vu comment ces approches complémentaires collaborent pour créer des modèles de domaine clairs, des contextes limités bien définis, et des applications mobiles riches en fonctionnalités métier.

Nos conseils pratiques et astuces

Explorez des conseils concrets et des astuces judicieuses pour optimiser l'utilisation de l'architecture hexagonale dans le développement d'applications mobiles.

Dans la mise en œuvre de l'architecture hexagonale, privilégiez la clarté. Des noms de classes explicites aux commentaires informatifs, assurez-vous que chaque composant de votre application est compréhensible. La transparence facilite la collaboration et la maintenance à long terme.

Investissez dans des tests unitaires approfondis. L'architecture hexagonale favorise la testabilité, alors profitez-en. Des tests solides garantissent la stabilité de votre application et facilitent l'identification rapide des problèmes potentiels.

Accompagnez votre code d'une documentation complète. Décrivez les choix architecturaux, les interactions clés, et les modèles de domaine. Une documentation détaillée facilite l'intégration de nouveaux membres dans l'équipe et assure une compréhension globale du projet.

Soyez sélectif quant aux dépendances. Limitez-vous aux dépendances nécessaires pour éviter la complexité inutile. Une architecture hexagonale bien conçue privilégie la simplicité, ce qui facilite la maintenance et l'évolutivité.

Adoptez une approche itérative. L'itération continue associée au réajustement est essentielle. Recueillez les retours, identifiez les améliorations possibles, et évoluez constamment. Cette approche flexible s'aligne parfaitement avec les principes de l'architecture hexagonale.

L'architecture hexagonale se révèle comme une approche incontournable pour le développement d'applications mobiles. Avec ses principes solides, sa mise en pratique transparente, et sa synergie avec le Domain-Driven Design, elle offre une solution robuste et flexible.

Priorisez la clarté, investissez dans des tests unitaires approfondis, documentez judicieusement, évitez les dépendances superflues, et adoptez une approche itérative pour un succès continu.

En embrassant l'architecture hexagonale, vous développez des applications mobiles plus résilientes et créez une base pour l'innovation future. Restez agile, apprenez constamment, et évoluez avec votre application.

L'architecture hexagone est le socle sur lequel repose l'avenir du développement logiciel mobile.

Échangeons sur votre projet !

Application web
Application mobile
Logiciel métier
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.