Creational Patterns

Abstract Factory

Il pattern abstract factory prevede un meccanismo (ad esempio un metodo) che generi un insieme di classi che sono tra loro interdipendenti, ma la cui classe concreta è nota solo in runtime.

L’esempio riportato anche da E. Gamma è quello dell’interfaccia grafica ed, in particolare, dell’istanziazione dell’interfaccia in base, ad esempio, al sistema operativo in cui l’applicazione è in esecuzione. Nell’esempio sotto utilizziamo sempre il problema di istanziazione di elementi dell’interfaccia grafica, ma risolviamo il problema di istanziare un elemento chiaro di giorno ed uno scuro di notte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Main {
    public static Object[] createButtonAndText() {
        Object[] gui = new Object[2];
        if (LocalDateTime.now().getHour() > 17 || LocalDateTime.now().getHour() < 9) {
            gui[0] = new DarkButton();
            gui[1] = new DarkText();
        } else {
            gui[0] = new LightButton(); 
            gui[1] = new LightText();
        }
        return gui;
    }
    public static void main(String[] args) {
       Object[] gui = createButtonAndText(); // abstract factory method
       Button b = (Button)gui[0]; // DarkButton if night, LightButton otherwise
       Text t = (Text)gui[1]; // DarkText if night, LightText otherwise
    }
}

Factory methods

Il factory method pattern prevede un metodo che viene utilizzato per istanziare un oggetto utilizzando la sottoclasse opportuna in base al runtime.

Consideriamo il seguente esempio in linguaggio Java

String[] types = {"student", "professor", "ATA", "student"};
Person[] persons = new Person[types.length];
for (int i = 0; i < persons.length; i++) {
    persons[i] = Person.createPersonFromString(types[i]);
}

il metodo statico createPersonFromString della classe Person è il nostro factory method. La sua implementazione terrà conto dell’informazione ottenuta in runtime (in questo una stringa) per decidere quale classe istanziare e ritornare, a patto che sia assegnabile ad un referenza di tipo Person (mediante cast implicito). Una possibile implementazione del metodo è fornita qui sotto.

public static Person createPersonFromString(String type) {
    if (type.toLowerCase().equals("professor")) {
        return new Professor();
    }
    if (type.toLowerCase().equals("student")) {
        return new Student();
    }
    return new Person();
}

Singleton

Il singleton pattern prevede che una classe possa avere al massimo un’istanza. Il tentativo di successive istanziazioni, deve ritornare l’unica istanza disponibile (oppure generare un errore, se appropriato).

L’implementazione in Java del pattern singleton richiede, per prima cosa, di impedire l’utilizzo del costruttore della classe, in questo modo l’unico modo per ottenere un’istanza è attraverso un factory method che sarà in grado di verificare che l’istanza creata sia unica.

public class SimpleSingleton {
    private static SimpleSingleton instance = null;
    public static SimpleSingleton factoryMethod() {
        if (instance == null) {
            instance = new SimpleSingleton();
        }
        return instance;
    }
    private SimpleSingleton() { }
}

Oltre ad aver reso private l’unico costruttore, la classe implementa un metodo static (perché static?) che restituisce un’istanza di SimpleSingleton. Il metodo verifica se una istanza è già stata creata, nel qual caso la restituisce, se l’istanza ancora non è stata creata la crea la prima volta e la assegna alla variabile private static di nome instance.

Attenzione

Affinché il codice funzioni, è necessario che factoryMethod sia un metodo della classe SimpleSingleton, se così non fosse, infatti, tale metodo non potrebbe accedere al costruttore private e di conseguenza non sarebbe in alcun modo possibile istanziare la classe.

Per verificare l’effettivo funzionamento del factory method possiamo invocarlo due volte assegnando il risultato a due variabili diverse, in seguito confrontando i riferimenti utilizzando l’operatore == potremmo verificare che i riferimenti “puntino” allo stesso oggetto.

public class SimpleSingletonMain {
    public static void main(String[] args) {
        // SimpleSingleton ss = new SimpleSingleton(); // Errore
        SimpleSingleton simple1 = SimpleSingleton.factoryMethod();
        SimpleSingleton simple2 = SimpleSingleton.factoryMethod();
        System.out.println(simple1 == simple2); // True if singleton
    }
}

Builder

Il builder pattern si applica quando la costruzione di un oggetto risulta complessa e di non facile scrittura utilizzando il solo costruttore della classe.

