Accueil / Tutoriels / State : Gérer élégamment les changements de comportement

State : Gérer élégamment les changements de comportement

Publié le 16 Juin 2025
Conception

State est un design pattern comportemental qui permet de gérer proprement les changements de comportement d’un objet en fonction de son état interne. Plutôt que d’enchaîner des conditions if, else ou des blocs switch, le pattern State propose une approche orientée objet : chaque état devient une classe à part entière, responsable de son propre comportement et de ses transitions.

Ce pattern est particulièrement utile en développement web, où de nombreux objets évoluent selon un cycle de vie bien défini. Une commande peut passer de “en attente” à “ expédiée”, un utilisateur de “visiteur” à “abonné”, une session de “valide” à “expirée”. Plutôt que d’alourdir le code métier avec des vérifications d’état éparpillées, le pattern State apporte structure, lisibilité et extensibilité.

Principe du pattern State

Le pattern State propose de déléguer le comportement d’un objet à des objets représentant son état. Plutôt que de multiplier les conditions if ou switch pour ajuster son comportement, l’objet principal délègue ses actions à une instance de "state", dont la responsabilité est de réagir de manière appropriée.

Cela permet d’isoler clairement les comportements associés à chaque état, de simplifier la logique métier et de rendre le code plus évolutif. Lorsqu’un changement d’état survient, on remplace simplement l’objet représentant l’état actuel par un autre, sans modifier la structure de l’objet principal. Le design pattern State est donc particulièrement adapté aux situations où un objet peut avoir plusieurs comportements bien distincts, qui dépendent de son état courant.

Cas d’utilisation en programmation web

Gérer le cycle de vie d’une commande e-commerce

Dans une application e-commerce, une commande peut être dans plusieurs états : en attente de paiement, payée, en cours de préparation, expédiée, livrée, annulée... Chaque transition autorisée et chaque action disponible dépendent de l’état actuel de la commande. Par exemple, une commande "en attente" peut être annulée, mais une commande "livrée" ne peut plus l’être. Le pattern State permet d’implémenter cette logique métier sans multiplier les conditions dans la classe principale Order.

Implémenter un workflow utilisateur dynamique

Dans une application SaaS, le parcours d’un utilisateur peut dépendre de son état : invité, inscrit, abonné, administrateur. Chaque état débloque certaines fonctionnalités ou restreint l’accès à d’autres. Le pattern State permet ici de déléguer les autorisations ou les règles à des objets encapsulant chaque état, rendant l’application plus modulaire et plus facile à faire évoluer sans casser les règles d’accès.

Points d’attentions

Le pattern State introduit une certaine complexité structurelle en multipliant les classes, ce qui peut sembler lourd pour des cas simples. De plus, il faut éviter de recréer un switch géant sous une autre forme, en ayant par exemple une classe de gestion centrale qui pilote les transitions à la place des objets d’état eux-mêmes. Le vrai pouvoir de ce design pattern vient du fait que chaque état est autonome et peut piloter la transition vers un autre état.

Il est essentiel de bien délimiter les responsabilités de chaque classe afin d’éviter que les objets d’état ne se transforment en classes fourre-tout difficiles à maintenir. Si les transitions d’état sont trop implicites ou dispersées dans le code, on risque de perdre en lisibilité et en clarté sur le comportement de l’application.

Implémenter le pattern State

On implémente une interface ou une abstraction représentant un état, avec une ou plusieurs méthodes que tous les états doivent implémenter. Chaque classe concrète représente un état particulier et encapsule le comportement spécifique à ce moment-là.

L’objet principal (contexte) contient une référence vers un objet d’état courant. Lorsqu’un événement survient, l’objet délègue l’exécution de la logique métier à l’objet d’état. Ce dernier peut ensuite décider de changer l’état du contexte si nécessaire.

Ce modèle est propre, testable et facile à faire évoluer. Il peut aussi s’adapter en programmation fonctionnelle, en utilisant des closures ou des fonctions encapsulées pour simuler les transitions d’états.

Voici un exemple en Go :

Nous allons modéliser un utilisateur dont les droits et les interactions évoluent selon son statut. Un visiteur peut voir une version limitée du tableau de bord, s’enregistrer pour accéder à plus de fonctionnalités, puis devenir abonné premium pour débloquer l’ensemble des services.

Chaque état (invité, inscrit, premium) encapsule son propre comportement, et l’objet User délègue à l’état courant la gestion de ses actions.


package userstate

type UserState interface {
   ViewDashboard()
   Upgrade(user *User)
}


package userstate

import "fmt"

type GuestState struct{}

func (g *GuestState) ViewDashboard() {
   fmt.Println("Accès limité. Connectez-vous pour plus de fonctionnalités.")
}

func (g *GuestState) Upgrade(user *User) {
   fmt.Println("Passage à l'état Utilisateur enregistré.")
   user.SetState(&RegisteredState{})
}

type RegisteredState struct{}

func (r *RegisteredState) ViewDashboard() {
   fmt.Println("Bienvenue sur votre tableau de bord utilisateur.")
}

func (r *RegisteredState) Upgrade(user *User) {
   fmt.Println("Passage à l'état Abonné premium.")
   user.SetState(&PremiumState{})
}

type PremiumState struct{}

func (p *PremiumState) ViewDashboard() {
   fmt.Println("Bienvenue dans votre espace premium avec toutes les fonctionnalités.")
}


func (p *PremiumState) Upgrade(user *User) {
   fmt.Println("Vous êtes déjà au niveau maximum.")
}


package userstate

type User struct {
   state UserState
}

func NewUser(initialState UserState) *User {
   return &User{state: initialState}
}

func (u *User) SetState(state UserState) {
   u.state = state
}

func (u *User) ViewDashboard() {
   u.state.ViewDashboard()
}

func (u *User) Upgrade() {
   u.state.Upgrade(u)
}


package main

import "training.go/designpatterns/state/userstate"

func main() {
   user := userstate.NewUser(&userstate.GuestState{})

   user.ViewDashboard()
   user.Upgrade()

   user.ViewDashboard()
   user.Upgrade()

   user.ViewDashboard()
   user.Upgrade()
}

Le même exemple en PHP :


namespace Practice\DesignPatterns\State;

interface UserStateInterface
{
   public function viewDashboard();
   public function upgrade(User $user);
}


namespace Practice\DesignPatterns\State;

class User {
   private UserStateInterface $state;

   public function __construct(UserStateInterface $state) {
       $this->state = $state;
   }

   public function setState(UserStateInterface $state): void {
       $this->state = $state;
   }

   public function viewDashboard(): void {
       $this->state->viewDashboard();
   }

   public function upgrade(): void {
       $this->state->upgrade($this);
   }
}


namespace Practice\DesignPatterns\State;

class GuestState implements UserStateInterface
{
   public function viewDashboard(): void {
       echo "Accès limité. Connectez-vous pour plus de fonctionnalités.\n";
   }

   public function upgrade(User $user): void {
       echo "Passage à l'état Utilisateur enregistré.\n";
       $user->setState(new RegisteredState());
   }
}


namespace Practice\DesignPatterns\State;

class PremiumState implements UserStateInterface
{
   public function viewDashboard(): void {
       echo "Bienvenue dans votre espace premium avec toutes les fonctionnalités.\n";
   }

   public function upgrade(User $user): void {
       echo "Vous êtes déjà au niveau maximum.\n";
   }
}


namespace Practice\DesignPatterns\State;

class RegisteredState implements UserStateInterface
{
   public function viewDashboard(): void {
       echo "Bienvenue sur votre tableau de bord utilisateur.\n";
   }

   public function upgrade(User $user): void {
       echo "Passage à l'état Abonné premium.\n";
       $user->setState(new PremiumState());
   }
}


declare(strict_types=1);

use Practice\DesignPatterns\State\GuestState;
use Practice\DesignPatterns\state\User;

require "./vendor/autoload.php";

$user = new User(new GuestState());
$user->viewDashboard();
$user->upgrade();

$user->viewDashboard();
$user->upgrade();

$user->viewDashboard();
$user->upgrade();

Conclusion

Le design pattern State est un allié précieux dès lors que vos objets doivent changer de comportement de façon dynamique selon leur état. Il permet de structurer proprement cette logique, d’éviter des blocs conditionnels interminables, et de faire évoluer votre code sans le rigidifier. Que vous soyez sur un backend Go ou PHP, ce pattern s’adapte parfaitement à une architecture web moderne, en particulier lorsqu’il est question de workflows, de transitions ou de cycles de vie métier. Utilisé avec discernement, il renforce la lisibilité, l’organisation et la maintenabilité de vos projets.

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern State. Vous pouvez retrouver mon article sur les design patterns ou les autres tutoriels sur Builder, Singleton, Mediator, Prototype et Memento.