Série « Design Patterns en Java » : 9 articles en 4 familles, chacun lisible seul.
- Créationnels : Singleton et Factory Method (1/2) · Abstract Factory et Builder (2/2)
- Structurels : Adapter et Decorator (1/2) · Facade et Proxy (2/2)
- Comportementaux, orchestration : Strategy et Observer (1/2) · Template Method et State (2/2)
- Comportementaux, communication : Command (1/3) · Chain of Responsibility et Mediator (2/3) · Iterator et Visitor (3/3, cet article)
Cette dernière partie traite deux patterns de parcours et d'opérations. Iterator est intégré au langage Java (la boucle for-each). Visitor reste un outil à choisir en connaissance de cause, avec une alternative moderne à connaître : une hiérarchie sealed combinée à un switch avec pattern matching. Elle se termine par un scénario de commande qui réunit plusieurs patterns de la série, puis par un bilan des 17 patterns présentés.
Le code est illustratif et simplifié : les classes non définies (Category, Product, Order, OrderRepository, OrderRequest…) sont supposées exister. Les noms suivent les conventions Java et le rôle du pattern en suffixe, comme dans le reste de la série ; les diagrammes UML reprennent les noms du code.. Les exemples ont été compilés et exécutés avec le JDK 21 et Spring Boot 3.5 ; les versions minimales de Java sont indiquées quand une fonctionnalité en dépend.
Iterator et Visitor : parcourir et ajouter des opérations
Iterator : le pattern devenu invisible
Iterator est intégré au langage Java. Chaque boucle for-each sur un Iterable utilise un Iterator :
// Quand on écrit :
for (Product product : products) {
System.out.println(product.getName());
}
// Le compilateur génère l'équivalent de :
Iterator<Product> it = products.iterator();
while (it.hasNext()) {
Product product = it.next();
System.out.println(product.getName());
}
L'interface est dans le JDK depuis Java 1.2 (extrait simplifié) :
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException(); }
default void forEachRemaining(Consumer<? super E> action) { /* ... */ }
}
Un Iterator personnalisé pour un arbre de catégories :
public class CategoryTreeIterator implements Iterator<Category> {
private final Deque<Category> stack = new ArrayDeque<>();
public CategoryTreeIterator(Category root) {
if (root != null) {
stack.push(root);
}
}
@Override
public boolean hasNext() {
return !stack.isEmpty();
}
@Override
public Category next() {
if (!hasNext()) throw new NoSuchElementException();
Category current = stack.pop();
// Ajoute les enfants en ordre inverse pour un parcours en profondeur
List<Category> children = current.getChildren();
for (int i = children.size() - 1; i >= 0; i--) {
stack.push(children.get(i));
}
return current;
}
}
// La collection qui implémente Iterable
public class CategoryTree implements Iterable<Category> {
private final Category root;
public CategoryTree(Category root) {
this.root = root;
}
@Override
public Iterator<Category> iterator() {
return new CategoryTreeIterator(root);
}
}
// Utilisation naturelle
CategoryTree tree = new CategoryTree(rootCategory);
for (Category category : tree) {
System.out.println(category.getName());
}
// Ou avec l'API Stream
StreamSupport.stream(tree.spliterator(), false)
.filter(c -> c.getLevel() > 2)
.map(Category::getName)
.forEach(System.out::println);
Visitor : double dispatch et opérations sans modification
Le Visitor permet d'ajouter des opérations à une hiérarchie de classes sans modifier ces classes. Il est souvent jugé peu intuitif, mais il sert pour l'analyse et la transformation d'arbres.
Contrairement à Iterator, Java ne l'a pas intégré au langage. Il faut l'écrire soi-même, et il impose un compromis qu'il vaut mieux connaître avant de l'adopter.
// Hiérarchie de nœuds de document
public interface DocumentNode {
void accept(DocumentVisitor visitor);
}
public class ParagraphNode implements DocumentNode {
private final String text;
public ParagraphNode(String text) { this.text = text; }
public String getText() { return text; }
@Override
public void accept(DocumentVisitor visitor) {
visitor.visit(this); // Double dispatch
}
}
public class ImageNode implements DocumentNode {
private final String url;
private final String alt;
public ImageNode(String url, String alt) { this.url = url; this.alt = alt; }
public String getUrl() { return url; }
public String getAlt() { return alt; }
@Override
public void accept(DocumentVisitor visitor) {
visitor.visit(this);
}
}
public class TableNode implements DocumentNode {
private final List<List<String>> rows;
public TableNode(List<List<String>> rows) { this.rows = rows; }
public List<List<String>> getRows() { return rows; }
@Override
public void accept(DocumentVisitor visitor) {
visitor.visit(this);
}
}
// L'interface Visitor : une nouvelle opération = un nouveau Visitor
public interface DocumentVisitor {
void visit(ParagraphNode node);
void visit(ImageNode node);
void visit(TableNode node);
}
Le « double dispatch » se joue en deux temps. node.accept(visitor) est résolu à l'exécution selon le type réel du nœud (polymorphisme classique), puis l'appel visitor.visit(this) est résolu à la compilation selon le type statique de this, ce qui sélectionne la bonne surcharge.
Ajout d'opérations sans modifier les nœuds :
// Export HTML
public class HtmlExportVisitor implements DocumentVisitor {
private final StringBuilder html = new StringBuilder();
@Override
public void visit(ParagraphNode node) {
html.append("<p>").append(escapeHtml(node.getText())).append("</p>\n");
}
@Override
public void visit(ImageNode node) {
html.append(String.format("<img src=\"%s\" alt=\"%s\" />\n",
safeUrl(node.getUrl()), escapeHtml(node.getAlt())));
}
@Override
public void visit(TableNode node) {
html.append("<table>\n");
for (List<String> row : node.getRows()) {
html.append(" <tr>");
for (String cell : row) {
html.append("<td>").append(escapeHtml(cell)).append("</td>");
}
html.append("</tr>\n");
}
html.append("</table>\n");
}
public String getHtml() { return html.toString(); }
// Toute donnée insérée dans du HTML doit être échappée, sinon injection XSS
private static String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
// Liste blanche de schémas : refuse javascript:, data:, etc.
private static String safeUrl(String url) {
if (url != null
&& (url.startsWith("https://") || url.startsWith("http://")
|| (url.startsWith("/") && !url.startsWith("//")))) {
return escapeHtml(url);
}
return "";
}
}
// Compteur de mots : nouvelle opération, aucune modification des nœuds
public class WordCountVisitor implements DocumentVisitor {
private int totalWords = 0;
@Override
public void visit(ParagraphNode node) {
totalWords += countWords(node.getText());
}
@Override
public void visit(ImageNode node) {
totalWords += countWords(node.getAlt());
}
@Override
public void visit(TableNode node) {
for (List<String> row : node.getRows()) {
for (String cell : row) {
totalWords += countWords(cell);
}
}
}
// "".split("\\s+") retourne un tableau d'un élément : le vide se traite à part
private static int countWords(String text) {
if (text == null || text.isBlank()) return 0;
return text.trim().split("\\s+").length;
}
public int getTotalWords() { return totalWords; }
}
Utilisation :
List<DocumentNode> document = List.of(
new ParagraphNode("Les design patterns donnent un vocabulaire commun."),
new ImageNode("/img/uml.png", "Diagramme UML"),
new TableNode(List.of(
List.of("Pattern", "Type"),
List.of("Command", "Comportemental")
))
);
// Export HTML
HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();
document.forEach(node -> node.accept(htmlVisitor));
System.out.println(htmlVisitor.getHtml());
// Comptage de mots
WordCountVisitor wordCounter = new WordCountVisitor();
document.forEach(node -> node.accept(wordCounter));
System.out.println("Mots : " + wordCounter.getTotalWords()); // 11
Visitor est à la base de nombreux outils : compilateurs (visiteurs d'AST), analyseurs statiques, et java.nio.file.FileVisitor pour parcourir les arborescences de fichiers.
Le compromis du Visitor. Ajouter une opération est facile : une nouvelle classe, sans toucher aux nœuds. Ajouter un type de nœud est coûteux : il faut ajouter une méthode visit à l'interface, donc modifier chaque visiteur existant. Le pattern convient donc aux hiérarchies stables dont on multiplie les opérations.
L'alternative moderne : sealed et le switch avec pattern matching. Les record (Java 16) et les types sealed (Java 17) existent avant Java 21, mais le switch avec pattern matching sur type n'est définitif qu'en Java 21. Depuis cette version, une hiérarchie scellée de record combinée à un switch exhaustif exprime la même chose avec bien moins de cérémonie (ces types remplacent la hiérarchie précédente, ils ne s'y ajoutent pas) :
public sealed interface DocumentNode permits ParagraphNode, ImageNode, TableNode {}
public record ParagraphNode(String text) implements DocumentNode {}
public record ImageNode(String url, String alt) implements DocumentNode {}
public record TableNode(List<List<String>> rows) implements DocumentNode {}
public final class DocumentOps {
private DocumentOps() {}
public static int wordCount(DocumentNode node) {
return switch (node) {
case ParagraphNode p -> countWords(p.text());
case ImageNode i -> countWords(i.alt());
case TableNode t -> t.rows().stream()
.flatMap(List::stream)
.mapToInt(DocumentOps::countWords)
.sum();
};
}
private static int countWords(String text) {
return (text == null || text.isBlank()) ? 0 : text.trim().split("\\s+").length;
}
}
Le compilateur vérifie que le switch couvre tous les sous-types : ajouter un nouveau nœud fait échouer la compilation à chaque endroit à compléter. On retrouve la même contrainte que le Visitor, sans accept() ni double dispatch. Ajouter un type de nœud casse donc autant les visiteurs que les switch. Le Visitor garde son intérêt quand on cible une version de Java antérieure à 21, ou quand l'API est consommée par du code tiers qui fournit ses propres opérations (comme FileVisitor).
Les patterns comportementaux en synergie finale
Les patterns d'orchestration (Strategy et Observer, Template Method et State) et ceux de communication se combinent dans un scénario de commande e-commerce :
// Système de gestion de commandes e-commerce
//
// Strategy -> Calcul du prix selon le type de client (partie orchestration)
// Observer -> Notification des changements de commande (partie orchestration)
// Template -> Processus standard d'import de produits (partie orchestration)
// State -> Cycle de vie de la commande (partie orchestration)
// Command -> Historique des actions admin avec undo/redo
// Chain of Resp -> Pipeline de contrôle des requêtes API (en amont, au niveau de la passerelle)
// Mediator -> Coordination du traitement de commande
@Service
public class OrderPlacementService {
private final PricingService pricingService; // Strategy
private final CommandHistoryRegistry histories; // Command
private final OrderProcessingCoordinator orchestrator; // Mediator
private final OrderRepository orderRepository;
public OrderPlacementService(PricingService pricingService,
CommandHistoryRegistry histories,
OrderProcessingCoordinator orchestrator,
OrderRepository orderRepository) {
this.pricingService = pricingService;
this.histories = histories;
this.orchestrator = orchestrator;
this.orderRepository = orderRepository;
}
@Transactional
public OrderResult place(String username, OrderRequest request, Customer customer) {
// 1. Strategy : calcul du meilleur prix applicable
PriceResult price = pricingService.calculatePrice(request.product(), customer);
// 2. Command : création annulable, dans l'historique de l'utilisateur
// (CreateOrderCommand suit le modèle de CreateArticleCommand)
CreateOrderCommand command = new CreateOrderCommand(orderRepository, request, price);
CommandResult created = histories.forUser(username).executeCommand(command);
if (!created.success()) {
return OrderResult.failed(created.message());
}
// 3. Mediator : coordination stock, paiement, expédition, notifications.
// La commande est créée à l'état initial ; les transitions suivantes
// (confirmation, expédition, livraison, annulation) relèvent du State.
Order order = orderRepository.findById(command.getCreatedOrderId()).orElseThrow();
return orchestrator.processOrder(order);
}
}
Les contrôles HTTP (rate limiting, authentification, autorisation, validation) interviennent en amont, via la chaîne de filtres vue plus haut. Les événements applicatifs de l'article précédent (Observer) permettent ensuite de brancher le stock, les e-mails et la facturation sans toucher à ce service.
Ce qu'il faut retenir
| Pattern | Quand l'utiliser | Exemples JDK / Spring |
|---|---|---|
| Iterator | Parcours uniforme de collections | Iterable, Iterator, Spliterator |
| Visitor | Opérations sur une hiérarchie stable, sans la modifier | FileVisitor, visiteurs d'AST ; alternative : sealed + switch |
Bilan de la série
Cette série de neuf articles couvre 17 design patterns du Gang of Four.
Créationnels (4) : Singleton, Factory Method, Abstract Factory, Builder. Ils décrivent comment créer des objets.
Structurels (4) : Adapter, Decorator, Facade, Proxy. Ils décrivent comment assembler des objets pour obtenir de nouvelles fonctionnalités.
Comportementaux, orchestration (4) : Strategy, Observer, Template Method, State. Ils décrivent comment les objets se comportent et changent de comportement.
Comportementaux, communication (5) : Command, Chain of Responsibility, Mediator, Iterator, Visitor. Ils décrivent comment les objets communiquent et collaborent.
Le catalogue du GoF compte 23 patterns. L'objectif n'est pas de tous connaître par cœur, mais de reconnaître les problèmes qu'ils résolvent dans le code courant : un switch sur un type qui grandit sans cesse renvoie à Strategy ou à State, de la duplication dans un processus à Template Method, un couplage fort entre services à Observer ou à Mediator.
Les patterns ne s'appliquent pas mécaniquement : ce sont des noms donnés à des solutions que les développeurs rencontrent régulièrement, ce qui facilite la discussion dans une équipe.
Partie précédente : Chain of Responsibility et Mediator.
Série « Design Patterns en Java » : 9 articles en 4 familles, chacun lisible seul.
- Créationnels : Singleton et Factory Method (1/2) · Abstract Factory et Builder (2/2)
- Structurels : Adapter et Decorator (1/2) · Facade et Proxy (2/2)
- Comportementaux, orchestration : Strategy et Observer (1/2) · Template Method et State (2/2)
- Comportementaux, communication : Command (1/3) · Chain of Responsibility et Mediator (2/3) · Iterator et Visitor (3/3, cet article)