Design Patterns su casi reali: Abstract Factory

La cosa più importante quando si parla di design patterns è riconoscere quando può essere utile applicarli per modellare qualcosa che si deve sviluppare. In questo post vediamo una possibile applicazione reale del design pattern Abstract Factory.
Supponiamo di avere un sistema che deve processare dei report che possono essere di due categorie: report relativi a transazioni in INPUT e report relativi a transazioni in OUTPUT. Per ciascuna categoria ci possono essere diversi tipi di report, ad esempio report relativi alle fatture, report relativi agli acquisti, ecc..
Ciascun report, a seconda della categoria e del tipo, ha una sua specifica modalità di processamento. I report arrivano sotto forma di una lista di stringhe, che rappresentano i nomi dei report stessi, letti da una determinata cartella nel file system. Nel nome stesso dei report sono indicate la sua categoria ed il suo tipo. Supponiamo che i nomi dei report siano nel formato seguente: <CAT>#<TYPE>#name.txt
Quindi, ad esempio:
IN_INV_001.txt : indica un report di categoria INPUT e della tipologia “invoice”
OUT_PUR_001.txt : indica un report di categoria OUTPUT e della tipologia “purchase”

Vediamo come utilizzare il pattern Abstract Factory per modellare un sistema di questo tipo, in modo da renderlo flessibile e scalabile.
Gli oggetti che ci servono sono:

  • Un’interfaccia che definisce il metodo per la creazione dei report e che deve essere implementata dalle factory concrete (IReportFactory)
  • Le due implementazioni che costituiscono le factory per le due categorie (INReportFactory e OUTReportFactory)
  • L’astrazione dei nostri oggetti report (AReport)
  • L’astrazione per i report di ciascuna categoria (AINReport e AOUTReport)
  • Le implementazioni concrete dei report per ciascuna categoria e ciascun tipo (INInvoiceReport, INPurchaseReport, OUTInvoiceReport, OUTPurchaseReport)
  • La classe “client” che utilizza i report e che ha necessita di istanziarli (Archive)
  • Un provider di factory, per disaccoppiare la loro istanziazione dall’oggetto utilizzatore (FactoryProvider)

Come risultato di questa analisi possiamo produrre il seguente Class Diagram UML:
Abstract Factory Design Pattern

Passiamo ora all’implementazione Java delle strutture che abbiamo identificato:

Partiamo dall’astrazione degli oggetti report, dove definiamo un membro di tipo String che conterrà il nome del file del report. Definiamo anche un costruttore che inizializza questo membro, che verrà richiamato dai costruttori delle sottoclassi concrete ed un metodo che definisce le operazioni di processamento comuni a tutti i report, indipendentemente dalla categoria o dal tipo.

public abstract class AReport {

    protected String name;

    protected AReport(String name) {
        this.name = name;
    }

    public void processReport() {
        System.out.println("Processing report: " + this.name);
    };
}

A questo punto passiamo a definire le astrazioni per le due categorie di report, INPUT ed OUTPUT, che erediteranno dalla precedente ed aggiungeranno nell’override del metodo processReport() le eventuali operazioni comuni a livello di categoria.

public abstract class AINReport extends AReport {

    protected AINReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {

        super.processReport();
        System.out.println("Performing IN Reports common stuff");

    }
}
public abstract class AOUTReport extends AReport {

    protected AOUTReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {
        super.processReport();
        System.out.println("Performing OUT Reports common stuff");
    }
}

Ora possiamo creare le implementazioni concrete dei nostri oggetti report, definendone una per ciascuna categoria e ciascun tipo.

public class INInvoiceReport extends AINReport {

    protected INInvoiceReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {
        super.processReport();
        System.out.println("Performing IN Reports Invoice specific stuff");
    }
}
public class INPurchaseReport extends AINReport {

    protected INPurchaseReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {
        super.processReport();
        System.out.println("Performing IN Reports Purchase specific stuff");
    }
}
public class OUTInvoiceReport extends AOUTReport {

    protected OUTInvoiceReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {
        super.processReport();
        System.out.println("Performing OUT Reports Invoice specific stuff");
    }
}
public class OUTPurchaseReport extends AOUTReport {

    protected OUTPurchaseReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {
        super.processReport();
        System.out.println("Performing OUT Reports Purchase specific stuff");
    }
}

