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 cr...
Design pattern per esempi: i GoF creazionali

Di Riccardo Golia - Microsoft MVP

Nell’articolo dedicato all’introduzione ai design pattern, si è visto come l’esigenza di progettare applicazioni object-oriented che si sappiano adattare ai cambiamenti nel tempo in modo efficace e flessibile porta gli architetti a cercare spesso soluzioni di disegno che contemplano l’utilizzo dei design pattern. In generale i design pattern concorrono a limitare l’accoppiamento tra i tipi, incentivando la riusabilità e l’estendibilità della struttura ad oggetti su cui si basa l’applicazione.

In questa pagina

I design pattern creazionali I design pattern creazionali
Abstract Factory Abstract Factory
Builder Builder
Factory Method Factory Method
Singleton Singleton
Conclusioni Conclusioni
Riferimenti Riferimenti

Dal momento che i design pattern possono essere utilizzati sia nella fase di disegno di un nuovo sistema, sia per rivedere un sistema esistente applicando il refactoring al codice, in entrambe le situazioni è sempre importante fare attenzione a come essi vengono applicati. L’approccio corretto da parte dell’architetto in generale deve essere orientato alla ricerca delle soluzioni più semplici tra quelle che effettivamente risolvono le problematiche incontrate. Questo modo di procedere non significa banalizzare il processo di scomposizione dell’applicazione in oggetti, né tanto meno sminuire l’importanza ricoperta dai pattern, implica semmai la necessità di non sovradimensionare inutilmente il sistema in fase di progettazione, introducendo una complessità che si può rivelare in seguito assai dannosa in termini di manutenibilità.

La scelta se applicare o meno uno o più pattern deriva spesso dalla conoscenza che un architetto o uno sviluppatore ha di essi e dall’esperienza accumulata nel loro uso. Conoscere i pattern sulla carta non basta per poter operare scelte di utilizzo consapevoli, spesso un esempio di implementazione può essere molto più chiarificatore di tante parole astratte.

In questo e nei prossimi due articoli vengono proposti dodici esempi riguardanti i design pattern GoF (Gang of Four) più utilizzati, suddivisi in base alla loro categoria di appartenenza (creazionali, strutturali e comportamentali). 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#.

In questo articolo vengono trattati i design pattern GoF creazionali e viene proposta una serie di esempi relativi ai pattern principali.

I design pattern creazionali

I pattern creazionali forniscono un’astrazione del processo di creazione delle istanze delle classi, favorendo l’indipendenza del sistema dalle modalità di creazione e dai tipi concreti effettivamente generati.

Ci sono due aspetti che caratterizzano i pattern appartenenti a questa categoria:

  • la capacità di mascherare al client la conoscenza degli oggetti concreti creati, sfruttando tipi astratti per definire le interfacce di riferimento;

  • la capacità di nascondere le modalità di creazione all’utilizzatore dell’istanza.

Queste due caratteristiche conferiscono una notevole flessibilità al processo di creazione, dal momento che ciò che viene creato in generale risulta essere disaccoppiato dal contesto di utilizzo. Infatti solo l’oggetto creatore conosce il tipo effettivo dell’istanza e ciò che viene esternamente reso pubblico è unicamente l’interfaccia di riferimento.

Abstract Factory

L’Abstract Factory (detto anche Kit) è un pattern creazionale che ha lo scopo di fornire un’interfaccia per la creazione di famiglie di oggetti tra loro correlati o dipendenti limitando l’accoppiamento derivante dall’uso diretto delle classi concrete. L’applicazione di questo pattern si rivela assai utile quando si vuole rendere un sistema indipendente dalle modalità di creazione, composizione e rappresentazione degli oggetti costituenti, rendendo note unicamente le interfacce e non le implementazioni concrete. Questo consente di rendere tra loro interscambiabili le diverse implementazioni che soddisfano una determinata interfaccia, senza che il contesto d’uso dell’istanza debba essere modificato al variare dell’implementazione scelta.

Figura 1

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

  • AbstractFactory (IShapeFactory)
    Definisce l’interfaccia di riferimento per gli oggetti che creano le istanze.

  • ConcreteFactory (MyShapeFactory)
    Implementa in modo concreto l’interfaccia definita da AbstractFactory e crea effettivamente una tipologia specifica di oggetti appartenenti ad una famiglia.

  • AbstractProduct  (Circle e Rectangle)
    Definisce l’interfaccia di riferimento per una famiglia di oggetti da creare tramite il factory corrispondente.

  • ConcreteProduct (MyCircle e MyRectangle)
    Implementa in modo concreto l’oggetto appartenente alla famiglia per cui vale l’interfaccia AbstractProduct e che viene creato dall’oggetto factory corrispondente.

  • Client (Program)
    Utilizza unicamente le classi astratte del factory e dell’oggetto da creare, senza conoscerne gli aspetti implementativi. L’annullamento dell’accoppiamento tra il client e gli oggetti concreti è ottenuto tramite l’inversione delle dipendenze, uno dei principi base dell’Object Oriented Design (OOD).

