Versione per la stampa       Invia     
Valuta il contenuto e lascia un commento
MSDN
MSDN Library
Articoli tecnici
Design pattern
 Design pattern per esempi: i GoF co...
Design pattern per esempi: i GoF comportamentali

Di Riccardo Golia - Microsoft MVP

In questa pagina

I design pattern comportamentali I design pattern comportamentali
Command Command
Observer Observer
Strategy Strategy
Template Method Template Method

Questo articolo conclude la carrellata di esempi riguardanti i design pattern della famiglia dei GoF (Gang of Four). Nella prima parte sono stati affrontati i pattern GoF creazionali, ovvero quelli che riguardano la creazione di istanze. Nella seconda parte sono stati presi in considerazione i pattern GoF strutturali relativi principalmente alla composizione di classi e oggetti. In questo articolo vengono trattati i design pattern comportamentali che si occupano del modo in cui classi e oggetti interagiscono reciprocamente e distribuiscono fra loro le responsabilità.

Come per i GoF creazionali e strutturali, anche in questo articolo per ogni pattern viene specificato quanto segue:

  • un’introduzione con la spiegazione delle motivazioni che possono comportare l’uso del design pattern in questione;

  • l’elenco dei partecipanti del pattern con l’indicazione delle rispettive responsabilità e lo schema UML di base (diagramma delle classi);

  • la spiegazione dell’esempio e il relativo schema UML (diagramma delle classi);

  • l’implementazione dell’esempio in C#.

I design pattern comportamentali

I design pattern comportamentali si riferiscono alla distribuzione delle responsabilità tra oggetti tra loro correlati. Essi non affrontano unicamente gli aspetti relativi alla struttura degli oggetti e delle classi interagenti, ma si focalizzano soprattutto sulle modalità di comunicazione e collaborazione.

La maggior parte di questi pattern fornisce soluzioni per incapsulare le diverse funzionalità in un oggetto specifico con l’intento di delegare ad esso l’esecuzione del codice vero e proprio. Questo approccio permette in generale di eliminare le dipendenze dirette tra i vari oggetti coinvolti, limitando l’accoppiamento e facilitando la possibilità di estendere e modificare il codice senza grossi sforzi. La distribuzione delle responsabilità porta inevitabilmente a ridurre la genericità di ciascun partecipante dei pattern, aspetto in gran parte dovuto alla forte specializzazione che caratterizza le diverse classi che di volta in volta sono delegate a fornire le funzionalità richieste.

Command

Il pattern Command (noto anche come Action o Transaction) permette di inoltrare richieste ad oggetti senza conoscere assolutamente nulla dell’operazione da eseguire o del destinatario della richiesta. Questo è possibile per il fatto che il pattern in questione tratta la richiesta come un oggetto differente rispetto sia al richiedente che all’oggetto destinatario. Questo oggetto specifica l’azione da svolgere sul destinatario, sfruttandone i comportamenti in modo da tale da poter portare a termine la richiesta.

Il pattern Command permette quindi di incapsulare una richiesta in un oggetto permettendo al client di inoltrare richieste di varia natura, anche in funzione di destinatari diversi. Il pattern in questione può essere applicato:

  • per parametrizzare gli oggetti rispetto ad una azione da compiere;

  • per specificare, accodare ed eseguire svariate richieste in tempi diversi, anche trasferendo un comando da un contesto di esecuzione ad un altro;

  • per consentire l’annullamento delle operazioni eseguite (undo, rollback), mantenendo preventivamente lo stato per annullare gli effetti dei comandi stessi.

Il vantaggio più significativo nell’applicazione di questo pattern è il fatto di ottenere un perfetto disaccoppiamento tra l’oggetto che invoca il comando e il destinatario, ovvero quello che conosce il modo per portare a termine l’operazione. Questo aspetto consente di poter aggiungere comandi ulteriori associati a destinatari diversi in modo abbastanza immediato senza che sia necessario modificare le classi già esistenti. Inoltre il fatto che i comandi siano oggetti distinti permette di poterli eventualmente aggregare insieme (applicando il pattern Composite) allo scopo di formare un comando complesso, costituito da una serie arbitraria di azioni.