Adesso che abbiamo definito il modello dati dei nostri report, passiamo alla definizione del pattern creazionale con cui verranno istanziati a seconda della categoria e del tipo richiesto. Iniziamo dall’interfaccia che definisce la factory ed il metodo per la creazione di un report che tutte le factory concrete delle varie categorie dovranno implementare.

public interface IReportFactory {

    public AReport createReport(String type, String name);

}

Definiamo quindi le due factory concrete per le due categorie INPUT ed OUTPUT, che si occuperanno dell’istanziazione dei report veri e propri, a seconda del tipo richiesto.

public class INReportFactory implements IReportFactory {

    @Override
    public AReport createReport(String type, String name) {
        AReport doc = null;
        switch(type) {
            case "INV":
                doc = new INInvoiceReport(name);
            break;
            case "PUR":
                doc = new INPurchaseReport(name);
            break;
            default:
                break;
        }
        return doc;
    }
}
public class OUTReportFactory implements IReportFactory {

    @Override
    public AReport createReport(String type, String name) {
        AReport doc = null;
        switch(type) {
            case "INV":
                doc = new OUTInvoiceReport(name);
            break;
            case "PUR":
                doc = new OUTPurchaseReport(name);
            break;
            default:
                break;
        }
        return doc;
    }
}

A questo punto creiamo il FactoryProvider, cioè un oggetto che avrà il compito di istanziare la corretta factory concreta, necessaria alla creazione del corretto oggetto report richiesto, in base alla sua categoria ed al suo tipo. Spesso, la logica contenuta nel factory provider viene inserita direttamente nell’oggetto “client”, utilizzatore degli oggetti del modello (nel nostro caso sarebbe la classe Archive), ma è preferibile utilizzare questo ulteriore livello di disaccoppiamento, in modo da evitare di dover modificare la classe utilizzatrice in caso di aggiunta di nuove categorie (più avanti vediamo un esempio).

public abstract class FactoryProvider {

    public static IReportFactory getFactory(String factoryType) {
        IReportFactory rf = null;
        switch(factoryType) {
            case "IN":
                    rf = new INReportFactory();
                break;
            case "OUT":
                    rf = new OUTReportFactory();
                break;
            default:
                break;
        }
        return rf;
    }
}

Infine definiamo la classe Archive, che sarà il nostro oggetto utilizzatore dei report. Essa conterrà una lista di report ed un metodo per l’inserimento di un nuovo report, che sfrutterà il FactoryProvider e l’interfaccia della factory per istanziare nuovi report, senza dipendere dalle implementazioni concrete. Inoltre aggiungiamo un metodo che consente di effettuare il processamento di tutti i report presenti nella lista.

import java.util.ArrayList;
import java.util.List;

public class Archive {

        private List reportList;

        public void addReport(String fam, String type, String name) {
            IReportFactory rf = FactoryProvider.getFactory(fam);
            if (this.reportList == null) {
                this.reportList = new ArrayList();
            }
            this.reportList.add(rf.createReport(type, name));
        }

        public void processAllReports() {

            for (AReport r: this.reportList) {
                r.processReport();
                System.out.println("-----");
            }
        }
}

Per verificare il tutto utilizziamo una classe di test, in cui creiamo un archivio al quale andiamo ad aggiungere una lista di report che il sistema deve aggiungere all’archivio e poi processare.

public class AbstractFactoryTest {

    public static void main(String[] args) {

        String [] reports = {"IN_INV_001.txt","OUT_PUR_001.txt","IN_INV_002.txt", "IN_PUR_001.txt", "OUT_PUR_002.txt", "OUT_INV_001.txt", "IN_INV_003.txt"};
        String tmp[] = null;
		
        Archive a = new Archive();

        for (String s: reports) {
            tmp = s.split("_");
            a.addReport(tmp[0], tmp[1], s);
        }

        a.processAllReports();
    }
}

Eseguendo il programma di test otteniamo il seguente risultato:

Processing report: IN_INV_001.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----
Processing report: OUT_PUR_001.txt
Performing OUT Reports common stuff
Performing OUT Reports Purchase specific stuff
-----
Processing report: IN_INV_002.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----
Processing report: IN_PUR_001.txt
Performing IN Reports common stuff
Performing IN Reports Purchase specific stuff
-----
Processing report: OUT_PUR_002.txt
Performing OUT Reports common stuff
Performing OUT Reports Purchase specific stuff
-----
Processing report: OUT_INV_001.txt
Performing OUT Reports common stuff
Performing OUT Reports Invoice specific stuff
-----
Processing report: IN_INV_003.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----