Supponiamo di dover creare un personaggio in un gioco di ruolo, ogni personaggio ha diversi caratteristiche: aspetto fisico, armamento, inventario, … La creazione di ognuna di queste caratteristiche può richiedere una notevole quantità di parametri. Ad esempio per creare l’armamento del personaggio dovremmo poter specificare: arma, elmo, corazza, scudo, … A sua volta ognuna di queste caratteristiche può richiedere una creazione complessa.

Vediamo in che modo si potrebbe scrivere in Java questa situazione, prima senza utilizzare il builder pattern. La classe Character che rappresenta il nostro personaggio potrebbe essere simile alla seguente

public class Character {
    private String name;
    private int lifePoints;
    private String skill;
    private Weapon weapon;
    private Armor armor;

    public Character(String name, int lifePoints, String skill, Weapon weapon, Armor armor) {
        this.name = name;
        this.lifePoints = lifePoints;
        this.skill = skill;
        this.weapon = weapon;
        this.armor = armor;
    }
}

Per creare un oggetto il codice assomiglierà al seguente

public class Main {
    public static void main(String[] args) {
        Character wizard = new Character(
            "Wizard Gandalf",
            100,
            "Magic",
            new Weapon(),
            new Armor()
        );
        System.out.println(wizard);
    }
}

Anche se non eccessivamente complicato in apparenza, questo codice è fortemente incompleto, infatti crea un’arma ed un’armatura vuota, cosa che è raramente ciò che si vuole fare. In altre parole, il codice reale sarebbe molto più complesso di quello mostrato sopra.

Vediamo ora come il builder pattern risolve questo problema. Per prima cosa abbiamo bisogno di una classe CharacterBuilder la quale conterrà i metodi necessari per impostare i vari valori associati al personaggio.

public class CharacterBuilder {
    private String name;
    private int lifePoints;
    // .. 
    public CharacterBuilder setName(String name) {
        this.name = name;
        return this;
    }
    public CharacterBuilder setLifePoints(int lifePoints) {
        this.lifePoints = lifePoints;
        return this;
    }
    // ...
    public Character build() {
        return new Character(name, lifePoints, skill, weapon, armor);
    }
}

Il codice riportato qui è parziale per questioni di spazio, ma sufficiente a comprendere la logica della classe builder. Vediamo quindi quali sono le caratteristiche di questa classe.

  • Contiene tutti gli stessi campi che contiene la classe che si vuole costruire (Character nell’esempio). Questi campi verranno utilizzati quando verrà effettivamente istanziata la classe Character.
  • Ogni campo ha un metodo set associato il quale metodo restituisce l’istanza stessa del builder (i return this; presenti nei vari set); vedremo sotto perché questo è comodo.
  • Un metodo build restituisce l’oggetto che si vuole creare (Character) nel nostro esempio occupandosi di invocare i metodi adeguati ad esempio il costruttore (come nell’esempio sopra) oppure un factory method.

Vediamo ora come si utilizza la classe CharacterFactory per creare un oggetto di tipo Character.

public class Main {
    public static void main(String[] args) {
        CharacterBuilder builder = new CharacterBuilder();
        builder
            .setName("Warrior Aragorn")
            .setLifePoints(100)
            .setSkill("Sword");
        Character warrior = builder.build();
    }
}

Anche se il numero di righe è paragonabile (se non superiore) all’utilizzo diretto del costruttore, l’utilizzo di un builder risulta preferibile per diversi motivi:

  • il codice risulta più facile da gestire e da leggere;
  • se si cambia la classe Character basta cambiare la classe CharacterBuilder;
  • l’oggetto viene creato solo una volta dal metodo build che può verificare che tutti i campi siano coerenti.
Osserva

Si potrebbe pensare che la creazione di una nuova classe CharacterBuilder rappresenti uno “spreco” di codice in quanto i vari set potevano essere creati nella classe Character. Tuttavia, come si vede dall’utilizzo sopra, il codice per creare un personaggio utilizzando CharacterBuilder risulta molto più compatto e più facile da comprendere.

Inoltre l’oggetto Character viene creato dalla chiamata a build che può verificare che tutto sia corretto e che si possa procedere alla creazione di un Character valido. Utilizzando metodi set nella classe Character sarebbe necessario, prima creare un personaggio in modo “parziale” e solo dopo riempire le sue caratteristiche utilizzando i vari metodi set.

  • Michele Schimd © 2024
  • Ultimo aggiornamento: 17/02/2024
  • Materiale di studio e di esercizio per gli alunni dello Zuccante.

Creative Commons License