*

I partecipanti di questo pattern sono (tra parentesi sono indicati gli oggetti equivalenti nell’esempio proposto successivamente):

  • Command (ICommand)
    Definisce l’interfaccia di riferimento per ogni comando.

  • ConcreteCommand (Open, Read, Write e Close)
    Definisce un legame tra il Receiver e un’azione. Implementa in modo particolare il metodo Execute() invocando i metodi del Receiver.

  • Invoker  (Reader e Writer)
    Aggrega i diversi comandi e delega a loro l’esecuzione delle azioni previste.

  • Receiver  (System.Console)
    Conosce il modo di eseguire le operazioni associate ad una particolare richiesta.

  • Client (Program)
    Tramite l’Invoker attiva ed esegue un ConcreteCommand che va a interessare il Receiver corrispondente.

L’esempio proposto si riferisce all’esecuzione di due sequenze di comandi da parte delle classi Reader e Writer. L’esecuzione di ciascuna azione consiste nella scrittura sulla console di una stringa contenente il nome di ciascun comando appartenente alla sequenza. L’oggetto di destinazione pertanto è System.Console: Reader e Writer portano a compimento le operazioni desiderate sfruttando il suo metodo WriteLine(string). I singoli comandi sono classi diverse che implementano in modo particolare il metodo Execute(), definito nell’interfaccia ICommand. Si noti come il client (classe Program) sia completamente all’oscuro sia di come ciascun comando viene eseguito, sia di quale sequenza effettiva di azioni viene intrapresa di volta in volta.

*
using System;

namespace DesignPatterns.Command
{
    public interface ICommand
    {
        void Execute();
    }

    public class Open : ICommand
    {
        public virtual void Execute()
        {
            Console.WriteLine("Open");
        }
    }

    public class Read : ICommand
    {
        public virtual void Execute()
        {
            Console.WriteLine("Read");
        }
    }
    
    public class Write : ICommand
    {
        public virtual void Execute()
        {
            Console.WriteLine("Write");
        }
    }

    public class Close : ICommand
    {
        public virtual void Execute()
        {
            Console.WriteLine("Close");
        }
    }

    public class Reader
    {
        private ICommand[] _commands;

        public Reader()
        {
            _commands = new ICommand[] { new Open(), new Read(), new Close() };
        }

        public void Read()
        {
            foreach (ICommand cmd in _commands)
                cmd.Execute();
        }
    }

    public class Writer
    {
        private ICommand[] _commands;

        public Writer()
        {
            _commands = new ICommand[] { new Open(), new Write(), new Close() };
        }

        public void Write()
        {
            foreach (ICommand cmd in _commands)
                cmd.Execute();
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            Reader reader = new Reader();
            reader.Read();
            Writer writer = new Writer();
            writer.Write();
            Console.ReadLine();
        }
    }
}

Observer

Il pattern Observer (noto anche col nome Publish-Subscribe) permette di definire una dipendenza uno a molti fra oggetti, in modo tale che se un oggetto cambia il suo stato interno, ciascuno degli oggetti dipendenti da esso viene notificato e aggiornato automaticamente. L’Observer nasce dall’esigenza di mantenere un alto livello di consistenza fra classi correlate, senza peraltro produrre situazioni di forte dipendenza e accoppiamento elevato.

Il pattern Observer si presta ad essere utilizzato in diversi casi. Ad esempio, quando un’astrazione presenta due diversi aspetti tra loro dipendenti, è possibile definire due classi in cui incapsulare questi aspetti in modo tale da poterli utilizzare in maniera indipendente. In questo scenario occorre comunque prevedere un meccanismo di comunicazione che permetta di mantenere la consistenza tra le istanze delle due classi e il pattern Observer fornisce una soluzione elegante al problema senza generare accoppiamento. Un’altra situazione tipica di utilizzo si ha quando la modifica dello stato di un oggetto (per esempio, un controllo dell’interfaccia utente) implica un cambiamento dello stato di altri oggetti correlati, a prescindere dal loro numero (per esempio, altri controlli). In questo caso la modifica dello stato dell’oggetto (detto anche Publisher) si deve propagare agli oggetti correlati (detti anche Subscriber) in modo tale che essi possano aggiornare il loro stato interno di conseguenza.