I vantaggi di modellare un problema di questo tipo con una soluzione basata su un design pattern come l’Abstract Factory sono la sua flessibilità e la sua scalabilità, dati principalmente dai seguenti aspetti:

  • Archive dipende solo da interfacce e non conosce nulla di come le factory concrete ed i relativi report vengano creati
  • L’eventuale aggiunta di una nuova categoria di report non ha nessun impatto sulla classe utilizzatrice Archive, ma viene gestita facilmente nel FactoryProvider, aggiungendo semplicemente un nuovo case nello switch per l’istanziazione della nuova factory concreta
  • L’aggiunta di una nuova tipologia di report ad una categoria esistente è completamente trasparente sia per la classe Archive che per il FactoryProvider e viene gestita semplicemente nella factory concreta della categoria aggiungendo un nuovo case nello switch
  • Il FactoryProvider è completamente disaccoppiato e puÚ essere riutilizzato ovunque

Proviamo a mettere in atto alcune delle estensioni del sistema ipotizzate nell’elenco precedente per verificare nella pratica gli impatti che si hanno sul codice esistente.

Aggiunta di una nuova categoria di report
Supponiamo di dover aggiungere al sistema una nuova categoria di report MIXED che prevede anch’essa la possibilità di avere due tipologie di report “Invoice” e “Purchase”.
Per farlo dobbiamo creare la nuova astrazione per i report della categoria MIXED, che estende AReport come le precedenti, e le due implementazioni concrete dei report di tipo invoice e purchase per questa categoria.

public abstract class AMIXReport extends AReport {

    protected AMIXReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {

        super.processReport();
        System.out.println("Performing MIX Reports common stuff");
    }
}
public class MIXInvoiceReport extends AMIXReport{

    protected MIXInvoiceReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {
        super.processReport();
        System.out.println("Performing MIX Reports Invoice specific stuff");
    }
}
public class MIXPurchaseReport extends AMIXReport{

    protected MIXPurchaseReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {
        super.processReport();
        System.out.println("Performing MIX Reports Purchase specific stuff");
    }
}

Creiamo poi la nuova factory concreta per la nuova categoria che implementa ovviamente l’interfaccia della factory generica.

public class MIXReportFactory implements IReportFactory {

    @Override
    public AReport createReport(String type, String name) {
        AReport doc = null;
        switch(type) {
            case "INV":
                doc = new MIXInvoiceReport(name);
            break;
            case "PUR":
                doc = new MIXPurchaseReport(name);
            break;
            default:
                break;
        }
        return doc;
    }
}

Fino a questo momento abbiamo solo creato nuove classi, senza modificare nulla del codice esistente. L’unica modifica necessaria è relativa al FactoryProvider che è proprio il livello di disaccoppiamento che abbiamo inserito e su cui vogliamo che le modifiche siano concentrate, perchè è una classe sotto il nostro controllo. Classe che modifichiamo nel modo seguente:

public abstract class FactoryProvider {

    public static IReportFactory getFactory(String factoryType) {
        IReportFactory rf = null;
        switch(factoryType) {
            case "IN":
                    rf = new INReportFactory();
                break;
            case "OUT":
                    rf = new OUTReportFactory();
                break;
            case "MIX":
                    rf = new MIXReportFactory();
            default:
                break;
        }
        return rf;
    }
}

La classe Archive non subisce nessuna modifica ed è in grado di gestire i nuovi report della nuova categoria in modo completamente trasparente!
Modifichiamo nella classe di test la lista dei report da processare, inserendone alcuni della nuova categoria creata, e verifichiamo che vengano gestiti correttamente.

public class AbstractFactoryTest {

    public static void main(String[] args) {

        String [] reports = {"MIX_INV_001.txt","MIX_PUR_002.txt","IN_INV_001.txt","OUT_PUR_001.txt","IN_INV_002.txt", "IN_PUR_001.txt", "OUT_PUR_002.txt", "OUT_INV_001.txt", "IN_INV_003.txt"};

        Archive a = new Archive();

        String tmp[] = null;

        for (String s: reports) {
            tmp = s.split("_");
            a.addReport(tmp[0], tmp[1], s);
        }

        a.processAllReports();
    }
}

