Salta al contenuto principale

06 - Command, Chain of Responsibility e Template Method

Pattern comportamentali avanzati: Command con undo/redo, Chain of Responsibility per middleware e Template Method per algoritmi riutilizzabili.

Cosa ne pensi di questo articolo?

06 - Command, Chain of Responsibility e Template Method
Design PatternsFoto:Daniil KomovPexels

In Breve

Pattern comportamentali avanzati: Command con undo/redo, Chain of Responsibility per middleware e Template Method per algoritmi riutilizzabili.

Command, Chain of Responsibility e Template Method

I pattern comportamentali gestiscono la comunicazione e le responsabilità tra oggetti. Command incapsula richieste come oggetti (undo/redo), Chain of Responsibility passa richieste lungo una catena di handler (middleware), e Template Method definisce scheletri di algoritmi con passi personalizzabili.

🎯 Cosa Imparerai

  • Command pattern: incapsulare azioni come oggetti
  • Implementare undo/redo con Command
  • Chain of Responsibility: catena di handler
  • Template Method: algoritmi con hook points

Command Pattern

Il Command pattern incapsula una richiesta come un oggetto, permettendo di parametrizzare metodi con operazioni, mettere in coda richieste, e supportare operazioni annullabili (undo/redo).

TypeScript - Command Base
// Interfaccia Command
interface Command {{ '{' }}
  execute(): void;
  undo(): void;
{{ '}' }}

// Receiver: oggetto che esegue l'azione reale
class Light {{ '{' }}
  private isOn: boolean = false;

  turnOn(): void {{ '{' }}
    this.isOn = true;
    console.log("💡 Luce accesa");
  {{ '}' }}

  turnOff(): void {{ '{' }}
    this.isOn = false;
    console.log("🌑 Luce spenta");
  {{ '}' }}

  getState(): boolean {{ '{' }}
    return this.isOn;
  {{ '}' }}
{{ '}' }}

// Concrete Commands
class TurnOnCommand implements Command {{ '{' }}
  constructor(private light: Light) {{ '{' }}{{ '}' }}

  execute(): void {{ '{' }}
    this.light.turnOn();
  {{ '}' }}

  undo(): void {{ '{' }}
    this.light.turnOff();
  {{ '}' }}
{{ '}' }}

class TurnOffCommand implements Command {{ '{' }}
  constructor(private light: Light) {{ '{' }}{{ '}' }}

  execute(): void {{ '{' }}
    this.light.turnOff();
  {{ '}' }}

  undo(): void {{ '{' }}
    this.light.turnOn();
  {{ '}' }}
{{ '}' }}

// Invoker: esegue i comandi
class RemoteControl {{ '{' }}
  private history: Command[] = [];

  executeCommand(command: Command): void {{ '{' }}
    command.execute();
    this.history.push(command);
  {{ '}' }}

  undo(): void {{ '{' }}
    const command = this.history.pop();
    if (command) {{ '{' }}
      command.undo();
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

// Utilizzo
const light = new Light();
const remote = new RemoteControl();

remote.executeCommand(new TurnOnCommand(light));  // "💡 Luce accesa"
remote.executeCommand(new TurnOffCommand(light)); // "🌑 Luce spenta"
remote.undo(); // "💡 Luce accesa" (annulla spegnimento)

Command con Undo/Redo Completo

Implementazione avanzata con undo/redo multipli:

Editor di Testo con Undo/Redo
class TextEditor {{ '{' }}
  private content: string = "";

  getContent(): string {{ '{' }}
    return this.content;
  {{ '}' }}

  setContent(content: string): void {{ '{' }}
    this.content = content;
  {{ '}' }}

  append(text: string): void {{ '{' }}
    this.content += text;
  {{ '}' }}

  delete(length: number): void {{ '{' }}
    this.content = this.content.slice(0, -length);
  {{ '}' }}
{{ '}' }}

class AppendCommand implements Command {{ '{' }}
  constructor(
    private editor: TextEditor,
    private text: string
  ) {{ '{' }}{{ '}' }}

  execute(): void {{ '{' }}
    this.editor.append(this.text);
  {{ '}' }}

  undo(): void {{ '{' }}
    this.editor.delete(this.text.length);
  {{ '}' }}
{{ '}' }}