*

I partecipanti di questo pattern sono:

  • Subject (delegate Subject.Notify)
    Conosce i suoi Observer. Fornisce l’interfaccia per associare e rimuovere oggetti Observer.

  • Observer
    Fornisce l’interfaccia di notifica per gli oggetti a cui devono essere segnalati i cambiamenti di stato di Subject.

  • ConcreteSubject (Subject)
    Contiene lo stato monitorato dagli Observer a cui viene inviata la notifica.

  • ConcreteObserver (Observer)
    Mantiene un riferimento ad un oggetto ConcreteSubject. Contiene le informazioni da mantenere sincronizzate con lo stato del Subject. Implementa il metodo di gestione della notifica da eseguire allo scopo di mantenere sincronizzati gli stati degli oggetti.

Il meccanismo per inviare notifiche nell’ambito del .NET Framework è fornito in modo nativo dai tipi delegate e dagli eventi. Una classe che funge da Publisher (classe Subject) espone in generale sulla sua interfaccia una serie di eventi corrispondenti ad un tipo particolare di delegate. Le classi Subscriber (classe Observer) sottoscrivono l’evento e ad esso associano un metodo interno (comunemente detto event handler) che deve rispettare la firma definita dal tipo delegate associato all’evento. L’event handler viene chiamato nel momento in cui il Publisher inoltra ai suoi Subscriber la notifica, rendendo possibile in questo modo l’esecuzione di codice in ciascun Subscriber al variare dello stato interno del Publisher.

*
using System;

namespace DesignPatterns.Observer
{
    public class Subject
    {
        public delegate void Notify();

        public event Notify OnNotify;

        public void DoSomething()
        {
            if (OnNotify != null)
            {
                Console.WriteLine("Subject fires event");
                OnNotify();
            }
        }
    }

    public class Program
    {
        public class Observer
        {
            private static int _idx = 1;
            private int _number;

            public Observer(Subject s)
            {
                s.OnNotify += new Subject.Notify(EventHandler);
                _number = _idx++;
            }

            public override string ToString()
            {
                return _number.ToString();
            }

            public void EventHandler()
            {
                Console.WriteLine("Observer {0} was called by subject", this);
            }
        }

        public static void Main(string[] args)
        {
            Subject s = new Subject();
            Observer o1 = new Observer(s);
            Observer o2 = new Observer(s);
            s.DoSomething();
            Console.ReadLine();
        }
    }
}

Strategy

Il pattern Strategy permette di definire una famiglia di algoritmi, di incapsularli e renderli intercambiabili fra loro. Questo pattern consente agli algoritmi di variare in modo indipendente rispetto al loro contesto di utilizzo, fornendo un basso accoppiamento tra le classi partecipanti del pattern e una alta coesione funzionale delle diverse strategie di implementazione.

Il pattern Strategy fornisce di fatto un meccanismo di configurazione per una determinata classe, utilizzando un comportamento specifico scelto fra tanti e “iniettato” nel momento dell’utilizzo. L’operazione di iniezione può avvenire secondo diverse modalità, a seconda dei casi. La modalità più comune di iniezione prevede l’uso del costruttore della classe per selezionare l’algoritmo concreto da eseguire successivamente.

*

I partecipanti di questo pattern sono:

  • Strategy (SortAlgorithm)
    Dichiara l’interfaccia di riferimento per tutti gli algoritmi concreti.

  • ConcreteStrategy (QuickSort, BubbleSort e MergeSort)
    Implementa un particolare algoritmo utilizzando l’interfaccia definita da Strategy.

  • Context (Context)
    Carica un oggetto ConcreteStrategy e utilizza un riferimento a Strategy per eseguire l’algoritmo concreto. Definisce l’interfaccia per accedere ai membri dell’algoritmo caricato.

