| Blog2j | ||||
|
Cerca con Google
Ricerca personalizzata
|
Creare una factory con spring
In questo tutorial vedremo vari modi di come organizzare le classi di business di una applicazione. Partendo da un semplice esempio rappresentato da un metodo che accede a un db ed esegue una elaborazione sui dati vedremo come applicare alcuni pattern per migliorare il design del codice. Infine vedremo come eliminare la factory introdotta sostituendola con spring. Il codice sorgente dell'esempio è disponibile qui, per funzionare ha bisogno delle librerie spring.jar e spring-test.jar (disponibili nel sito ufficiale) e di commons-logging.jar. In questo tutorial vedremo vari modi di come organizzare le classi di business di una applicazione. Partendo da un semplice esempio rappresentato da un metodo che accede a un db ed esegue una elaborazione sui dati vedremo come applicare alcuni pattern per migliorare il design del codice. Infine vedremo come eliminare la factory introdotta sostituendola con spring. Il codice sorgente dell'esempio è disponibile qui, per funzionare ha bisogno delle librerie spring.jar e spring-test.jar (disponibili nel sito ufficiale) e di commons-logging.jar. L'esempio preso in considerazione è un metodo che legge da un database una lista di risultati di partite di calcio e la elabora ritornando la classifica del campionato: public static List<PuntiSquadra> calcolaClassifica() {
List<Partita> partite = Db.eseguiQuery("Select * from partita");
Map<String, PuntiSquadra> mapSquadre = new HashMap<String, PuntiSquadra>();
for (Partita p : partite) {
PuntiSquadra puntiSquadraCasa = getPuntiSquadra(mapSquadre,
p.getSquadraCasa());
PuntiSquadra puntiSquadraFuori = getPuntiSquadra(mapSquadre,
p.getSquadraFuori());
int goalSquadraCasa = p.getGoalSquadraCasa();
int goalSquadraFuori = p.getGoalSquadraFuori();
puntiSquadraCasa.setPartiteGiocate(puntiSquadraCasa.getPartiteGiocate() + 1);
puntiSquadraFuori.setPartiteGiocate(puntiSquadraFuori.getPartiteGiocate() + 1);
puntiSquadraCasa.setGoalFatti(puntiSquadraCasa.getGoalFatti() +
goalSquadraCasa);
puntiSquadraCasa.setGoalSubiti(puntiSquadraCasa.getGoalSubiti() +
goalSquadraFuori);
puntiSquadraFuori.setGoalFatti(puntiSquadraFuori.getGoalFatti() +
goalSquadraFuori);
puntiSquadraFuori.setGoalSubiti(puntiSquadraFuori.getGoalSubiti() +
goalSquadraCasa);
if (goalSquadraCasa > goalSquadraFuori) {
puntiSquadraCasa.setPunti(puntiSquadraCasa.getPunti() + 3);
} else if (goalSquadraCasa < goalSquadraFuori) {
puntiSquadraFuori.setPunti(puntiSquadraFuori.getPunti() + 3);
} else {
puntiSquadraCasa.setPunti(puntiSquadraCasa.getPunti() + 1);
puntiSquadraFuori.setPunti(puntiSquadraFuori.getPunti() + 1);
}
}
List<PuntiSquadra> classifica = new ArrayList<PuntiSquadra>(mapSquadre.values());
Collections.sort(classifica);
return classifica;
}
Questo metodo è stato definito static, esegue una elaborazione e non ha bisogno di salvare dati a livello di oggetto. Per questo semplice esempio non è un grosso problema aver definito questo metodo static ma in applicazioni complesse può rappresentare un problema definire i metodi di business in questo modo. Infatti l'implementazione di un metodo static è fissa, non è possibile separare la signature del metodo dall'implementazione. Un primo miglioramento è aggiungere una interfaccia che contiene il metodo visto: public interface GestoreClassifica {
List<PuntiSquadra> calcolaClassifica();
}
L'implementazione di questa interfaccia deve comunque essere stateless e logicamente deve esserci all'interno dell'applicazione una sola istanza di questa classe. Per ottenere questo possiamo sfruttare il pattern singleton che ci assicura che sarà creata una sola istanza della classe: public class GestoreClassificaSingleton implements GestoreClassifica {
public static GestoreClassifica instance;
private GestoreClassificaSingleton() {
}
public static GestoreClassifica singleton() {
if (instance == null) {
instance = new GestoreClassificaSingleton();
}
return instance;
}
@Override
public List<PuntiSquadra> calcolaClassifica() {
....
}
}
Da notare che il costruttore della classe è privato, una nuova istanza può essere creata solo all'interno della classe (viene creata solo una volta all'interno del metodo singleton). In un ambiente multithread (per esempio in una applicazione web) il semplice if che controlla se l'istanza è null non basta per essere sicuri di creare una sola istanza ma deve essere usato un blocco synchronized. Un esempio di utilizzo della classe scritta è il seguente: public static void main(String[] args) {
GestoreClassifica gestoreClassifica = GestoreClassificaSingleton.singleton();
List<PuntiSquadra> classifica = gestoreClassifica.calcolaClassifica();
System.out.println("Squadra \tP\tG\tGF\tGS\t");
for (PuntiSquadra p : classifica) {
System.out.println(MessageFormat.format("{0}\t{1}\t{2}\t{3}\t{4}", p.getSquadra(), p.getPunti(), p
.getPartiteGiocate(), p.getGoalFatti(), p.getGoalSubiti()));
}
}
Usando il pattern singleton siamo riusciti a separare l'interfaccia dall'implementazione ma fino ad adesso abbiamo creato una sola implementazione. A volte è necessario scrivere più implementazioni di una classe di business, per esempio per avere più implementazioni per gestire database diversi o per cambiare leggermente delle logiche di business per adattarsi a clienti diversi. In questi casi può essere sfruttato il pattern factory, l'oggetto non viene creato direttamente con una new (o attraverso un singleton) ma viene richiesto a una classe factory che istanzia la giusta classe in base a una certa logica. La factory è l'unica classe che conosce le varie implementazioni dell'interfaccia e che conosce la logica di scelta dell'implementazione da usare. Nel nostro semplice esempio semplificato la factory gestisce una solo implementazione: public class Factory {
private static GestoreClassifica gestoreClassifica;
public static GestoreClassifica getGestoreClassifica() {
if (gestoreClassifica == null) {
gestoreClassifica = new GestoreClassificaImpl();
}
return gestoreClassifica;
}
}
Un'altra cosa che può essere migliorata è la gestione dell'accesso al database (nell'esempio visto c'è una classe Db che simula un framework di accesso a db). Infatti inserendo la chiamata alla query direttamente nel metodo non è possibile riutilizzare la query in altre classi. Per aumentare il riuso del codice possiamo sfruttare il pattern dao (data access object). Un dao è una classe che si occupa esclusivamente dell'accesso al database per eseguire le operazioni crud (create read update delete) su una tabella. Anche i dao sono oggetti stateless e possono essere usati come singleton o istanziati con una factory. In questo esempio usiamo la stessa factory già vista per istanziare il dao: public interface PartitaDAO {
List<Partita> select();
}
public class PartitaDAOImpl implements PartitaDAO {
@Override
public List<Partita> select() {
return Db.eseguiQuery("Select * from partita");
}
}
public class Factory {
...
private static PartitaDAOImpl partitaDAO;
public static PartitaDAO getPartitaDAO() {
if (partitaDAO == null) {
partitaDAO = new PartitaDAOImpl();
}
return partitaDAO;
}
}
Modifichiamo di conseguenza la classe che calcola la classifica per eseguire la query su db usando il dao ritornato dalla factory: public class GestoreClassificaImpl implements GestoreClassifica {
private PartitaDAO partitaDAO = Factory.getPartitaDAO();
@Override
public List<PuntiSquadra> calcolaClassifica() {
List<Partita> partite = partitaDAO.select();
...
}
}
Dopo aver scritto un po' di codice scriviamo anche alcuni test per verificare che il codice scritto sia corretto. In questo esempio usiamo la libreria junit per scrivere i test. Una classe di test che controlla la correttezza della classe dao è la seguente: public class PartitaDAOTest extends TestCase {
public void testCalcolaClassifica() {
PartitaDAO partitaDAO = new PartitaDAOImpl();
List<Partita> list = partitaDAO.select();
assertEquals(4, list.size());
}
}
Ad essere pignoli si potrebbero trovare diversi difetti a questa classe, per esempio i controlli fatti sono minimi ed il test dipende dai dati che ci sono su db e quindi non è ripetibile, per evitare ciò avremmo dovuto usare una libreria tipo dbUnit. Scriviamo un test (anche questo molto semplificato) per la classe che calcola la classifica: public class GestoreClassificaFactoryTest extends TestCase {
public void testCalcolaClassifica() {
List<PuntiSquadra> classifica = new GestoreClassificaImpl().calcolaClassifica();
assertEquals(4, classifica.size());
}
}
Questo test ha un problema ulteriore, viene passato solo il funzionamento sia del gestoreClassifica che del dao è corretto, per come abbiamo definito la nostra interfaccia è difficile testare le due cose in modo separato. Facciamo un altro passo avanti per migliorare il design e aumentare il disaccoppiamento fra le classi viste, aggiungiamo il framework spring al progetto. Spring è un framework java (ma esiste anche la versione per .net) che permette di gestire le relazioni fra le classi sfruttando il pattern dependency injection. Con questo pattern si arriva a un risultato simile a quello di una factory ma in un modo diverso, in pratica una classe che ha bisogno di un servizio non chiede una istanza a una factory ma è il container (ovvero la factory di spring) che inietta all'interno della nostra classe l'istanza giusta. Si ha quella che viene chiamata inversion of control, non è una classe che chiede l'istanza giusta al framework ma il framework che la imposta nella classe. Per capirci meglio vediamo l'esempio pratico: public class GestoreClassificaSpringImpl implements GestoreClassifica {
private final PartitaDAO partitaDAO;
public GestoreClassificaSpringImpl(PartitaDAO partitaDAO) {
this.partitaDAO = partitaDAO;
}
@Override
public List<PuntiSquadra> calcolaClassifica() {
List<Partita> partite = partitaDAO.select();
...
}
}
Il cambiamento è lieve ma importante, l'oggetto partitaDAO non viene chiesto alla factory ma viene messo come parametro al costruttore. In questo modo la classe GestoreClassificaSpringImpl non referenzia la factory e non sa niente di come viene creata l'istanza di partitaDAO, c'è quindi un maggiore disaccoppiamento nelle classi. Ovviamente da qualche parte c'è da scrivere come creare l'oggetto di tipo PartitaDAO, il posto giusto è il file di configurazione di spring: <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<bean id="partitaDAO" class="it.blog2j.dao.PartitaDAOImpl" />
<bean id="gestoreClassifica" class="it.blog2j.spring.GestoreClassificaSpringImpl">
<constructor-arg ref="partitaDAO" />
</bean>
</beans>
In questo file di configurazione abbiamo definito i bean gestiti con spring e le relazioni fra i vari bean, in particolare abbiamo specificato con il tag constructor-arg di passare il bean partitaDAO come argomento del costruttore del bean gestoreClassifica. Di default spring instanzia un solo oggetto per bean, ovvero i bean definiti sono tutti singleton. L'injection di un bean in un altro avviene principalmente in due modi, il primo modo è quello appena visto, è chiamato constructor injection. L'altro modo consiste nel richiamare un metodo set di una proprietà all'interno del bean (setter injection). Esiste anche un terzo modo chiamato lookup method injection ma è un po' più complicato e meno usato nella pratica. Un metodo main che lancia il metodo di creazione della classifica è il seguente: public static void main(String[] args) {
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource(
"it/blog2j/spring/factoryConfig.xml"));
GestoreClassifica gestoreClassifica =
(GestoreClassifica) beanFactory.getBean("gestoreClassifica");
List<PuntiSquadra> classifica = gestoreClassifica.calcolaClassifica();
System.out.println("Squadra \tP\tG\tGF\tGS\t");
for (PuntiSquadra p : classifica) {
System.out.println(MessageFormat.format("{0}\t{1}\t{2}\t{3}\t{4}",
p.getSquadra(), p.getPunti(), p.getPartiteGiocate(),
p.getGoalFatti(), p.getGoalSubiti()));
}
}
Possiamo notare che è rimasta una chiamata ad una factory, di solito questa chiamata è l'unico punto del codice in cui c'è una dipendenza ad una classe di spring (ovvero la factory stessa). L'introduzione di spring in un progetto porta di solito ad un maggiore disaccoppiamento delle classi e di conseguenza a classi testabili più facilmente. Nel nostro esempio possiamo separare facilmente il test del dao da quello del gestoreClassifica: public class GestoreClassificaSpringIsolationTest extends TestCase {
public void testCalcolaClassificaVuota() {
GestoreClassifica gestoreClassifica = new GestoreClassificaSpringImpl(
new PartitaDAO() {
@Override
public List<Partita> select() {
return new ArrayList<Partita>();
}
});
List<PuntiSquadra> classifica = gestoreClassifica.calcolaClassifica();
assertEquals(0, classifica.size());
}
public void testCalcolaClassificaUnaPartita() {
GestoreClassifica gestoreClassifica = new GestoreClassificaSpringImpl(
new PartitaDAO() {
@Override
public List<Partita> select() {
return Arrays.asList(new Partita("s1", "s2", 0, 0));
}
});
List<PuntiSquadra> classifica = gestoreClassifica.calcolaClassifica();
assertEquals(2, classifica.size());
}
}
In questo test viene istanziata manualmente la classe GestoreClassificaSpringImpl passando nel costruttore un oggetto creato al volo, chiamato anche oggetto mock. Facendo così il calcolo della classifica può essere testato in isolamento, se il test non viene passato siamo sicuri che il problema sia nel calcolo e non per esempio nell'accesso al db o nei dati presenti nel db. Esistono librerie apposite per creare oggetti mock in modo semplice, per esempio jmock o easyMock. Se invece vogliamo testare tutto insieme (factory di spring compresa) possiamo scrivere la seguente classe di test: public class GestoreClassificaSpringTest extends
AbstractDependencyInjectionSpringContextTests {
protected GestoreClassifica gestoreClassifica;
public GestoreClassificaSpringIsolationTest() {
setPopulateProtectedVariables(true);
}
public void testCalcolaClassifica() {
List<PuntiSquadra> classifica = gestoreClassifica.calcolaClassifica();
assertEquals(4, classifica.size());
}
@Override
protected String[] getConfigLocations() {
return new String[] { "classpath:it/blog2j/spring/factoryConfig.xml" };
}
}
Questo test estende una classe di spring in modo da non dover istanziare la factory manualmente, viene creata automaticamente all'inizio del test in base ai nomi dei file (una factory di spring può essere costruita anche mettendo insieme più file xml) ritornati dal metodo getConfigLocations. Il bean gestoreClassifica viene iniettato automaticamente in quanto è definito protected e nel costruttore viene invocato il metodo setPopulateProtectedVariables. Spring è un framework molto vasto, in questa introduzione abbiamo visto solo una piccola parte. Altri moduli interessanti sono:
Articoli correlati:
Factory complesse con spring Joshua Bloch - Effective java Second Edition Gestione di una web app con eclipse e maven Griglia Extjs con paginazione, ordinamento e parametri di ricerca Chiamate ajax verso bean di Spring con Dwr Comandi e shortcut di Eclipse Gestione dei javascript e dei css in una web app con jawr Gestione di una web app con eclipse e maven [old] Validazione client con jquery Tutorial javascript Validazione client con extJs Introduzione a groovy |
News
|
||