kandinsky quadro astratto

Written by 19:32 Creational Pattern, Design Pattern, DEVeloperS, Object Scoped Pattern

Abstract Factory

L’AbstractFactory è un design pattern che si pone come capostipite, super-factory di tutte le classi addette alla creazione di oggetti. Dunque, stiamo parlando di un pattern creazionale che gioca molto con l’astrattismo delle interfacce per fornirci una soluzione alla produzione di famiglie di prodotti correlati senza dover specificare le loro classi concrete.
La descrizione del pattern seguirà il preciso template della GoF che precedentemente abbiamo illustrato nell’introduzione ai design pattern.
Tuttavia… se ci occorresse giusto una rapida occhiata, l’immagine seguente offre uno spezzato del pattern e della sua tipologia (purpose e scope).   

Abstract Factory

Intento

Fornisce una interfaccia per creare famiglie di oggetti correlati o dipendenti senza specificare le loro classi concrete

Abstract Factory

Conosciuto anche come…

Questa pattern è conosciuto anche col nome KIT, nome alternativo che risulterà chiaro continuando la lettura.

Motivazione

Un esempio di problema reale aiuterà a capire bene le motivazioni del pattern.
Supponiamo di voler realizzare una applicazione che ha come obiettivo la creazione di file con il resoconto delle spese sostenute durante un certo periodo. Il processo preleverà i dati da una sorgente (tipicamente un database), li elaborerà creando un file in un determinato formato ed infine caricherà il report su di uno storage persistente.
Dalle nostre interviste condotte coi stakeholders, siamo venuti a conoscenza del fatto che l’attuale processo si basa su questo stack tecnologico: DBMS PostgreSQL locale, storage su file system locale e report in formato testuale CSV con intestazione.
Tuttavia, occorre affiancare anche un secondo sistema che supporti una differente “famiglia tecnologica” basata per lo più sui prodotti in cloud di Amazon. Nello specifico, un database documentale MongoDB, uno storage Amazon S3 e, per quanto riguarda il report, si vuole passare al PDF, di più semplice lettura.
Sebbene, a lungo andare, il sistema cloud-based soppianterà l’attuale, avremo bisogno di supportare entrambe le famiglie di prodotti. Inoltre, sapendo quanto le tecnologie siano fugaci e temporanee nel mondo dell’information technology, vorremmo scrivere del codice quanto più agnostico allo specifico stack, per agevolare il trapasso del vecchio a favor del nuovo.

Senza arrovellarci troppo il cervello, rimembriam quel tempo della vita mortale quando beltà splendea e lieto era il seguitar delle lezioni di Programmazione 2 sui design pattern. Dunque, rispolveriamo Elements of Reusable Object-Oriented Software e… spulciamo la soluzione proposta.

Innanzitutto, creiamo una interfaccia astratta AbstractApplicationFactory che dichiara i metodi per la creazione di ogni tipo di componente (alias prodotto) che ci serve, ovvero:

  • il Repository per l’estrazione dei dati delle spese (Expense) dal database;
  • l’Exporter per la produzione del File report;
  • lo Storage per il salvataggio del file prodotto.  

Dopodiché, affianchiamo alle interfacce astratte dei tre tipi di prodotto le classi concrete che le implementano per una determinata variante tecnologica.
Infine, scriveremo le differenti sottoclassi di AbstractApplicationFactory per creare tutti i prodotti di una determinata famiglia.
Da parte sua, il codice client che sfrutta il pattern si preoccuperà solamente di allocare la giusta sottoclasse factory, ad esempio CloudApplicationFactory. Tramite i suoi metodi, istanzierà il componente che necessita e lo utilizzerà come concordato con la sua interfaccia.

Abstract Factory

Ricapitolando… ci sarà una sottoclasse concreta di AbstractApplicationFactory per ognuna delle famiglie tecnologiche. Ogni sottoclasse implementa le operazioni per creare l’appropriato componente della famiglia. Ad esempio, il metodo createRepository() della classe CloudApplicationFactory, ci darà un oggetto Repository il cui tipo a runtime sarà MongoDBRepository. Il Client utilizzerà i vari componenti esclusivamente tramite una delle possibili factory ed utilizzerà i componenti in stile “black-box“, facendo fede al comportamento definito dall’interfaccia e non alla particolare implementazione.

Un esempio di Client può essere il seguente:

public class Client {
	
	public static void main( String[] args ) {
		
		AbstractApplicationFactory factory = new CloudApplicationFactory();
		Exporter exporter = factory.createExporter();
		Repository repository  = factory.createRepository();
		Storage storage = factory.createStorage();
		try {
			Collection<Expense> last30DaysExpenses = 
					repository.findExpenses(
							LocalDate.now(), LocalDate.now().minusDays(30));
			File report = exporter.export(last30DaysExpenses);
			storage.store(report);
		} catch (Exception e) {
			System.out.println("Something wrong but... DON'T PANIC!");
		}
	}
}

Applicabilità

Quando Usare AbstractFactory

Struttura & Partecipanti