L’esempio proposto per questo pattern include innanzitutto un’interfaccia IShape (forma) che dichiara un metodo Print che deve essere presente in tutte le istanze che la implementano. In particolare queste istanze sono rappresentate dagli oggetti di tipo MyCircle e MyRectangle, che implementano l’interfaccia IShape in modo indiretto tramite le classi base Circle e Rectangle rispettivamente. L’oggetto MyShapeFactory implementa l’interfaccia IShapeFactory e nei metodi CreateCircle e CreateRectangle crea tramite il costruttore di default le istanze di tipo MyCircle e MyRectangle. Nel client (Program) non viene fatto alcun riferimento alle classi MyCircle e MyRectangle. Dal momento che il client non conosce in modo diretto i tipi effettivamente creati dal factory, qualsiasi dipendenza di Program dai tipi concreti effettivamente utilizzati al suo interno viene eliminata.

Figura 2
using System;

namespace DesignPatterns.AbstractFactory
{
    public interface IShape 
    {
        void Print();
    }

    public class Rectangle : IShape
    {
        public virtual void Print()
        {
            Console.WriteLine("Rectangle");
        }
    }

    public class Circle : IShape
    {
        public virtual void Print()
        {
            Console.WriteLine("Circle");
        }
    }

    public interface IShapeFactory
    {
        Rectangle CreateRectangle();
        Circle CreateCircle();
    }

    public class MyRectangle : Rectangle
    {
        public override void Print()
        {
            Console.WriteLine("MyRectangle");
        }
    }

    public class MyCircle : Circle
    {
        public override void Print()
        {
            Console.WriteLine("MyCircle");
        }
    }

    public class MyShapeFactory : IShapeFactory
    {
        public Rectangle CreateRectangle()
        {
            return new MyRectangle();
        }

        public Circle CreateCircle()
        {
            return new MyCircle();
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            IShapeFactory fac = new MyShapeFactory();
            Circle c = fac.CreateCircle();
            Rectangle r = fac.CreateRectangle();
            c.Print();
            r.Print();
            Console.ReadLine();
        }
    }
}

Builder

Il pattern Builder consente di dividere la costruzione di un oggetto complesso e composito dalla sua rappresentazione, in maniera tale che lo stesso processo di costruzione possa essere utilizzato per creare rappresentazioni diverse. L’applicazione di questo pattern si rivela assai indicata quando l’algoritmo di creazione dell’oggetto composito deve essere mantenuto distinto dalle parti costituenti e dal modo con cui esse sono unite insieme a formare un tutt’uno, consentendo un migliore controllo del processo di costruzione e isolando da tutto il resto il codice di assemblaggio.

Figura 3

I partecipanti di questo pattern sono:

  • Builder
    Rappresenta l’interfaccia di riferimento (generalmente astratta) per la creazione delle parti costituenti l’oggetto da costruire.

  • ConcreteBuilder (Wheel, Engine e Chassis)
    Genera e costruisce ogni singola parte concreta dell’oggetto composito tramite l’implementazione di Builder. Definisce un metodo di costruzione BuildPart e uno di accesso al risultato della costruzione GetResult.

  • Director (CarBuilder)
    Assembla l’oggetto utilizzando l’interfaccia Builder. Infatti il client (Program) istanzia questo oggetto configurandolo in maniera tale da farlo operare con l’oggetto Builder desiderato.

  • Product (Car)
    Rappresenta l’oggetto composito che è il risultato dell’operazione di costruzione e assemblaggio.

Quello proposto è un esempio molto semplificato di applicazione del pattern in questione. Si tratta della costruzione di un oggetto di tipo Car che comprende quattro proprietà, ovvero un array composto da 4 elementi di tipo Wheel (ruota), un Engine (motore) e un Chassis (telaio). Ciascuna di queste parti implementa in modo particolare il metodo ToString di System.Object (lo possiamo considerare come l’equivalente del metodo GetResult nella rappresentazione generale) e definisce un costruttore, accettando eventuali parametri utili alla creazione delle singole istanze (lo possiamo pensare come l’equivalente del metodo BuildPart nella rappresentazione generale). L’oggetto che è incaricato di costruire l’assemblato è la classe CarBuilder che, tramite il metodo statico CreateCar, accetta i parametri di costruzione validi per le diverse parti e chiama i costruttori per la generazione delle istanze. Il metodo ToString della classe Car richiama internamente i metodi ToString delle parti costituenti per ottenere una rappresentazione completa dell’oggetto.

