Chapitre 10
10 — Patterns Comportementaux : Command & Chain of Responsibility
10 — Patterns Comportementaux : Command & Chain of Responsibility
Chapitre 10 : Command & Chain of Responsibility
Durée estimée : 4 séances de 3h Objectifs : Maîtriser Command (undo/redo, queue, transaction) et Chain of Responsibility (middleware, pipeline, validation).
Partie 1 : Le Pattern Command
1.1 Définition et Intention
Le pattern Command transforme une requête en objet, permettant de paramétrer, mettre en file d'attente, logger, et annuler des opérations.
Intention du GoF : "Encapsuler une requête sous forme d'objet, permettant de paramétrer des clients avec des requêtes, de les mettre en file d'attente, de les logger, et de supporter les opérations annulables."
1.2 Problème résolu
Dans un éditeur de texte, chaque action (copier, coller, annuler) doit être :
- Déclenchée par l'interface utilisateur
- Exécutée sur le document
- Annulable (undo)
- Journalisable
Sans Command, l'éditeur est couplé à toutes les actions. Avec Command, chaque action est un objet autonome.
Diagramme en cours de génération...
1.3 Structure UML
Diagramme en cours de génération...
1.4 Implémentation — Undo/Redo Editor
interface Command {
execute(): void;
undo(): void;
}
class TextEditor {
private content: string = '';
insertText(position: number, text: string): void {
this.content = this.content.slice(0, position) + text + this.content.slice(position);
}
deleteText(start: number, end: number): string {
const deleted = this.content.slice(start, end);
this.content = this.content.slice(0, start) + this.content.slice(end);
return deleted;
}
getContent(): string {
return this.content;
}
toString(): string {
return this.content;
}
}
class InsertTextCommand implements Command {
private backup: string = '';
constructor(
private editor: TextEditor,
private position: number,
private text: string
) {}
execute(): void {
this.backup = this.editor.getContent();
this.editor.insertText(this.position, this.text);
}
undo(): void {
this.editor['content'] = this.backup; // Restore backup
}
}
class DeleteTextCommand implements Command {
private backup: string = '';
private deletedText: string = '';
constructor(
private editor: TextEditor,
private start: number,
private end: number
) {}
execute(): void {
this.backup = this.editor.getContent();
this.deletedText = this.editor.deleteText(this.start, this.end);
}
undo(): void {
this.editor['content'] = this.backup;
}
}
class CommandInvoker {
private history: Command[] = [];
private redoStack: Command[] = [];
private readonly maxHistory = 50;
execute(command: Command): void {
command.execute();
this.history.push(command);
this.redoStack = []; // Clear redo on new command
if (this.history.length > this.maxHistory) {
this.history.shift();
}
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo(): void {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
getHistory(): number {
return this.history.length;
}
}
// Usage
const editor = new TextEditor();
const invoker = new CommandInvoker();
invoker.execute(new InsertTextCommand(editor, 0, 'Hello'));
invoker.execute(new InsertTextCommand(editor, 5, ' World'));
console.log(editor.toString()); // "Hello World"
invoker.undo();
console.log(editor.toString()); // "Hello"
invoker.redo();
console.log(editor.toString()); // "Hello World"
invoker.execute(new DeleteTextCommand(editor, 0, 5));
console.log(editor.toString()); // " World"
1.5 Command Queue — Job Processing
interface Job {
execute(): Promise<void>;
getType(): string;
}
class EmailJob implements Job {
constructor(
private to: string,
private subject: string,
private body: string
) {}
async execute(): Promise<void> {
console.log(`Sending email to ${this.to}: ${this.subject}`);
await this.delay(100); // Simulate SMTP
console.log(`Email sent to ${this.to}`);
}
getType(): string { return 'email'; }
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class ReportJob implements Job {
constructor(
private reportName: string,
private format: string
) {}
async execute(): Promise<void> {
console.log(`Generating report: ${this.reportName}.${this.format}`);
await this.delay(500);
console.log(`Report generated: ${this.reportName}.${this.format}`);
}
getType(): string { return 'report'; }
}
class JobQueue {
private queue: Job[] = [];
private running = false;
private concurrency: number;
private activeJobs = 0;
constructor(concurrency = 2) {
this.concurrency = concurrency;
}
add(job: Job): void {
this.queue.push(job);
this.processNext();
}
private async processNext(): Promise<void> {
if (this.running || this.activeJobs >= this.concurrency) return;
this.running = true;
while (this.queue.length > 0 && this.activeJobs < this.concurrency) {
const job = this.queue.shift()!;
this.activeJobs++;
job.execute()
.catch(err => console.error(`Job ${job.getType()} failed:`, err))
.finally(() => {
this.activeJobs--;
this.running = false;
this.processNext();
});
}
this.running = false;
}
get length(): number { return this.queue.length; }
}
// Usage
const queue = new JobQueue(3);
queue.add(new EmailJob('alice@test.com', 'Welcome!', '...'));
queue.add(new EmailJob('bob@test.com', 'Invoice', '...'));
queue.add(new ReportJob('sales-2024', 'pdf'));
queue.add(new ReportJob('inventory', 'xlsx'));
1.6 Command en Java — Transaction Banking
import java.util.Stack;
// Command interface
interface Transaction {
void execute();
void undo();
}
// Receiver
class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
balance += amount;
System.out.println("Deposited: $" + amount + ", Balance: $" + balance);
}
public void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
System.out.println("Withdrew: $" + amount + ", Balance: $" + balance);
} else {
throw new RuntimeException("Insufficient funds");
}
}
public double getBalance() { return balance; }
}
class DepositCommand implements Transaction {
private BankAccount account;
private double amount;
public DepositCommand(BankAccount account, double amount) {
this.account = account;
this.amount = amount;
}
public void execute() {
account.deposit(amount);
}
public void undo() {
account.withdraw(amount);
}
}
class WithdrawCommand implements Transaction {
private BankAccount account;
private double amount;
public WithdrawCommand(BankAccount account, double amount) {
this.account = account;
this.amount = amount;
}
public void execute() {
account.withdraw(amount);
}
public void undo() {
account.deposit(amount);
}
}
class TransactionManager {
private Stack<Transaction> history = new Stack<>();
public void executeTransaction(Transaction transaction) {
transaction.execute();
history.push(transaction);
}
public void undoLastTransaction() {
if (!history.isEmpty()) {
Transaction transaction = history.pop();
transaction.undo();
}
}
}
// Usage
public class CommandDemo {
public static void main(String[] args) {
BankAccount account = new BankAccount(1000);
TransactionManager manager = new TransactionManager();
manager.executeTransaction(new DepositCommand(account, 500));
manager.executeTransaction(new WithdrawCommand(account, 200));
manager.undoLastTransaction(); // Undo withdraw
System.out.println("Final balance: $" + account.getBalance());
}
}
1.7 Command en PHP
<?php
interface Command {
public function execute(): void;
public function undo(): void;
}
class Light {
public function turnOn(): void {
echo "Light is ON\n";
}
public function turnOff(): void {
echo "Light is OFF\n";
}
}
class LightOnCommand implements Command {
public function __construct(private Light $light) {}
public function execute(): void {
$this->light->turnOn();
}
public function undo(): void {
$this->light->turnOff();
}
}
class LightOffCommand implements Command {
public function __construct(private Light $light) {}
public function execute(): void {
$this->light->turnOff();
}
public function undo(): void {
$this->light->turnOn();
}
}
class RemoteControl {
private ?Command $command = null;
private array $history = [];
public function setCommand(Command $command): void {
$this->command = $command;
}
public function pressButton(): void {
if ($this->command) {
$this->command->execute();
$this->history[] = $this->command;
}
}
public function pressUndo(): void {
$command = array_pop($this->history);
if ($command) {
$command->undo();
}
}
}
// Usage
$light = new Light();
$on = new LightOnCommand($light);
$off = new LightOffCommand($light);
$remote = new RemoteControl();
$remote->setCommand($on);
$remote->pressButton(); // Light is ON
$remote->setCommand($off);
$remote->pressButton(); // Light is OFF
$remote->pressUndo(); // Light is ON
?>
Partie 2 : Le Pattern Chain of Responsibility
2.1 Définition et Intention
Chain of Responsibility permet de passer une requête le long d'une chaîne de handlers potentiels. Chaque handler décide s'il traite la requête ou la passe au suivant.
Intention du GoF : "Éviter de coupler l'émetteur d'une requête à son récepteur en donnant à plusieurs objets la chance de traiter la requête. Chaîner les objets récepteurs et passer la requête le long de la chaîne jusqu'à ce qu'un objet la traite."
2.2 Problème résolu
Un système de support client : une requête arrive et doit être traitée par le bon niveau (FAQ → Chatbot → Niveau1 → Niveau2 → Manager).
Diagramme en cours de génération...
2.3 Structure UML
Diagramme en cours de génération...
2.4 Implémentation — Middleware HTTP
interface HttpRequest {
method: string;
url: string;
headers: Record<string, string>;
body?: any;
user?: any;
}
interface HttpResponse {
statusCode: number;
body: any;
}
interface Middleware {
setNext(middleware: Middleware): Middleware;
handle(request: HttpRequest, response: HttpResponse): Promise<void>;
}
abstract class AbstractMiddleware implements Middleware {
private next: Middleware | null = null;
setNext(middleware: Middleware): Middleware {
this.next = middleware;
return middleware;
}
async handle(request: HttpRequest, response: HttpResponse): Promise<void> {
if (this.next) {
await this.next.handle(request, response);
}
}
}
class AuthMiddleware extends AbstractMiddleware {
async handle(request: HttpRequest, response: HttpResponse): Promise<void> {
const token = request.headers['authorization'];
if (!token) {
response.statusCode = 401;
response.body = { error: 'Unauthorized' };
return;
}
// Validate token
if (token === 'valid-token') {
request.user = { id: 1, role: 'user' };
await super.handle(request, response);
} else {
response.statusCode = 403;
response.body = { error: 'Invalid token' };
}
}
}
class LoggingMiddleware extends AbstractMiddleware {
async handle(request: HttpRequest, response: HttpResponse): Promise<void> {
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`);
const start = Date.now();
await super.handle(request, response);
const duration = Date.now() - start;
console.log(`[${request.method} ${request.url}] ${response.statusCode} (${duration}ms)`);
}
}
class RateLimitMiddleware extends AbstractMiddleware {
private requests: Map<string, number[]> = new Map();
private readonly limit = 100;
private readonly windowMs = 60000;
async handle(request: HttpRequest, response: HttpResponse): Promise<void> {
const ip = request.headers['x-forwarded-for'] || 'unknown';
const now = Date.now();
if (!this.requests.has(ip)) {
this.requests.set(ip, []);
}
const timestamps = this.requests.get(ip)!;
const recent = timestamps.filter(t => now - t < this.windowMs);
if (recent.length >= this.limit) {
response.statusCode = 429;
response.body = { error: 'Too Many Requests' };
return;
}
recent.push(now);
this.requests.set(ip, recent);
await super.handle(request, response);
}
}
class RouterMiddleware extends AbstractMiddleware {
private routes: Map<string, (req: HttpRequest, res: HttpResponse) => Promise<void>> = new Map();
get(path: string, handler: (req: HttpRequest, res: HttpResponse) => Promise<void>): void {
this.routes.set(`GET:${path}`, handler);
}
post(path: string, handler: (req: HttpRequest, res: HttpResponse) => Promise<void>): void {
this.routes.set(`POST:${path}`, handler);
}
async handle(request: HttpRequest, response: HttpResponse): Promise<void> {
const key = `${request.method}:${request.url}`;
const handler = this.routes.get(key);
if (handler) {
await handler(request, response);
} else {
response.statusCode = 404;
response.body = { error: 'Not Found' };
}
}
}
// Usage
const router = new RouterMiddleware();
router.get('/api/users', async (req, res) => {
res.statusCode = 200;
res.body = { users: [{ id: 1, name: 'Alice' }] };
});
router.post('/api/users', async (req, res) => {
res.statusCode = 201;
res.body = { id: 2, name: req.body?.name };
});
// Build the chain
const auth = new AuthMiddleware();
const logging = new LoggingMiddleware();
const rateLimit = new RateLimitMiddleware();
auth.setNext(logging).setNext(rateLimit).setNext(router);
// Process a request
const request: HttpRequest = {
method: 'GET',
url: '/api/users',
headers: { 'authorization': 'valid-token' }
};
const response: HttpResponse = { statusCode: 200, body: {} };
await auth.handle(request, response);
console.log('Response:', response);
2.5 Chain of Responsibility — Validation Pipeline
interface ValidationContext {
field: string;
value: any;
errors: string[];
}
interface ValidationHandler {
setNext(handler: ValidationHandler): ValidationHandler;
validate(context: ValidationContext): void;
}
abstract class BaseValidationHandler implements ValidationHandler {
private next: ValidationHandler | null = null;
setNext(handler: ValidationHandler): ValidationHandler {
this.next = handler;
return handler;
}
validate(context: ValidationContext): void {
this.process(context);
if (this.next) {
this.next.validate(context);
}
}
protected abstract process(context: ValidationContext): void;
}
class RequiredHandler extends BaseValidationHandler {
protected process(context: ValidationContext): void {
if (context.value === null || context.value === undefined || context.value === '') {
context.errors.push(`${context.field} is required`);
}
}
}
class MinLengthHandler extends BaseValidationHandler {
constructor(private min: number) { super(); }
protected process(context: ValidationContext): void {
if (typeof context.value === 'string' && context.value.length < this.min) {
context.errors.push(`${context.field} must be at least ${this.min} characters`);
}
}
}
class MaxLengthHandler extends BaseValidationHandler {
constructor(private max: number) { super(); }
protected process(context: ValidationContext): void {
if (typeof context.value === 'string' && context.value.length > this.max) {
context.errors.push(`${context.field} must be at most ${this.max} characters`);
}
}
}
class EmailFormatHandler extends BaseValidationHandler {
private regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
protected process(context: ValidationContext): void {
if (typeof context.value === 'string' && context.value && !this.regex.test(context.value)) {
context.errors.push(`${context.field} is not a valid email`);
}
}
}
class UniqueEmailHandler extends BaseValidationHandler {
constructor(private existingEmails: string[]) { super(); }
protected process(context: ValidationContext): void {
if (this.existingEmails.includes(context.value)) {
context.errors.push(`${context.field} already exists`);
}
}
}
class FormValidator {
private fields: Map<string, ValidationHandler> = new Map();
addField(field: string, ...handlers: ValidationHandler[]): void {
if (handlers.length === 0) return;
// Chain handlers
for (let i = 0; i < handlers.length - 1; i++) {
handlers[i].setNext(handlers[i + 1]);
}
this.fields.set(field, handlers[0]);
}
validate(data: Record<string, any>): Record<string, string[]> {
const errors: Record<string, string[]> = {};
for (const [field, handler] of this.fields) {
const context: ValidationContext = {
field,
value: data[field],
errors: []
};
handler.validate(context);
if (context.errors.length > 0) {
errors[field] = context.errors;
}
}
return errors;
}
}
// Usage
const validator = new FormValidator();
validator.addField(
'email',
new RequiredHandler(),
new EmailFormatHandler(),
new MaxLengthHandler(255),
new UniqueEmailHandler(['existing@test.com'])
);
validator.addField(
'password',
new RequiredHandler(),
new MinLengthHandler(8),
new MaxLengthHandler(128)
);
const errors = validator.validate({
email: '',
password: 'short'
});
console.log(errors);
// { email: ['email is required', 'email is not a valid email'],
// password: ['password must be at least 8 characters'] }
2.6 Chain of Responsibility en Go
package main
import "fmt"
type Handler interface {
SetNext(handler Handler) Handler
Handle(request string) string
}
type BaseHandler struct {
next Handler
}
func (b *BaseHandler) SetNext(handler Handler) Handler {
b.next = handler
return handler
}
func (b *BaseHandler) Handle(request string) string {
if b.next != nil {
return b.next.Handle(request)
}
return ""
}
// Concrete handlers
type AuthHandler struct {
BaseHandler
}
func (h *AuthHandler) Handle(request string) string {
if request == "unauthenticated" {
return "401 Unauthorized"
}
return h.BaseHandler.Handle(request)
}
type PermissionHandler struct {
BaseHandler
requiredRole string
}
func (h *PermissionHandler) Handle(request string) string {
if request == "no-permission" {
return "403 Forbidden"
}
return h.BaseHandler.Handle(request)
}
type CacheHandler struct {
BaseHandler
}
func (h *CacheHandler) Handle(request string) string {
if request == "cached" {
return "200 OK (cached)"
}
return h.BaseHandler.Handle(request)
}
type RouteHandler struct {
BaseHandler
}
func (h *RouteHandler) Handle(request string) string {
return fmt.Sprintf("200 OK (handled: %s)", request)
}
func main() {
auth := &AuthHandler{}
perm := &PermissionHandler{}
cache := &CacheHandler{}
route := &RouteHandler{}
auth.SetNext(perm).SetNext(cache).SetNext(route)
tests := []string{"authenticated", "unauthenticated", "cached", "no-permission"}
for _, test := range tests {
result := auth.Handle(test)
fmt.Printf("Request %q -> %s\n", test, result)
}
}
Partie 3 : Command vs Chain of Responsibility
3.1 Différences
| Critère | Command | Chain of Responsibility |
|---|---|---|
| Unité | Une opération | Un gestionnaire |
| Flux | 1:1 (commande→receiver) | 1:N (requête→chaîne) |
| Contrôle | Centralisé (Invoker) | Distribué |
| Undo/Redo | Oui | Non |
| Queue | Oui | Non |
| Composition | Macro Command | Chaîne dynamique |
3.2 Quand les combiner ?
Un système de validation peut utiliser :
- Chain of Responsibility pour le pipeline de validation
- Command pour encapsuler chaque règle de validation comme une commande undoable
Partie 4 : Patterns Connexes
- Memento : Souvent utilisé avec Command pour sauvegarder l'état avant exécution
- Composite : MacroCommand = Composite de Command
- Strategy : Les handlers de la chaîne peuvent être des stratégies
- Observer : Notifier quand une commande est exécutée
Résumé
- Command : Encapsule une requête → undo/redo, queue, transaction
- Chain of Responsibility : Pipeline de handlers → middleware, validation
- Complémentaires : Command pour l'unité, CoR pour le pipeline
- Modernes : Express middleware, Redux middleware, HTTP pipeline
Prochain chapitre : State, Visitor & Template Method (avancé).