State

La programmation avec design patterns

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com
La gestion d'états trouve sa place aussi bien dans le monde réel que dans le monde virtuel. Quand on passe une commande sur une boutique en ligne, d'habitude la commande prend de différents état. La gestion de ces états se fait grâce à une valeur persistante dans une base de données ou autre moyen de stockage. Mais elle peut être encore plus fine grâce au design pattern appelé State.

Pour résumer rapidement de quoi il s'agit. Le design pattern State (état) a pour vocation de permettre à un objet de modifier son comportement au même moment que la modification de son état a lieu. Ce changement s'effectue grâce à la séparation de l'objet de ses états. Malgré cette séparation, les deux restent étroitement liés car l'objet, appelé dans ce cas contexte, contient une référence vers l'instance de son état actuel. De l'autre côté, chaque état contient une référence vers le contexte.

Le plus grand bénéfice de cette séparation est la lisibilité et la maintenabilité du code. L'application est alors abordable et modifiable plus facilement que celle écrite selon le principe de la "boolean-oriented programming" ("programmation orientée booléen"). Dans le cas de cette dernière, la pseudo-gestion des états se fait grâce à des instructions conditionnelles if-else. Et tant qu'on n'a pas une dizaine d'instructions, cette solution peut rester satisfaisante. La chose ne se complique qu'après.

Grâce à la séparation, le comportement du code est placé dans un seul endroit. La façon de faire est situé dans un autre endroit, ce qui garantit une compréhension rapide. Dans le cas qu'on verra plus loin, on va se focaliser sur la gestion d'un magasin qui pourra avoir un des 3 états : ouvert, fermé, en pause. Ces sont les états qui vont décrire le comportement du système. Le contexte, quant à lui, va déterminer la manière selon laquelle le code est exécuté.

Un autre avantage de ce patron de conception est la gestion de vie des objets et donc une légère influence sur le comportement du Garbage Collector. Dans la gestion des états if-else, on aura souvent quelques booléens représentants de différents états (par exemple : décongélé, rechauffé, préparé, prêt pour un plat). Ces booléens vont vivre pendant tout le cycle de vie de l'objet (dans notre cas, ce sera l'objet représentant un plat). Tandis que dans State, les états vivent uniquement le temps pendant lequel l'objet les utilise. Dans le cas d'un plat, on aura un objet "plat" qui peut prendre un des 4 états : "décongélé", "rechauffé", "préparé" et "prêt". Du coup, le passage de l'état "décongélé" en "rechauffé" supprimera automatiquement la référence du plat vers décongélé. L'état "décongélé" ne sera alors pas référéncé et deviendra éligible au Garbage Collector.

Exemple du design pattern State
Comme on a vu plus tôt, notr exemple du design pattern State va présenter la gestion d'un magasin par son propriétaire. Les états seront représentés par les classes implémentant l'interface StoreState. Le propriétaire sera représenté par la classe Owner qui implémentera l'interface OwnerContext. Regardons d'abord les états :

// states
interface StoreState{
public void open() throws Exception;

public void close() throws Exception;

public void closeForBack() throws Exception;
}

class OpenedState implements StoreState{
private Owner owner;

public OpenedState(Owner owner){
this.owner=owner;
}

@Override
public void open() throws Exception{
throw new Exception("You can't open an opened store.");
}

@Override
public void close(){
System.out.println("I'm closing the store.");
this.owner.changeState(new ClosedState(this.owner));
}

@Override
public void closeForBack(){
System.out.println("I'll be back for 10 minutes.");
this.owner.changeState(new ClosedForBackState(this.owner));
}
}

class ClosedState implements StoreState{
private Owner owner;

public ClosedState(Owner owner){
this.owner=owner;
}

@Override
public void open(){
System.out.println("I'm opening the store.");
this.owner.changeState(new OpenedState(this.owner));
}

@Override
public void close() throws Exception{
throw new Exception("You can't close already closed store.");
}

@Override
public void closeForBack() throws Exception{
throw new Exception("The store is already closed. You can't close it for back in 10 minutes.");
}
}

class ClosedForBackState implements StoreState{
private Owner owner;

public ClosedForBackState(Owner owner){
this.owner=owner;
}

@Override
public void open(){
System.out.println("I'm reopening the store after some pause.");
this.owner.changeState(new OpenedState(this.owner));
}

@Override
public void close() throws Exception{
throw new Exception("You can't close the store definitively when you are in pause.");
}

@Override
public void closeForBack() throws Exception{
throw new Exception("You can't reclose the store for pause.");
}
}