Figura 4
using System;

namespace DesignPatterns.Builder
{
    public class Car
    {
        private Wheel[] _wheels;
        private Engine _engine;
        private Chassis _chassis;

        public Wheel Wheel1
        {
            set { _wheels[0] = value; }
            get { return _wheels[0]; }
        }

        public Wheel Wheel2
        {
            set { _wheels[1] = value; }
            get { return _wheels[1]; }
        }

        public Wheel Wheel3
        {
            set { _wheels[2] = value; }
            get { return _wheels[2]; }
        }

        public Wheel Wheel4
        {
            set { _wheels[3] = value; }
            get { return _wheels[3]; }
        }

        public Engine Engine
        {
            set { _engine = value; }
            get { return _engine; }
        }

        public Chassis Chassis
        {
            set { _chassis = value; }
            get { return _chassis; }
        }

        public Car()
        {
          _wheels = new Wheel[4];
        }

        public override string ToString()
        {
            return _wheels[0].ToString() + " / " +
                   _wheels[1].ToString() + " / " +
                   _wheels[2].ToString() + " / " +
                   _wheels[3].ToString() + " / " +
                   _engine.ToString() + " / " + _chassis.ToString();
        }
    }

    public class Wheel
    {
        private double _size;

        public Wheel(double size) { _size = size; }

        public override string ToString()
        {
            return "Wheel " + _size.ToString();
        }
    }

    public class Engine
    {
        private double _power;

        public Engine(double power) { _power = power; }

        public override string ToString()
        {
            return "Engine " + _power.ToString();
        }
    }

    public class Chassis
    {
        public Chassis() {}

        public override string ToString()
        {
            return "Chassis";
        }
    }

    public class CarBuilder
    {
        public static Car CreateCar(double wheelSize, double enginePower)
        {
            Car c = new Car();
            c.Wheel1 = new Wheel(wheelSize);
            c.Wheel2 = new Wheel(wheelSize);
            c.Wheel3 = new Wheel(wheelSize);
            c.Wheel4 = new Wheel(wheelSize);
            c.Engine = new Engine(enginePower);
            c.Chassis = new Chassis();
            return c;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(CarBuilder.CreateCar(180, 110).ToString());
            Console.ReadLine();
        }
    }
}

Factory Method

Il pattern Factory Method definisce un’interfaccia di classe per la creazione di un oggetto, lasciando ai tipi derivati la decisione su quale oggetto debba essere effettivamente istanziato. Il pattern può rivelarsi utile quando una classe non è in grado di conoscere a priori il tipo di oggetti da creare piuttosto che quando si vuole delegare la creazione di un oggetto alle sottoclassi. L’applicazione del pattern consente di eliminare le dipendenze dai tipi concreti utilizzati.

Figura 5

I partecipanti di questo pattern sono:

  • Product (Shape)
    Definisce l’interfaccia base per gli oggetti da creare.

  • ConcreteProduct (Circle e Rectangle)
    Rappresenta una implementazione concreta di Product.

  • Creator (ShapeCreator)
    Dichiara il metodo factory che restituisce un oggetto di tipo Product.

  • ConcreteCreator
    Definisce il metodo factory effettivo per la creazione di un’istanza particolare di tipo Product.

Nell’esempio riportato di seguito è presente unicamente la classe concreta ShapeCreator. Nella sua interfaccia essa include il metodo CreateShape(ShapeType), che, invece di essere virtuale o astratto per essere personalizzato nelle classi derivate, è direttamente implementato e accetta un parametro che consente di selezionare il tipo dell’istanza da creare. In base al valore del parametro, il metodo ritorna un’istanza di Circle o di Rectangle, a seconda dei casi. Il metodo factory ritorna sempre e comunque un oggetto di tipo Shape, classe astratta base per Circle e Rectangle.

Figura 6
using System;

namespace DesignPatterns.FactoryMethod
{
    public enum ShapeType
    {
      Rectangle = 1,
      Circle = 2
    }

    public abstract class Shape
    {
        public abstract void Draw();
    }

    public class Rectangle : Shape
    {
        public override void Draw()
        {
            Console.WriteLine("Rectangle");
        }
    }

    public class Circle : Shape
    {
        public override void Draw()
        {
            Console.WriteLine("Circle");
        }
    }

    public class ShapeCreator
    {
        private static ShapeCreator _instance = new ShapeCreator();

        public static ShapeCreator Instance
        {
            get { return _instance; }
        }