class DeleteCommand implements Command {{ '{' }}
  private deletedText: string = "";

  constructor(
    private editor: TextEditor,
    private length: number
  ) {{ '{' }}{{ '}' }}

  execute(): void {{ '{' }}
    const content = this.editor.getContent();
    this.deletedText = content.slice(-this.length);
    this.editor.delete(this.length);
  {{ '}' }}

  undo(): void {{ '{' }}
    this.editor.append(this.deletedText);
  {{ '}' }}
{{ '}' }}

class CommandManager {{ '{' }}
  private history: Command[] = [];
  private redoStack: Command[] = [];

  execute(command: Command): void {{ '{' }}
    command.execute();
    this.history.push(command);
    this.redoStack = []; // Pulisci redo stack su nuova azione
  {{ '}' }}

  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);
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

// Utilizzo
const editor = new TextEditor();
const manager = new CommandManager();

manager.execute(new AppendCommand(editor, "Hello "));
manager.execute(new AppendCommand(editor, "World"));
console.log(editor.getContent()); // "Hello World"

manager.undo();
console.log(editor.getContent()); // "Hello "

manager.redo();
console.log(editor.getContent()); // "Hello World"

Chain of Responsibility Pattern

Il Chain of Responsibility permette a più oggetti di gestire una richiesta senza che il mittente conosca quale la gestirà. La richiesta passa lungo una catena finché qualcuno la gestisce.

Chain of Responsibility Base
// Handler astratto
abstract class Handler {{ '{' }}
  protected next: Handler | null = null;

  setNext(handler: Handler): Handler {{ '{' }}
    this.next = handler;
    return handler; // Fluent interface
  {{ '}' }}

  handle(request: string): void {{ '{' }}
    if (this.canHandle(request)) {{ '{' }}
      this.process(request);
    {{ '}' }} else if (this.next) {{ '{' }}
      this.next.handle(request);
    {{ '}' }} else {{ '{' }}
      console.log(`❌ Nessun handler per: ${{ '{' }}request{{ '}' }}`);
    {{ '}' }}
  {{ '}' }}

  protected abstract canHandle(request: string): boolean;
  protected abstract process(request: string): void;
{{ '}' }}

// Concrete Handlers
class SupportLevel1 extends Handler {{ '{' }}
  protected canHandle(request: string): boolean {{ '{' }}
    return request.includes("password");
  {{ '}' }}

  protected process(request: string): void {{ '{' }}
    console.log("👤 Level 1: Reset password inviato");
  {{ '}' }}
{{ '}' }}

class SupportLevel2 extends Handler {{ '{' }}
  protected canHandle(request: string): boolean {{ '{' }}
    return request.includes("bug");
  {{ '}' }}

  protected process(request: string): void {{ '{' }}
    console.log("🔧 Level 2: Investigazione bug iniziata");
  {{ '}' }}
{{ '}' }}

class SupportLevel3 extends Handler {{ '{' }}
  protected canHandle(request: string): boolean {{ '{' }}
    return request.includes("crash");
  {{ '}' }}

  protected process(request: string): void {{ '{' }}
    console.log("🚨 Level 3: Crash critico escalato a engineering");
  {{ '}' }}
{{ '}' }}

// Setup della catena
const level1 = new SupportLevel1();
const level2 = new SupportLevel2();
const level3 = new SupportLevel3();

level1.setNext(level2).setNext(level3);

// Utilizzo
level1.handle("Ho dimenticato la password"); // "👤 Level 1: Reset password inviato"
level1.handle("C'è un bug nel form"); // "🔧 Level 2: Investigazione bug iniziata"
level1.handle("L'app crasha"); // "🚨 Level 3: Crash critico escalato a engineering"
level1.handle("Domanda generica"); // "❌ Nessun handler per: Domanda generica"

Chain of Responsibility: Middleware HTTP

Esempio pratico: pipeline di middleware per gestire richieste HTTP:

HTTP Middleware Chain
interface Request {{ '{' }}
  url: string;
  method: string;
  headers: Record<string, string>;
  body?: any;
{{ '}' }}

type NextFunction = () => void;

abstract class Middleware {{ '{' }}
  protected next: Middleware | null = null;

  setNext(middleware: Middleware): Middleware {{ '{' }}
    this.next = middleware;
    return middleware;
  {{ '}' }}

  handle(req: Request, next: NextFunction): void {{ '{' }}
    this.process(req, () => {{ '{' }}
      if (this.next) {{ '{' }}
        this.next.handle(req, next);
      {{ '}' }} else {{ '{' }}
        next();
      {{ '}' }}
    {{ '}' }});
  {{ '}' }}

  protected abstract process(req: Request, next: NextFunction): void;
{{ '}' }}

