Chapitre 11
11 — Patterns Comportementaux : State, Visitor & More
11 — Patterns Comportementaux : State, Visitor & More
Chapitre 11 : State, Visitor, Interpreter, Iterator, Memento
Durée estimée : 5 séances de 3h Objectifs : Maîtriser les patterns comportementaux avancés : State (machines à états), Visitor (Double Dispatch), Interpreter, Iterator, Memento.
Partie 1 : Le Pattern State
1.1 Définition et Intention
Le State pattern permet à un objet de modifier son comportement quand son état interne change. L'objet semblera changer de classe.
Intention du GoF : "Permettre à un objet de modifier son comportement quand son état interne change. L'objet semblera changer de classe."
1.2 Problème résolu
Un document peut être dans différents états (Brouillon, En révision, Publié). Les actions possibles dépendent de l'état :
- Brouillon → peut être soumis pour révision
- En révision → peut être approuvé ou rejeté
- Publié → ne peut plus être modifié
Sans State : des conditionnels monstres (if (state === 'draft') ...).
Avec State : chaque état est une classe qui définit les comportements autorisés.
Diagramme en cours de génération...
1.3 Structure UML
Diagramme en cours de génération...
1.4 Implémentation — Document Workflow
interface DocumentState {
publish(doc: Document): void;
reject(doc: Document): void;
archive(doc: Document): void;
getStatus(): string;
}
class Document {
private state: DocumentState;
constructor() {
this.state = new DraftState();
}
setState(state: DocumentState): void {
this.state = state;
console.log(`Document state: ${state.getStatus()}`);
}
publish(): void { this.state.publish(this); }
reject(): void { this.state.reject(this); }
archive(): void { this.state.archive(this); }
getState(): DocumentState { return this.state; }
}
class DraftState implements DocumentState {
publish(doc: Document): void {
console.log('Submitting document for review');
doc.setState(new ModerationState());
}
reject(doc: Document): void {
console.log('Cannot reject a draft (not in review)');
}
archive(doc: Document): void {
console.log('Archiving draft directly');
doc.setState(new ArchivedState());
}
getStatus(): string { return 'Draft'; }
}
class ModerationState implements DocumentState {
publish(doc: Document): void {
console.log('Approving document for publication');
doc.setState(new PublishedState());
}
reject(doc: Document): void {
console.log('Rejecting document, returning to draft');
doc.setState(new DraftState());
}
archive(doc: Document): void {
console.log('Cannot archive while in moderation');
}
getStatus(): string { return 'In Moderation'; }
}
class PublishedState implements DocumentState {
publish(doc: Document): void {
console.log('Document is already published');
}
reject(doc: Document): void {
console.log('Cannot reject a published document');
}
archive(doc: Document): void {
console.log('Archiving published document');
doc.setState(new ArchivedState());
}
getStatus(): string { return 'Published'; }
}
class ArchivedState implements DocumentState {
publish(doc: Document): void {
console.log('Cannot publish archived document');
}
reject(doc: Document): void {
console.log('Cannot reject archived document');
}
archive(doc: Document): void {
console.log('Document is already archived');
}
getStatus(): string { return 'Archived'; }
}
// Usage
const doc = new Document();
doc.publish(); // → Moderation
doc.reject(); // → Draft
doc.publish(); // → Moderation
doc.publish(); // → Published
doc.archive(); // → Archived
doc.publish(); // Error: cannot publish archived
1.5 State vs Strategy
| Critère | State | Strategy |
|---|---|---|
| Intention | Changer comportement selon état | Choisir un algorithme parmi plusieurs |
| Transitions | L'état peut changer le state | La stratégie est fixe pour l'opération |
| Contexte | L'état dépend du contexte | La stratégie est indépendante |
| Nombre | États finis, transitions connues | N'importe quel nombre |
Partie 2 : Le Pattern Visitor
2.1 Définition et Intention
Le Visitor permet de séparer les opérations de la structure d'objets sur laquelle elles opèrent. On peut ajouter de nouvelles opérations sans modifier les classes des éléments.
Intention du GoF : "Représenter une opération à effectuer sur les éléments d'une structure d'objets. Visitor permet de définir une nouvelle opération sans changer les classes des éléments sur lesquels elle opère."
2.2 Problème résolu
Dans un système de fichiers, on a des fichiers et des dossiers. On veut ajouter des opérations (export, compression, antivirus) sans modifier les classes File et Directory à chaque fois.
2.3 Implémentation — AST Parser
interface Expression {
accept(visitor: ExpressionVisitor): number;
}
class NumberExpression implements Expression {
constructor(public value: number) {}
accept(visitor: ExpressionVisitor): number {
return visitor.visitNumber(this);
}
}
class AddExpression implements Expression {
constructor(public left: Expression, public right: Expression) {}
accept(visitor: ExpressionVisitor): number {
return visitor.visitAdd(this);
}
}
class SubtractExpression implements Expression {
constructor(public left: Expression, public right: Expression) {}
accept(visitor: ExpressionVisitor): number {
return visitor.visitSubtract(this);
}
}
class MultiplyExpression implements Expression {
constructor(public left: Expression, public right: Expression) {}
accept(visitor: ExpressionVisitor): number {
return visitor.visitMultiply(this);
}
}
interface ExpressionVisitor {
visitNumber(expr: NumberExpression): number;
visitAdd(expr: AddExpression): number;
visitSubtract(expr: SubtractExpression): number;
visitMultiply(expr: MultiplyExpression): number;
}
class EvaluatorVisitor implements ExpressionVisitor {
visitNumber(expr: NumberExpression): number {
return expr.value;
}
visitAdd(expr: AddExpression): number {
return expr.left.accept(this) + expr.right.accept(this);
}
visitSubtract(expr: SubtractExpression): number {
return expr.left.accept(this) - expr.right.accept(this);
}
visitMultiply(expr: MultiplyExpression): number {
return expr.left.accept(this) * expr.right.accept(this);
}
}
class StringifyVisitor implements ExpressionVisitor {
visitNumber(expr: NumberExpression): string {
return `${expr.value}`;
}
visitAdd(expr: AddExpression): string {
return `(${expr.left.accept(this)} + ${expr.right.accept(this)})`;
}
visitSubtract(expr: SubtractExpression): string {
return `(${expr.left.accept(this)} - ${expr.right.accept(this)})`;
}
visitMultiply(expr: MultiplyExpression): string {
return `(${expr.left.accept(this)} * ${expr.right.accept(this)})`;
}
}
// Usage
const expr = new AddExpression(
new NumberExpression(5),
new MultiplyExpression(
new NumberExpression(3),
new NumberExpression(2)
)
);
const evaluator = new EvaluatorVisitor();
console.log(`Result: ${expr.accept(evaluator)}`); // 11
const stringify = new StringifyVisitor();
console.log(`Expression: ${expr.accept(stringify)}`); // (5 + (3 * 2))
Partie 3 : Le Pattern Interpreter
3.1 Définition
Interpreter définit une représentation pour la grammaire d'un langage et un interpréteur qui utilise cette représentation pour interpréter des phrases.
3.2 Implémentation — Expression régulière simple
interface Expression {
interpret(context: string): boolean;
}
class LiteralExpression implements Expression {
constructor(private literal: string) {}
interpret(context: string): boolean {
return context.includes(this.literal);
}
}
class AndExpression implements Expression {
constructor(private expr1: Expression, private expr2: Expression) {}
interpret(context: string): boolean {
return this.expr1.interpret(context) && this.expr2.interpret(context);
}
}
class OrExpression implements Expression {
constructor(private expr1: Expression, private expr2: Expression) {}
interpret(context: string): boolean {
return this.expr1.interpret(context) || this.expr2.interpret(context);
}
}
class NotExpression implements Expression {
constructor(private expr: Expression) {}
interpret(context: string): boolean {
return !this.expr.interpret(context);
}
}
// Usage
const alice = new LiteralExpression('Alice');
const bob = new LiteralExpression('Bob');
const hello = new LiteralExpression('Hello');
const aliceOrBob = new OrExpression(alice, bob);
const greeting = new AndExpression(aliceOrBob, hello);
console.log(greeting.interpret('Hello Alice')); // true
console.log(greeting.interpret('Hello Bob')); // true
console.log(greeting.interpret('Hello Charlie')); // false
Partie 4 : Le Pattern Iterator
4.1 Définition
Iterator fournit un moyen d'accéder séquentiellement aux éléments d'un objet agrégé sans exposer sa représentation sous-jacente.
4.2 Implémentation
interface Iterator<T> {
current(): T;
next(): T;
hasNext(): boolean;
reset(): void;
}
interface IterableCollection<T> {
createIterator(): Iterator<T>;
}
class TreeNode<T> {
constructor(
public value: T,
public left: TreeNode<T> | null = null,
public right: TreeNode<T> | null = null
) {}
}
class BinaryTree<T> implements IterableCollection<T> {
constructor(public root: TreeNode<T> | null = null) {}
createIterator(order: 'inorder' | 'preorder' | 'postorder' = 'inorder'): Iterator<T> {
switch (order) {
case 'inorder': return new InOrderIterator(this.root);
case 'preorder': return new PreOrderIterator(this.root);
case 'postorder': return new PostOrderIterator(this.root);
}
}
}
class InOrderIterator<T> implements Iterator<T> {
private stack: TreeNode<T>[] = [];
private current: TreeNode<T> | null;
constructor(root: TreeNode<T> | null) {
this.current = root;
this.pushLeft(this.current);
}
private pushLeft(node: TreeNode<T> | null): void {
while (node) {
this.stack.push(node);
node = node.left;
}
}
current(): T {
return this.stack[this.stack.length - 1]?.value;
}
next(): T {
const node = this.stack.pop()!;
const value = node.value;
if (node.right) {
this.pushLeft(node.right);
}
return value;
}
hasNext(): boolean {
return this.stack.length > 0;
}
reset(): void {
this.stack = [];
this.pushLeft(this.current);
}
}
// Usage
const tree = new BinaryTree(
new TreeNode(1,
new TreeNode(2, new TreeNode(4), new TreeNode(5)),
new TreeNode(3, new TreeNode(6), new TreeNode(7))
)
);
const iterator = tree.createIterator('inorder');
while (iterator.hasNext()) {
console.log(iterator.next()); // 4, 2, 5, 1, 6, 3, 7
}
Partie 5 : Le Pattern Memento
5.1 Définition
Memento capture et externalise l'état interne d'un objet sans violer l'encapsulation, permettant une restauration ultérieure.
5.2 Implémentation
class Memento {
constructor(private state: any) {}
getState(): any {
return this.state;
}
}
// Originator
class TextEditor {
private content: string = '';
private cursorPosition: number = 0;
type(text: string): void {
this.content += text;
this.cursorPosition = this.content.length;
}
deleteLast(characters: number): void {
this.content = this.content.slice(0, -characters);
this.cursorPosition = this.content.length;
}
save(): Memento {
return new Memento({
content: this.content,
cursorPosition: this.cursorPosition
});
}
restore(memento: Memento): void {
const state = memento.getState();
this.content = state.content;
this.cursorPosition = state.cursorPosition;
}
toString(): string {
return this.content;
}
}
// Caretaker
class History {
private mementos: Memento[] = [];
private currentIndex: number = -1;
save(memento: Memento): void {
// Clear any redo history
this.mementos = this.mementos.slice(0, this.currentIndex + 1);
this.mementos.push(memento);
this.currentIndex++;
}
undo(): Memento | null {
if (this.currentIndex > 0) {
this.currentIndex--;
return this.mementos[this.currentIndex];
}
return null;
}
redo(): Memento | null {
if (this.currentIndex < this.mementos.length - 1) {
this.currentIndex++;
return this.mementos[this.currentIndex];
}
return null;
}
}
// Usage
const editor = new TextEditor();
const history = new History();
editor.type('Hello');
history.save(editor.save());
editor.type(' World');
history.save(editor.save());
console.log(editor.toString()); // "Hello World"
editor.restore(history.undo()!);
console.log(editor.toString()); // "Hello"
editor.restore(history.redo()!);
console.log(editor.toString()); // "Hello World"
Partie 6 : Résumé et Comparaison
| Pattern | Intention | Structure | Quand l'utiliser |
|---|---|---|---|
| State | Comportement varie selon l'état | Context + State | Workflow, machines à états |
| Visitor | Opérations séparées de la structure | Element + Visitor | AST, fichiers, hiérarchies |
| Interpreter | Grammaire + interprétation | Expression + Context | DSL, règles, requêtes |
| Iterator | Parcours sans exposer structure | Aggregate + Iterator | Collections, arbres |
| Memento | Sauvegarde/restauration état | Originator + Memento + Caretaker | Undo, transactions |
Exercices
- Machine à états : Implémenter un feu de signalisation (Vert→Orange→Rouge→Vert)
- Visitor : Exporter un document HTML en PDF, TXT, et Markdown
- Interpreter : Parser et exécuter des expressions mathématiques simples
- Iterator : Parcourir un graphe en DFS et BFS
- Memento : Système de sauvegarde automatique d'un formulaire
Prochain chapitre : Architectural MVC, MVVM, MVP & Clean Architecture.