L’esempio proposto si riferisce all’utilizzo di un algoritmo per ordinare un insieme di numeri interi. Dal momento che l’ordinamento può essere fatto in svariati modi, esistono diverse versioni parallele dello stesso algoritmo. Incapsulando ciascuna di queste implementazioni in un oggetto specifico, è possibile renderle tra loro intercambiabili senza impattare sul codice che effettivamente utilizza l’algoritmo e sul risultato dell’operazione. La classe astratta SortAlgorithm rappresenta il tipo base da cui ogni implementazione dell’algoritmo deriva. Nell’esempio sono inclusi tre tipi distinti di ordinamento corrispondenti ad altrettante classi concrete: QuickSort, BubbleSort e MergeSort. La classe Context carica l’istanza di un algoritmo passata tramite il costruttore. Il metodo SortArray(int[]) di Context internamente utilizza il metodo Sort(int) di SortAlgorithm, implementato in modo particolare da ciascuna delle classi derivate. In questo modo il client (classe Program) utilizza unicamente Context per eseguire gli ordinamenti e qualsiasi modifica degli algoritmi e delle classi relative non impatta su di esso.

*
using System;

namespace DesignPatterns.Strategy
{
    public abstract class SortAlgorithm
    {
        public abstract int[] Sort(int[] array);
    }

    public class QuickSort : SortAlgorithm
    {
        public override int[] Sort(int[] array)
        {
            Array.Sort<int>(array);
            return array;
        }
    }

    public class BubbleSort : SortAlgorithm
    {
        public override int[] Sort(int[] array)
        {
            throw new NotImplementedException();
        }
    }

    public class MergeSort : SortAlgorithm
    {
        public override int[] Sort(int[] array)
        {
            throw new NotImplementedException();
        }
    }

    public class Context
    {
        private SortAlgorithm _algorithm;

        public Context(SortAlgorithm algorithm)
        {
            _algorithm = algorithm;
        }

        public int[] SortArray(int[] array)
        {
            return _algorithm.Sort(array);
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            int[] array = { 21, 10, 71, 18, 8, 5, 20, 1, 67 };

            Context ctx = new Context(new QuickSort());
            array = ctx.SortArray(array);

            foreach (int i in array)
                Console.WriteLine(i.ToString());

            Console.ReadLine();
        }
    }
}
using System;

namespace DesignPatterns.Strategy
{
    public abstract class SortAlgorithm
    {
        public abstract int[] Sort(int[] array);
    }

    public class QuickSort : SortAlgorithm
    {
        public override int[] Sort(int[] array)
        {
            Array.Sort<int>(array);
            return array;
        }
    }

    public class BubbleSort : SortAlgorithm
    {
        public override int[] Sort(int[] array)
        {
            throw new NotImplementedException();
        }
    }

    public class MergeSort : SortAlgorithm
    {
        public override int[] Sort(int[] array)
        {
            throw new NotImplementedException();
        }
    }

    public class Context
    {
        private SortAlgorithm _algorithm;

        public Context(SortAlgorithm algorithm)
        {
            _algorithm = algorithm;
        }

        public int[] SortArray(int[] array)
        {
            return _algorithm.Sort(array);
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            int[] array = { 21, 10, 71, 18, 8, 5, 20, 1, 67 };

            Context ctx = new Context(new QuickSort());
            array = ctx.SortArray(array);

            foreach (int i in array)
                Console.WriteLine(i.ToString());

            Console.ReadLine();
        }
    }
}

Template Method

Il pattern Template Method permette di definire la struttura di una algoritmo all’interno di un metodo di una superclasse (detto appunto metodo template), delegando alcuni passi alle classi derivate. Questo pattern lascia che le classi derivate possano definire in modo particolare alcuni passi dell’algoritmo senza dover implementare ogni volta da zero la struttura dell’algoritmo stesso.