class AuthMiddleware extends Middleware {{ '{' }}
  protected process(req: Request, next: NextFunction): void {{ '{' }}
    console.log("🔐 Checking authentication...");
    if (req.headers['authorization']) {{ '{' }}
      console.log("✅ Authenticated");
      next();
    {{ '}' }} else {{ '{' }}
      console.log("❌ Unauthorized - blocking request");
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

class LoggingMiddleware extends Middleware {{ '{' }}
  protected process(req: Request, next: NextFunction): void {{ '{' }}
    console.log(`📝 ${{ '{' }}req.method{{ '}' }} ${{ '{' }}req.url{{ '}' }}`);
    next();
  {{ '}' }}
{{ '}' }}

class ValidationMiddleware extends Middleware {{ '{' }}
  protected process(req: Request, next: NextFunction): void {{ '{' }}
    console.log("✔️ Validating request...");
    if (req.method === 'POST' && !req.body) {{ '{' }}
      console.log("❌ Validation failed: missing body");
    {{ '}' }} else {{ '{' }}
      console.log("✅ Validation passed");
      next();
    {{ '}' }}
  {{ '}' }}
{{ '}' }}

// Setup middleware chain
const auth = new AuthMiddleware();
const logging = new LoggingMiddleware();
const validation = new ValidationMiddleware();

auth.setNext(logging).setNext(validation);

// Utilizzo
const request: Request = {{ '{' }}
  url: '/api/users',
  method: 'POST',
  headers: {{ '{' }} authorization: 'Bearer token123' {{ '}' }},
  body: {{ '{' }} name: 'Alice' {{ '}' }}
{{ '}' }};

auth.handle(request, () => console.log("🎯 Request processed!"));
// Output:
// 🔐 Checking authentication...
// ✅ Authenticated
// 📝 POST /api/users
// ✔️ Validating request...
// ✅ Validation passed
// 🎯 Request processed!

Template Method Pattern

Il Template Method definisce lo scheletro di un algoritmo in una classe base, delegando alcuni passi alle sottoclassi. Le sottoclassi ridefiniscono certi passi senza cambiare la struttura complessiva.

Template Method Base
abstract class DataProcessor {{ '{' }}
  // Template method: definisce l'algoritmo
  public process(): void {{ '{' }}
    this.readData();
    this.processData();
    this.saveData();
    this.sendNotification(); // Hook opzionale
  {{ '}' }}

  // Passi astratti (da implementare)
  protected abstract readData(): void;
  protected abstract processData(): void;
  protected abstract saveData(): void;

  // Hook: metodo con implementazione di default
  protected sendNotification(): void {{ '{' }}
    // Default: non fa nulla (le sottoclassi possono override)
  {{ '}' }}
{{ '}' }}

// Concrete implementation
class CSVProcessor extends DataProcessor {{ '{' }}
  protected readData(): void {{ '{' }}
    console.log("📄 Reading CSV file...");
  {{ '}' }}

  protected processData(): void {{ '{' }}
    console.log("🔄 Parsing CSV rows...");
  {{ '}' }}

  protected saveData(): void {{ '{' }}
    console.log("💾 Saving to database...");
  {{ '}' }}

  protected sendNotification(): void {{ '{' }}
    console.log("📧 Email notification sent");
  {{ '}' }}
{{ '}' }}

class JSONProcessor extends DataProcessor {{ '{' }}
  protected readData(): void {{ '{' }}
    console.log("📄 Reading JSON file...");
  {{ '}' }}

  protected processData(): void {{ '{' }}
    console.log("🔄 Parsing JSON objects...");
  {{ '}' }}

  protected saveData(): void {{ '{' }}
    console.log("💾 Saving to cache...");
  {{ '}' }}

  // Non override sendNotification (usa default)
{{ '}' }}

// Utilizzo
const csvProcessor = new CSVProcessor();
csvProcessor.process();
// 📄 Reading CSV file...
// 🔄 Parsing CSV rows...
// 💾 Saving to database...
// 📧 Email notification sent