        public Shape CreateShape(ShapeType type)
        {
            switch (type)
            {
                case ShapeType.Rectangle:
                    return new Rectangle();
                case ShapeType.Circle:
                    return new Circle();
                default:
                    throw new ArgumentException("type");
            }
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            Shape[] shapes =
            new Shape[] { ShapeCreator.Instance.CreateShape(ShapeType.Circle), 
                          ShapeCreator.Instance.CreateShape(ShapeType.Rectangle) };
            foreach (Shape s in shapes)
              s.Draw();
            Console.ReadLine();
        }
    }
}

Singleton

Lo scopo del pattern Singleton è quello di assicurare che per una determinata classe esista un’unica istanza attiva, fornendo un entry-point globale all’istanza stessa. Questo pattern si può rivelare utile nel caso in cui si abbia la necessità di centralizzare informazioni e comportamenti in un’unica entità condivisa da tutti i suoi utilizzatori. La soluzione che più si adatta a risolvere la questione associata al pattern (unicità dell’istanza) consiste nell’associare alla classe stessa la responsabilità di creare le proprie istanze. In questo modo è la classe stessa che può assicurare che nessun’altra istanza possa essere creata, intercettando e gestendo in modo centralizzato le richieste di creazione di nuove istanze.

L’unico partecipante del pattern pertanto è:

  • Singleton (One, Two e Three)
    Definisce un membro per accedere all’unica istanza esistente, generalmente creata internamente alla classe stessa.

Figura 7

L’esempio proposto mostra tre casistiche diverse di applicazione del pattern. La classe One prevede l’inizializzazione statica dell’istanza. La proprietà Instance ritorna l’oggetto equivalente di tipo One statico e privato. La classe Two prevede l’inizializzazione dinamica su richiesta tramite il controllo del riferimento all’istanza. La proprietà Instance ritorna anche in questo caso l’oggetto equivalente di tipo Two statico e privato. La classe Three effettua un doppio controllo sul riferimento all’istanza, dentro e fuori ad un blocco a mutua esclusione e in base ad esso attiva l’istanza. Ancora una volta la proprietà Instance ritorna l’oggetto equivalente di tipo Three statico e privato. Se i primi due casi non sono thread-safe, il terzo lo è (nell’ambito di uno stesso appdomain). La presenza del blocco di mutua esclusione garantisce che la creazione dell’istanza sia effettivamente eseguita una volta sola, anche in un contesto multi-thread.

Figura 8
using System;
using System.Threading;

namespace DesignPatterns.Singleton
{
    public sealed class One
    {
        private static One _instance = new One();
        private One() {}

        public static One Instance
        {
            get { return _instance; }
        }

        public void DoSomething()
        {
            Console.WriteLine("One");
        }
    } // One

    public sealed class Two
    {
        private static Two _instance;
        private Two() {}

        public static Two Instance
        {
            get
            {
                if (_instance == null)
                    _instance = new Two();
                return _instance;
            }
        }

        public void DoSomething()
        {
            Console.WriteLine("Two");
        }
    } // Two

    public sealed class Three
    {
        private static Three _instance;
        private static object _syncLock = new object();
        private Three() {}

        public static Three Instance
        {
            get
            {
                if (_instance == null)
                    lock (_syncLock)
                    {
                        if (_instance == null)
                            _instance = new Three();
                    } // lock
                return _instance;
            }
        }

        public void DoSomething()
        {
            Console.WriteLine("Three");
        }
    } // Three

    public class Program
    {
        static void Main(string[] args)
        {
            One.Instance.DoSomething();
            Two.Instance.DoSomething();
            Three.Instance.DoSomething();
            Console.ReadLine();
        }
    }
}

Conclusioni

Dal momento che la soluzione associata ad un design pattern viene espressa in un modo sufficientemente generale da lasciare numerosi gradi di libertà, l’implementazione non necessariamente segue lo schema di base. Anche negli esempi presentati nel corso dell’articolo questo aspetto è evidente: non sempre esiste una corrispondenza esatta tra la forma generale e le scelte implementative che conducono alla soluzione della problematica concreta di sviluppo.

Questo deve far riflettere sul reale ruolo che i pattern ricoprono e sul modo con cui devono essere utilizzati. Essi infatti forniscono di volta in volta una soluzione di massima ad un problema specifico di disegno, ma la scelta finale su come mettere in pratica questa soluzione dipende sempre dal buon senso e dalla consapevolezza di chi applica concretamente il pattern.

In questo articolo abbiamo visto quattro esempi di implementazione relativi ai design pattern GoF di tipo creazionale. Nei prossimi due articoli tratteremo i pattern strutturali e comportamentali.

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.


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