Come da prassi, la struttura del pattern viene rappresentata tramite un class diagram UML.
Per rendere più chiaro il tutto, le interfacce sono state colorate in blu, così come le frecce che mostrano le relazioni d’uso del Client con la factory ed i prodotti astratti.
Inoltre viene data evidenza, tramite due set di icone poste sui vertici delle classi, alle varianti di prodotto presenti (naturalmente la molteplicità si può estendere) ed al loro legame con la factory adibita a crearli.

Conseguenze

Il pattern ha una serie di benefici ma anche qualche trade-off da pagare.

Innanzitutto, esso isola le classi concrete e nasconde al client i dettagli implementativi. Il pattern aiuta a controllare le istanze che una applicazione produce. Infatti, le factory concrete incapsulano ed hanno la responsabilità del processo di creazione dei prodotti. Inoltre, come ben sottolieato già, i client manipolano gli oggetti sempre attraverso le loro interfacce, e le classi concrete non verranno mai menzionate nel codice del client (stile black-box, scegliendo al più all’avvio dell’applicazione la factory desiderata).

Rende semplice lo scambio tra famiglie di prodotti. La factory concreta viene menzionata (e dunque allocata) in un unico punto del codice. Dunque, se si dovesse cambiare la factory che l’applicazione usa basterà semplicemente cambiare in quel punto il codice. Dal momento che una factory crea una completa famiglia di prodotti, l’intera famiglia cambierà all’unisono.

Promuove la consistenza e coesione tra prodotti. Le famiglie di prodotti sono progettate per lavorare insieme ed è chiara l’importanza che una applicazione utilizzi solamente una delle famiglie di prodotti per volta. Per come è strutturato, il pattern rafforza notevolmente questo vincolo.

Il supporto a nuove tipologie di prodotti è complicato. Estendere le factory astratte alla produzione di nuovi tipi di prodotti non è semplice. Questo poichè l’interfaccia AbstractFactory definisce e fissa i metodi di creazione dei prodotti che devono essere poi implementati da tutte le factory concrete. L’aggiunta di un nuovo metodo porterà inevitabilmente a modifiche in tutte le factory concrete. Comunque, c’è da dire che questo limite viene superato attraverso l’utilizzo di un secondo pattern, come vedremo nel seguito.

Implementazione

Nel seguito descriviamo alcune tecniche per l’implementazione del pattern.

Factories as singletons. Tipicamente, una applicazione necessità di solamente una instanza di una factory concreta. Questo rende la factory elegibile ad essere implementata tramite il pattern Singleton (nel seguito un esempio).

public class CloudApplicationFactory implements AbstractApplicationFactory{

	static CloudApplicationFactory instance;
	
	private CloudApplicationFactory() {
		//private constructor
	}
	
	public static CloudApplicationFactory getInstance() {
		//single access point to instance
		return instance == null ? new CloudApplicationFactory() : instance;
	}
	
	@Override
	public Repository createRepository() {
		return new MongoDBRepository();
	}

	@Override
	public Exporter createExporter() {
		return new PDFExporter();
	}

	@Override
	public Storage createStorage() {
		return new S3Storage();
	}

}

Creating the product. AbstractFactory è solo una interfaccia per la creazione dei prodotti, sta dunque alle sottoclassi di ConcreteProduct la loro creazione. Il modo più efficace per far ciò è la definizione di un factory method (cfr. Factory Method pattern) per ogni prodotto.

Prototype. Se sono possibili molte famiglie di prodotti, le factory concrete possono essere implementate tramite il pattern Prototype.

Defining extensible factories. Come visto, l’AbstractFactory definisce una operazione per ogni tipo di prodotto che può produrre. Il tipo di prodotto è dunque codificato nella firma del metodo e l’aggiunta di un nuovo make method comporterà modifiche in cascata su tutte le sue sottoclassi. Una soluzione più flessibile (ma meno sicura) prevede l’aggiunta di un parametro (ad esempio il valore di una enum) che specifica il tipo di di oggetto da creare. Con questo approccio, l’AbstractFactory necessita di un singolo metodo (magari tramite il pattern Factory Method). Un esempio potrebbe essere il seguente (nota che la mia tentazione a chiamare Bean il tipo di ritorno del metodo di creazione è stato forte, anche perchè questo pattern è molto ma molto radicato nella dependency injection).

public interface AbstractFactory {
	
	enum ProductType { REPOSIROTY, EXPORTER, STORAGE };
	
	Product create( ProductType type );

}

Si ma… il codice?

Don’t panic, lo puoi trovare in questo repository git su GitHub.

Dove viene utilizzato?

Un esempio concreto di utilizzo è il BeanFactory di Spring sulla quale, comunque, non mi dilungo. Piuttosto esorto chiunque a mostrarmi esempi reali per poter estendere l’articolo.

Pattern correlati

Bene o male i pattern correlati li abbiamo già passati in rassegna. Ad ogni modo, repetita iuvant, quindi… Le factory concrete vengono spesse implementate tramite il pattern Factory Method (che è di gran lunga meno impegnativo), ma potrebbero essere implementate anche tramite il Prototype. Quello che molto realisticamente avviene è l’utilizzo congiunto dell’Abstract Factory con il Factory Method.

Inoltre, le factory vengono spesso implementate come Singleton.

(Visited 109 times, 1 visits today)
Close