const jsonProcessor = new JSONProcessor();
jsonProcessor.process();
// 📄 Reading JSON file...
// 🔄 Parsing JSON objects...
// 💾 Saving to cache...
// (nessuna notifica)

Template Method Avanzato: Test Framework

Esempio realistico: framework per test automatizzati con setup/teardown:

Test Framework con Template Method
abstract class TestCase {{ '{' }}
  // Template method
  public run(): void {{ '{' }}
    this.setUp();
    try {{ '{' }}
      this.runTest();
      console.log("✅ Test passed");
    {{ '}' }} catch (error) {{ '{' }}
      console.log(`❌ Test failed: ${{ '{' }}error{{ '}' }}`);
    {{ '}' }} finally {{ '{' }}
      this.tearDown();
    {{ '}' }}
  {{ '}' }}

  // Hook methods
  protected setUp(): void {{ '{' }}
    console.log("🔧 Setting up test...");
  {{ '}' }}

  protected tearDown(): void {{ '{' }}
    console.log("🧹 Cleaning up...");
  {{ '}' }}

  // Passo astratto: ogni test definisce il proprio
  protected abstract runTest(): void;
{{ '}' }}

// Test concreti
class DatabaseTest extends TestCase {{ '{' }}
  private connection: any;

  protected setUp(): void {{ '{' }}
    super.setUp();
    console.log("🔌 Connecting to test database...");
    this.connection = {{ '{' }} connected: true {{ '}' }};
  {{ '}' }}

  protected runTest(): void {{ '{' }}
    console.log("🧪 Testing database queries...");
    if (!this.connection.connected) {{ '{' }}
      throw new Error("Connection failed");
    {{ '}' }}
  {{ '}' }}

  protected tearDown(): void {{ '{' }}
    console.log("🔌 Closing database connection...");
    this.connection = null;
    super.tearDown();
  {{ '}' }}
{{ '}' }}

class APITest extends TestCase {{ '{' }}
  protected setUp(): void {{ '{' }}
    super.setUp();
    console.log("🌐 Starting mock API server...");
  {{ '}' }}

  protected runTest(): void {{ '{' }}
    console.log("🧪 Testing API endpoints...");
    // Test logic here
  {{ '}' }}

  protected tearDown(): void {{ '{' }}
    console.log("🛑 Stopping mock server...");
    super.tearDown();
  {{ '}' }}
{{ '}' }}

// Utilizzo
new DatabaseTest().run();
// 🔧 Setting up test...
// 🔌 Connecting to test database...
// 🧪 Testing database queries...
// ✅ Test passed
// 🔌 Closing database connection...
// 🧹 Cleaning up...

new APITest().run();
// 🔧 Setting up test...
// 🌐 Starting mock API server...
// 🧪 Testing API endpoints...
// ✅ Test passed
// 🛑 Stopping mock server...
// 🧹 Cleaning up...

Quando Usare Ogni Pattern

✅ Usa Command quando:

  • Serve undo/redo nelle applicazioni
  • Vuoi mettere in coda operazioni (job queue)
  • Serve parametrizzare oggetti con azioni
  • Logging e transazioni: registrare comandi eseguiti

✅ Usa Chain of Responsibility quando:

  • Più oggetti possono gestire una richiesta, ma non sai quale
  • Vuoi middleware/pipeline per processing sequenziale
  • Set di handler può cambiare dinamicamente
  • Gestione eventi, logging, autenticazione

✅ Usa Template Method quando:

  • Hai algoritmi con struttura comune ma passi diversi
  • Vuoi evitare duplicazione di codice simile
  • Serve controllo su quali passi le sottoclassi possono override
  • Framework: definisci scheletro, utenti implementano dettagli

Esempio Pratico: Sistema di Pagamento

Tutti e Tre i Pattern Combinati
// COMMAND: Transazioni con undo
interface PaymentCommand {{ '{' }}
  execute(): void;
  undo(): void;
{{ '}' }}