On remarque que les méthodes dont l'exécution n'est pas permise lancent une exception. Ce comportement est beaucoup plus facile à comprendre et à mettre en place que des boucles conditionnelles. La partie du contexte utilise cette technique :


// context
interface OwnerContext{
public void startDay();

public void makePause();

public void endDay();

public void changeState(StoreState state);
}

class Owner implements OwnerContext{
private StoreState state;

public Owner(){
this.state=new ClosedState(this);
}

@Override
public void startDay(){
try{
this.state.open();
}
catch(Exception e){
System.out.println("An error occured on opening the store : "+e.getMessage());
}
}

@Override
public void makePause(){
try{
this.state.closeForBack();
}
catch(Exception e){
System.out.println("An error occured on making a pause : "+e.getMessage());
}
}

@Override
public void endDay(){
try{
this.state.close();
}
catch(Exception e){
System.out.println("An error occured on closing the store : "+e.getMessage());
}
}

@Override
public void changeState(StoreState state){
this.state=state;
}
}

On voit ici comment sont liés les parties contexte et états. L'invocation de chaque méthode du contexte va provoquer l'appel d'une méthode appartenant à un état. Et inversement, l'exécution de chaque méthode de l'état va modifier l'état actuel du contexte grâce à l'appel du changeState(). L'ensembe fonctionnel se présente ainsi :


public class State{
public static void main(String[] args){
final OwnerContext owner=new Owner();
owner.startDay();
owner.makePause();
owner.startDay();
owner.endDay();
owner.makePause();
}
}


Le résultat sera alors le suivant :

I'm opening the store.
I'll be back for 10 minutes.
I'm reopening the store after some pause.
I'm closing the store.
An error occured on making a pause : The store is already closed. You can't close it for back in 10 minutes.

On observe que le comportement est le même que prévu. L'enchaînement logique "fermeture - ouverture - pause - ouverture - fermeture" se fait sans aucun souci. Cependant, quand on tente de faire une opération illogique, notamment mettre en pause un magasin fermé, une exception est lancé et provoque l'impossibilité de modifier le comportement du contexte.

Pour constater les bénéfices du design pattern State, regardons le même code, mais développé en logique orientée booléen :

public class State{
public static void main(final String[] args){
final Owner owner=new Owner();
owner.open();
owner.closeForBack();
owner.open();
owner.close();
owner.closeForBack();
owner.open();

}
}

class Owner{
private boolean isOpened=false;
private boolean isClosed=false;
private boolean isClosedForBack=false;

public Owner(){
this.isClosed=true;
}

public void open(){
if(!this.isOpened&&this.isClosed||this.isClosedForBack){
System.out.println("Open !");
this.isOpened=true;
this.isClosed=false;
this.isClosedForBack=false;
}
}

public void close(){
if(this.isOpened&&!this.isClosed&&!this.isClosedForBack){
System.out.println("Closed");
this.isClosed=true;
this.isOpened=false;
}
}

public void closeForBack(){
if(this.isOpened&&!this.isClosedForBack&&!this.isClosed){
System.out.println("Closed for back");
this.isClosedForBack=true;
this.isOpened=false;

}
}
}

Le cas n'est pas très tragique, mais c'est seulement à cause du fait qu'on ne gère pas beaucoup d'états. Au fur et à mesure d'ajout des états on s'apercevrait que cette méthode sera difficilement maintenable. En outre, l'erreur est humaine et, probablement à cause d'un mauvais copié-collé, on pourrait mettre certains états erronnés (par exemple en mettant isClosedForBack et isClosed à true).

La gestion des états ne doit pas se faire avec des if-else à la chaîne. Le patron de conception State permet de gérer de différents moments de vie d'un objet dynamiquement, en séparant la définition du comportement de son implémentation. Grâce à cette séparation le code devient plus lisible et plus évolutif.
Bartosz KONIECZNY 08-09-2013 17:40 design patterns
Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil MySQL

Vous rencontrez un problème lors de la suppression d'un élément ou d'une table liée à des contraintes des clés étrangères. L'une des solution peut être l'annulation de la vérification de ces contraintes.


-- Au début on indique à MySQL de ne pas vérifier les contraintes des clés étrangères
SET FOREIGN_KEY_CHECKS=0;
-- Ensuite on passe à l'opération de suppression d'une table
DROP TABLE ma_table;
-- A la fin (ou au moment voulu) on restaure la vérification des contraintes
SET FOREIGN_KEY_CHECKS=1;

Vous pouvez en savoir plus sur FOREIGN_KEY_CHEKS dans la documentation du MySQL.