Il risultato ottenuto è il seguente:

Processing report: MIX_INV_001.txt
Performing MIX Reports common stuff
Performing MIX Reports Invoice specific stuff
-----
Processing report: MIX_PUR_002.txt
Performing MIX Reports common stuff
Performing MIX Reports Purchase specific stuff
-----
Processing report: IN_INV_001.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----
Processing report: OUT_PUR_001.txt
Performing OUT Reports common stuff
Performing OUT Reports Purchase specific stuff
-----
Processing report: IN_INV_002.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----
Processing report: IN_PUR_001.txt
Performing IN Reports common stuff
Performing IN Reports Purchase specific stuff
-----
Processing report: OUT_PUR_002.txt
Performing OUT Reports common stuff
Performing OUT Reports Purchase specific stuff
-----
Processing report: OUT_INV_001.txt
Performing OUT Reports common stuff
Performing OUT Reports Invoice specific stuff
-----
Processing report: IN_INV_003.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----

Come possiamo vedere i nuovi report sono processati correttamente e tutto funziona!

Aggiunta di un tipo di report alla categoria INPUT
Proviamo invece ora ad aggiungere una nuova tipologia di report ad una categoria esistente. Supponiamo di volere aggiungere dei report di tipo “Order” alla categoria INPUT. Creaiamo quindi l’implementazione concreta INOrderReport che estende AINReport.

public class INOrderReport extends AINReport {

    protected INOrderReport(String name) {
        super(name);
    }

    @Override
    public void processReport() {

        super.processReport();
        System.out.println("Performing IN Reports Order specific stuff");
    }
}

L’unica modifica al codice esistente che dobbiamo fare è nella factory dei report della categoria INPUT. Anche in questo caso non è necessaria nessuna modifica alla classe Archive e questa volta neanche al FactoryProvider.

public class INReportFactory implements IReportFactory {

    @Override
    public AReport createReport(String type, String name) {
        AReport doc = null;
        switch(type) {
            case "INV":
                    doc = new INInvoiceReport(name);
                break;
            case "PUR":
                    doc = new INPurchaseReport(name);
                break;
            case "ORD":
                    doc = new INOrderReport(name);
                break;
            default:
                break;
        }
        return doc;
    }
}

Aggiungiamo nella lista di report della nostra classe di test un report di questo nuovo tipo, ad esempio “IN_ORD_004.txt”, ed eseguiamo nuovamente il programma.

public class AbstractFactoryTest {

    public static void main(String[] args) {

        String [] reports = {"IN_ORD_004.txt","MIX_INV_001.txt","MIX_PUR_002.txt","IN_INV_001.txt","OUT_PUR_001.txt","IN_INV_002.txt", "IN_PUR_001.txt", "OUT_PUR_002.txt", "OUT_INV_001.txt", "IN_INV_003.txt"};

        Archive a = new Archive();

        String tmp[] = null;

        for (String s: reports) {
            tmp = s.split("_");
            a.addReport(tmp[0], tmp[1], s);
        }

        a.processAllReports();
    }
}

Il risultato ottenuto è mostrato di seguito:

Processing report: IN_ORD_004.txt
Performing IN Reports common stuff
Performing IN Reports Order specific stuff
-----
Processing report: MIX_INV_001.txt
Performing MIX Reports common stuff
Performing MIX Reports Invoice specific stuff
-----
Processing report: MIX_PUR_002.txt
Performing MIX Reports common stuff
Performing MIX Reports Purchase specific stuff
-----
Processing report: IN_INV_001.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----
Processing report: OUT_PUR_001.txt
Performing OUT Reports common stuff
Performing OUT Reports Purchase specific stuff
-----
Processing report: IN_INV_002.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----
Processing report: IN_PUR_001.txt
Performing IN Reports common stuff
Performing IN Reports Purchase specific stuff
-----
Processing report: OUT_PUR_002.txt
Performing OUT Reports common stuff
Performing OUT Reports Purchase specific stuff
-----
Processing report: OUT_INV_001.txt
Performing OUT Reports common stuff
Performing OUT Reports Invoice specific stuff
-----
Processing report: IN_INV_003.txt
Performing IN Reports common stuff
Performing IN Reports Invoice specific stuff
-----

L’esempio completo di tutte le classi è scaricabile qui:

Leave a Reply

Your email address will not be published. Required fields are marked *