Il pattern Template Method fornisce un meccanismo molto efficace di riuso del codice. I metodi template infatti richiamano per lo più una serie di metodi astratti che devono essere implementati in modo particolare nelle classi derivate. Questo fa sì che nelle classi derivate non debba essere fornita ogni volta l’implementazione di tutto l’algoritmo, ma solamente delle parti salienti, limitando in questo modo le duplicazioni e migliorando la leggibilità e la manutenibilità del codice. Talvolta, invece dei metodi astratti, nella superclasse può risultare comodo utilizzare metodi virtuali per rappresentare i diversi passi dell’algoritmo. In questo caso la classe base fornisce per ciascun passo dell’algoritmo un’implementazione predefinita e la personalizzazione nelle classi derivate può essere omessa qualora non risulti necessaria.

*

I partecipanti di questo pattern sono:

  • AbstractClass (AlgorithmBase)
    Definisce i vari metodi astratti che rappresentano i diversi passi di un determinato algoritmo. Include un metodo generale che definisce la struttura dell’algoritmo e ingloba le chiamate ai vari metodi astratti.

  • ConcreteClass (MyAlgorithm)
    Fornisce un’implementazione concreta dei vari metodi astratti definiti in AbstractClass.

Nell’esempio proposto la funzione ExecuteAlgorithm() della classe AlgorithmBase è un metodo template. Internamente esso richiama tre metodi protetti e astratti corrispondenti ai vari passi dell’algoritmo da eseguire. Dal momento che AlgorithmBase è un classe astratta, i suoi membri astratti devono essere obbligatoriamente implementati nelle classi derivate (classe MyAlgorithm). Peraltro l’implementazione presente in MyAlgorithm non va ad interessare l’interfaccia pubblica definita nel tipo base, dal momento che i vari metodi astratti sono visibili unicamente nelle classi derivate in quanto protetti. In questo modo è possibile variare l’algoritmo per ogni diversa specializzazione di AlgorithmBase semplicemente ridefinendo di volta in volta i vari passi dell’algoritmo, senza peraltro modificare la struttura del tipo base e il modo in cui esso viene utilizzato nell’ambito del codice del client (classe Program).

*
using System;

namespace DesignPatterns.TemplateMethod
{
    public abstract class AlgorithmBase
    {
        public void ExecuteAlgorithm()
        {
            ExecuteStepOne();
            ExecuteStepTwo();
            ExecuteStepThree();
        }

        protected abstract void ExecuteStepOne();
        protected abstract void ExecuteStepTwo();
        protected abstract void ExecuteStepThree();
    }

    public class MyAlgorithm : AlgorithmBase
    {
        protected override void ExecuteStepOne()
        {
            Console.WriteLine("Step One");
        }

        protected override void ExecuteStepTwo()
        {
            Console.WriteLine("Step Two");
        }

        protected override void ExecuteStepThree()
        {
            Console.WriteLine("Step Three");
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            new MyAlgorithm().ExecuteAlgorithm();
            Console.ReadLine();
        }
    }
}

Conclusioni

In questo e nei precedenti tre articoli abbiamo parlato abbondantemente del cluster dei pattern GoF, proponendo in totale una dozzina di esempi concreti di implementazione, relativi per lo più ai design pattern di maggior utilizzo.

Questi articoli peraltro non hanno la pretesa di fornire una trattazione esaustiva dell’argomento. Per ulteriori approfondimenti si rimanda direttamente al libro di Gamma, Helm, Johnson e Vlissides (la famosa Gang of Four). Il loro libro non è un manuale sui design pattern semplicemente da leggere, ma soprattutto una guida di riferimento da sfogliare quotidianamente e da tenere sempre a portata di mano sulla propria scrivania.

Riferimenti

  1. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides – Design Patterns: Elements of Reusable Object-Oriented Software – Addison Wesley, 1995.

  2. Riccardo Golia – Introduzione ai design pattern – MSDN Italia, Novembre 2006.

  3. Riccardo Golia – Design pattern per esempi: i GoF creazionali – MSDN Italia, Dicembre 2006.

  4. Riccardo Golia – Design pattern per esempi: i GoF strutturali – MSDN Italia, Gennaio 2007.


© 2009 Microsoft Corporation. Tutti i diritti riservati. Condizioni per l'utilizzo | Marchi | Informativa sulla privacy
Page view tracker