class ChargeCommand implements PaymentCommand {{ '{' }}
  constructor(
    private account: string,
    private amount: number
  ) {{ '{' }}{{ '}' }}

  execute(): void {{ '{' }}
    console.log(`💳 Charged ${{ '{' }}this.amount{{ '}' }} to ${{ '{' }}this.account{{ '}' }}`);
  {{ '}' }}

  undo(): void {{ '{' }}
    console.log(`↩️ Refunded ${{ '{' }}this.amount{{ '}' }} to ${{ '{' }}this.account{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

// CHAIN: Validazione pagamento
abstract class PaymentHandler {{ '{' }}
  protected next: PaymentHandler | null = null;

  setNext(handler: PaymentHandler): PaymentHandler {{ '{' }}
    this.next = handler;
    return handler;
  {{ '}' }}

  handle(amount: number): boolean {{ '{' }}
    if (this.validate(amount)) {{ '{' }}
      return this.next ? this.next.handle(amount) : true;
    {{ '}' }}
    return false;
  {{ '}' }}

  protected abstract validate(amount: number): boolean;
{{ '}' }}

class AmountValidator extends PaymentHandler {{ '{' }}
  protected validate(amount: number): boolean {{ '{' }}
    if (amount > 0) {{ '{' }}
      console.log("✅ Amount valid");
      return true;
    {{ '}' }}
    console.log("❌ Invalid amount");
    return false;
  {{ '}' }}
{{ '}' }}

class FraudDetector extends PaymentHandler {{ '{' }}
  protected validate(amount: number): boolean {{ '{' }}
    if (amount < 10000) {{ '{' }}
      console.log("✅ No fraud detected");
      return true;
    {{ '}' }}
    console.log("🚨 Potential fraud");
    return false;
  {{ '}' }}
{{ '}' }}

// TEMPLATE METHOD: Processamento pagamento
abstract class PaymentProcessor {{ '{' }}
  public processPayment(amount: number): void {{ '{' }}
    if (!this.validatePayment(amount)) return;
    this.deductAmount(amount);
    this.sendReceipt();
  {{ '}' }}

  protected abstract validatePayment(amount: number): boolean;
  protected abstract deductAmount(amount: number): void;

  protected sendReceipt(): void {{ '{' }}
    console.log("📧 Receipt sent");
  {{ '}' }}
{{ '}' }}

class CreditCardProcessor extends PaymentProcessor {{ '{' }}
  private validator = new AmountValidator();

  constructor() {{ '{' }}
    super();
    this.validator.setNext(new FraudDetector());
  {{ '}' }}

  protected validatePayment(amount: number): boolean {{ '{' }}
    return this.validator.handle(amount);
  {{ '}' }}

  protected deductAmount(amount: number): void {{ '{' }}
    console.log(`💳 Card charged ${{ '{' }}amount{{ '}' }}`);
  {{ '}' }}
{{ '}' }}

// Utilizzo combinato
const processor = new CreditCardProcessor();
processor.processPayment(500);
// ✅ Amount valid
// ✅ No fraud detected
// 💳 Card charged 500
// 📧 Receipt sent

Conclusione

Questi tre pattern comportamentali risolvono problemi diversi ma complementari. Command incapsula azioni per undo/redo e queuing, Chain of Responsibility crea pipeline flessibili di handler, e Template Method riduce duplicazione con algoritmi riutilizzabili. Combinarli crea architetture eleganti e manutenibili.

🎯 Punti Chiave

  • Command = richieste come oggetti, undo/redo, job queues
  • Chain of Responsibility = catena di handler, middleware
  • Template Method = scheletro algoritmo, hook points
  • Usa Command per operazioni annullabili
  • Usa Chain per pipeline di processing
  • Usa Template Method per evitare duplicazione

Domande frequenti

Qual è il principale scopo del Command pattern?

Il Command pattern incapsula una richiesta come un oggetto, permettendo di parametrizzare metodi con operazioni, mettere in coda richieste e supportare operazioni annullabili (undo/redo).

Come funziona il Chain of Responsibility per gestire le richieste?

Il Chain of Responsibility passa la richiesta lungo una catena di handler finché qualcuno la gestisce, senza che il mittente conosca quale lo farà.

Qual è l'obiettivo del Template Method pattern?

Il Template Method definisce lo scheletro di un algoritmo in una classe base, delegando alcuni passi alle sottoclassi per ridurre la duplicazione e creare algoritmi riutilizzabili.

Perché è importante combinare questi tre pattern comportamentali?

Combinarli crea architetture eleganti e manutenibili, risolvendo problemi diversi ma complementari come l'incapsulamento di azioni per undo/redo, la creazione di pipeline flessibili di handler e la riduzione della duplicazione con algoritmi riutilizzabili.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti