sabato 13 febbraio 2021

Creational Patterns

Creational patterns

Introduzione

I Creational patterns sono un sottoinsieme dei design patterns che si prefigge di consentire il cambio dei tipi concreti utilizzati nelle classi senza bisogno di dover riscriverne il codice. 
E' possibile che i tipi effettivi da usare siano stabiliti a design time o dinamicamente a run-time, e in quest'ultimo caso non sarà necessaria una ricompilazione.
Quando una classe A crea imperativamente istanze di classi di tipo B, ad esempio per invocarne dei metodi o perché alcuni membri di A sono proprio di tipo B, si crea una dipendenza molto forte di A nei confronti di B, e rende una successiva sostituzione di B con una classe diversa, ad esempio derivata da B, impossibile a meno di cambiare il codice dei metodi di A che creano le istanze di B.
Questa dipendenza palesemente viola il principio OCP (open/closed principle), che afferma che le entità dovrebbero essere aperte alle estensioni, ma chiuse alle modifiche, infatti in questo caso non c'è modo di consentire un'estensione da B a B' se non cambiando il codice di A.
Come se non bastasse, quando si creano gli unit test per i metodi di A, non è possibile in queste condizioni sostituire B con altre classi. Ad esempio potrebbe essere che B richiede l'accesso ad un database o ad un web service o altre risorse di sistema, e questo nell'ambito di uno unit test è impraticabile. 
Esamineremo diversi design patterns che si occupano quindi di evitare di creare questa dipendenza.

Supponiamo che la classe A abbia un metodo che necessita di creare un'istanza di B:
class A {     
   void metodo(){
        B b = new B ();
        ..
    }
}
Vedremo nei vari casi come viene eliminata la dipendenza di A verso B.

Factory Method

Definisce un'interfaccia per creare un oggetto, e lascia che le classi derivate implementino tale interfaccia per stabilire quale classe concreata utilizzare.  E' da notare che invece della classe B in questi casi si usi di solito usare delle interfacce, che dovranno essere esposte sia da B che dalle sue eventuali sostitute. In alternativa si può usare la classe B, magari astratta, e consentire la sostituzione con classi derivate da B.

In questo caso si avrebbe dunque:
abstract class A {     
   abstract B creator();

   void metodo(){
        B b = creator();
        ..
    }
}

La creazione di b è delegata al metodo creator(), che sarà ridefinito nelle classi concrete che deriveranno da A. 
Ci sono diverse varianti di questa tecnica:
  • E' possibile che la classe A non sia astratta e provveda invece un'implementazione di default per  il metodo "creator"
  • E' possibile che il metodo creator restituisca diversi tipi di oggetto. In questo caso prevederà un parametro che le indicherà il tipo di oggetto da creare.
  • E' possibile, per i linguaggi che ammettono i template, che il tipo restituito dal metodo sia specificato nell'invocazione del template
Questa tecnica è molto utile ma diviene complicata se la classe B (o una sua derivata) per essere costruita ha bisogno a sua volta di altre classi. Senza contare il fatto che se la classe A dipende da più classi B,C,D, ogni sua classe derivata dovrà implementare i metodi creator per ognuna di queste.
A tal fine può essere agevolante restituire delle istanze di default nei metodi della classe base A.

Prototype

Utilizza un prototipo di una classe per scegliere la classe utilizzata, e ne invoca un metodo di "copia" o "duplicazione" per crearne nuove istanze.
Questo può agevolare la creazione di classi complicate, e comunque fa si che il tipo concreto da utilizzare sia deciso a run-time.

class A {   
   B b;  
   public A(B prototipo){
    b = prototipo.clone();
   }

   void metodo(){
        //usa l'istanza di B creata nel costruttore come clone del prototipo
        ..operazioni su b
    }
}

Questo può agevolare la creazione di istanze di classe complicate, ad esempio derivanti a loro volta dalla composizione dinamica di altri oggetti, e comunque fa si che il tipo concreto da utilizzare sia deciso a run-time.
La classe/interfaccia B in questo caso deve anche prevedere un metodo clone() che consenta la duplicazione dell'istanza.
Una variante di questa tecnica è l'uso di un prototype manager: il client (la classe A) potrebbe richiedere i prototipi non come parametri costruttore ma ad un servizio specifico, il "manager dei prototipi", che fornirà un metodo che restituisce un prototipo diverso in base a dei parametri specificati e/o ad una configurazione
C'è da dire che implementare il metodo di clonazione può non essere agevole se l'oggetto da copiare ingloba altri oggetti complessi. E' anche vero che in quest'ottica ogni classe implicata fornirà un metodo per clonare se stessa, quindi la clonazione di una classe composta alla fine si riduce alla clonazione dei dati "propri", invocando i metodi di cloning delle classi utilizzate, che innesca una discesa gerarchica.

Singleton

Non è un vero e proprio pattern che crea gli oggetti in realtà, infatti prevede che nell'applicazione esista solo un'istanza di una certa classe. Questo può essere utile per esempio nel caso di una configurazione di sistema, di una classe che interfaccia l'accesso al database, o ad  un web service.
Fatta questa premessa, una tipica implementazione è avere un metodo getInstance che restituisce l'unica istanza della classe, creandola se eventualmente non esiste.
Se nell'esempio di prima B fosse una classe singleton, avremmo qualcosa del tipo:

class A {   
   void metodo(){
        B b = B.getInstance(); //chiede alla classe B la restituzione della sua unica istanza
        ..operazioni su b
    }
}

Varianti del singleton possono prevedere la richiesta di una tra più tipologie di singleton in base ad uno o più parametri del metodo getInstance, e la creazione di "registry" dei singleton, una sorta di lookup che associa ad ogni Type la sua unica istanza.
Personalmente trovo molto comodo il registry dei singleton, che rende anche possibile registrare con molta dinamicità i tipi concreti a quelli astratti, anche in punti diversi del programma, e questo sia in base a condizioni che si possono verificare in esecuzione, che ad un eventuale file di configurazione.
Usando un singleton registry il nostro esempio diverrebbe qualcosa del tipo:

class A {   
   void metodo(){
        B b = registry.getInstance(typeOf(B)) as B; 
        ..operazioni su b
    }
}

Abstract Factory

Fornisce un'interfaccia per creare oggetti di un certo tipo senza specificare esattamente la classe concreta. 
In questo modello l'Abstract Factory è una classe astratta che definisce dei metodi che restituiscono delle classi che implementano delle interfacce. Per essere utilizzata è necessario derivarne una classe concreta che stabilisce quali sono le classi concrete che saranno restituite.

class A {   
   Factory factory;
   public A(Factory f){
    factory=f;
    //in alternativa, se la factory è un singleton, il parametro f è superfluo e possiamo 
    // scrivere:
    factory  = Factory.getInstance();    
   }

   void metodo(){
        B b = f.createB(); 
        ..operazioni su b
   }
}


E' possibile, anche in questo caso, che i metodi di creazione accettino dei parametri per influenzare il tipo concreto da creare, o le sue caratteristiche.
Personalmente ho trovato utile utilizzare una variante dell'abstract factory simile al registry dei singleton, in cui consentire, non con la derivazione, ma mediante la compilazione di un lookup, di associare a dei tipi astratti dei tipi concreti. Ha molto senso per oggetti che non hanno bisogno di parametri per la creazione. In alternativa, devono prevedere dei metodi di inizializzazione comuni nella loro interfaccia.
Utilizzando un registry, singleton, come abstract factory, si avrebbe un codice del tipo:

class A {   
   Factory factory;
   public A(){
    factory  = Factory.getInstance();    
   }

   void metodo(){
        B b = f.create(typeOf(B)) as B; 
        ..operazioni su b
   }
}


Builder

Separa la costruzione di un oggetto dalla sua rappresentazione cosi che lo stesso processo di costruzione possa servire a creare oggetti aventi proprietà diverse. 
Un builder si rende molto utile quando l'oggetto che si intende creare appartiene ad una classe composita, ad esempio una classe A che utilizzi come proprie "componenti" le classi (interfacce) B,C,D. Nell'ipotesi di voler consentire di variare i tipi concreti di B,C,D, e far si che i dettagli di costruzione di A non influenzino il modo in cui il client crea una classe A, avremo una situazione del tipo:

class builderA {   
   Factory factory;
   public builderA (){
   }
   void createB(parametri per la creazione di B){        
   }
   void createC(parametri per la creazione di C){        
   }
   void createD(parametri per la creazione di D){        
   }
   A create(){
   }
}

Dal punto di vista dell'utilizzatore, per la creazione di A ci sarà una sequenza del tipo:

          ///per semplificare uso un costruttore invocato imperativamente
      builderA builder = new builderA();       
      builder.createB(...);
      builder.createC(...);
      builder.createD(...);
      A a = builderA.create();
       
In sostanza il builder conosce i dettagli di come costruire la classe a seconda delle componenti che il client vede come indipendenti tra loro. In alternatia, le varie chiamate createB, createC, createD possono anche riguardare l'impostazione di alcuni aspetti che dovrà avere l'istanza di A che sarà creata restituita dal metodo create() del builder.
Quello che si richiede al metodo create() è che l'oggetto restituito rispetti l'interfaccia A, e non necessariamente sia di un tipo specifico.

Lazy Instantiation

E' una tecnica che prevede, nella creazione di oggetti complessi e compositi, di ritardare la creazione di eventuali parti di essi quando siano effettivamente richieste nell'esecuzione.
Questo ne velocizza l'instanziazione, e può anche evitare del tutto la valorizzazione delle proprietà "lazy" quando in effetti non sono necessarie.
Supponiamo ad esempio che la classe A abbia una proprietà pubblica p che viene calcolata nella sua costruzione ed il cui calcolo sia dispendioso in termini di risorse/tempo necessari.

class A {   
   public P p;
   public A(){
    p = .... calcolo dispendioso;    
   }
}

Secondo questa tecnica è possibile ritardare il calcolo di p alla prima volta che sarà effettivamente necessario, e si può implementare ad esempio con una property get o simili, o in alternativa implementando una funzione che ne restituisca il valore o la calcoli se questo calcolo non è stato ancora effettuato:

class A {   
   private P p;
   P getP(){
    if (p==null) {
        p= calcolo dispendioso...
    }
    return p;
   }

   public A(){    
   }
}

Analogamente nel caso di una property get:

class A {   
   private P _p;
   public P p {get {
                if (_p==null) {
                    _p= calcolo dispendioso...
                }   
              return _p;
              }

   public A(){    
   }
}

Stesso concetto può applicarsi, anche se con una diversa implementazione, nel calcolo di matrici o stutture dati complesse, rendendo il calcolo ogni singola cella o elemento "lazy".Stesso concetto può applicarsi, anche se con una diversa implementazione, nel calcolo di matrici o stutture dati complesse, rendendo il calcolo ogni singola cella o elemento "lazy".

Conclusione

Abbiamo esaminato alcuni dei principali pattern di creazione utilizzabili nell'ambito di un'applicazione di livello enterprise. E' certamente possibile utilizzare combinazioni di tali pattern in base alle necessità, tenendo sempre a mente l'obiettivo di ottenere  un software il più possibile manutenibile ed efficiente.



 

Nessun commento:

Posta un commento

Refactoring: improving modularization

  Modularità In generale, per modularità si intende la misura in cui un sistema può essere decomposto e ricombinato. La modularizzazione è a...