Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

AppFuse: realizzare un'applicazione completa

Prosegue la serie per realizzare una app completa con AppFuse. Questa volta implementiamo i servizi (DAO, Service, ...)
Prosegue la serie per realizzare una app completa con AppFuse. Questa volta implementiamo i servizi (DAO, Service, ...)
Link copiato negli appunti

Nel primo e secondo articolo di questa serie sul framework AppFuse abbiamo svolto diversi compiti importanti per il nostro progetto JobBoard:

  • abbiamo definito alcuni requisiti di base dell'applicazione;
  • creato dei mockup, ipotizzando delle user story convincenti;
  • abbiamo preparato la struttura del progetto tramite un Maven Archetype (strutture di progetto predefinite) fornito da AppFuse, in versione Hibernate / Spring MVC;
  • lanciato il primo build e la prima istanza di test.

Chi ha seguito tutti i passi precedenti avrà già verificato con soddisfazione che molte delle user story in realtà sono già pre-impostate da AppFuse, quindi non ci dilungheremo troppo su questi aspetti, ma ci occuperemo della realizzazione del cuore dell'applicazione, ovvero:

  • modello dei dati e mappatura sul database;
  • strato di accesso al database, comunemente definito DAO (Data Access Objects);
  • strato dei servizi applicativi, spesso chiamato Service Layer oppure (come nella documentazione di AppFuse) Manager Layer perché utilizzato come vero e proprio "centro servizi" dai vari client web, mobile, rich client, eccetera.

Architettura dell'applicazione

La suddivisione logica che andremo a operare in Model, DAO e Service non è casuale, di fatto è l'architettura utilizzata da tutte le applicazioni enterprise, nessuna esclusa, ovviamente con le variazioni e peculiarità del caso.

In alcuni casi infatti non abbiamo una separazione netta fra due strati adiacenti, ad esempio lo strato DAO a volte va a coincidere con il Model (come nei framework Rails-like), altre volte invece è implicito nel Service.

Questi accorpamenti sono spesso dettati dall'esigenza di risparmiare (meno codice = meno test = risparmio di tempo e denaro), tuttavia siamo convinti che limitino le possibilità di evoluzione del progetto.
Avere strati applicativi distinti e indipendenti infatti consente di "cambiare idea" quando il progetto è già in uno stadio avanzato, senza costringere a dover ripensare tutta l'applicazione da capo!

AppFuse adotta in pieno questa filosofia, è infatti organizzato secondo l'architettura classica che abbiamo esposto.

Creazione del modello

AppFuse fornisce in partenza due classi del modello, entrambe nel package org.appfuse.model : User e Role. Il loro sorgente però non è nella directory del progetto, ovvero in src/main/java, perché sono contenute in una libreria. Nel caso abbiate intenzione di modificare queste o altre classi del progetto è possibile eseguire il comando full-source, come spiegato sulla documentazione ufficiale.

Creiamo ora le nostre prime entity nel package it.html.jobboard.model. Ecco il sorgente della classe Company:

package it.html.jobboard.model;
import javax.persistence.*;
@Entity
@Table(name = "companies")
public class Company {
    private Long id;
    private String name;
    public Company() {}
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getId() {
        return id;
    }
    @Basic(optional = false)
    public String getName() {
        return name;
    }
	// TODO: altri getter/setter ...
}

La classe JobOffer sarà invece simile allo snippet seguente:

@Entity
@Table(name = "job_offers")
public class JobOffer {
    public enum ContractType {
        PERMANENT,
        TEMPORARY_CONTRACT
    }
    public JobOffer() {}
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public Long getId() {
        return id;
    }
    @ManyToOne(optional = false)
    public Company getCompany() {
        return company;
    }
    @Basic(optional = false)
    public String getTitle() {
        return title;
    }
    @Enumerated(EnumType.STRING)
    @Basic(optional = false)
    public ContractType getContractType() {
        return contractType;
    }
	// TODO: altri getter/setter ...
}

Vediamo a questo punto alcune delle loro caratteristiche principali:

  • c'è un ampio uso delle annotazioni di JPA (Java Persistence API), come @Entity, @Table, eccetera;
  • il campo ID è un intero auto-incrementale, come definito dalle annotazioni @Id e @GeneratedValue(strategy = GenerationType.AUTO);
  • abbiamo una relazione molti-a-uno (@ManyToOne) da JobOffer a Company;
  • l'enumerazione ContractType va configurata con @Enumerated(EnumType.STRING) per salvarla sotto forma di stringa, in caso contrario verrebbe salvato il suo ordinale;
  • l'annotazione @Basic(optional = false) indica che la proprietà (e quindi la colonna sul database) non può avere valore NULL.

Le possibilità di mapping offerte da JPA e le estensioni di Hibernate sono davvero molte e sofisticate, ma già con questi elementi basilari siamo in grado di creare uno schema di database non banale, con foreign key e vincoli di non-nullità.

Fortunatamente grazie a Maven e ad un plugin di Hibernate è sufficiente eseguire un comando Maven per creare tutte le tabelle e le relazioni

mvn hibernate3:hbm2ddl

In ogni caso, questa fase è inserite nel processo di build generale e viene eseguita ogni volta, in modo da ricreare da zero il database e ri-testare tutto in un ambiente pulito e soprattutto prevedibile. Dopo ogni build quindi, avremo lo schema job_board aggiornato sul database MySql. Controllate!

Ecco uno schema delle tabelle create, con tanto di relazioni:

Figura 1. schema delle tabelle create
(clic per ingrandire)


schema delle tabelle create

Nella prossima parte implementeremo anche gli oggetti DAo e finalmente i servizi veri e propri

DAO - Data Access Objects

A questo punto possiamo creare gli oggetti DAO, che si occuperanno di eseguire le query vere e proprie sul database: ricerca, lettura, salvataggio.

In un certo senso sono lo strato applicativo che si "sporca le mani" più degli altri, perché deve nascondere tutta la complessità di Hibernate e JDBC e mostrare allo strato superiore (il Service layer) solamente dei semplici metodi Java.

Questo approccio facilità senza dubbio la manutenibilità del codice nei vari layer, che non devono conoscere esattamente cosa succede nello strato sottostante e possono così occuparsi solamente delle proprie responsabilità.

Per rafforzare questo aspetto si usa creare almeno due sorgenti per ogni oggetto, sia nel DAO che nel Service layer: l'interfaccia e una (o più) implementazioni.

Ecco quindi l'interfaccia it.html.jobboard.dao.JobBoardDao:

public interface JobBoardDao {
    /**
     * Ricerca offerte di lavoro, mostra prima le più recenti.
     * @return lista di {@link JobOffer}
     */
    List<JobOffer> findAllJobOffer();
}

In pieno stile Agile svilupperemo i test prima delle implementazioni, come dettato dalle regole del Test-driven development. Quello che a prima vista può sembrare un controsenso è in realtà un ottimo modo per:

  • provare subito il proprio codice, identificando molto presto eventuali falle di design;
  • creare, di fatto, una specifica di funzionamento che va al di là del semplice elenco di parametri e tipi;
  • avere un ottimo "sistema di allarme" che scatterà nel momento in cui faremo delle modifiche che introducono bug o modificano il comportamento dei vari oggetti.

Ecco quindi il test per JobBoardDao, da salvare in una sotto-directory di src/test/java:

public class JobBoardDaoTest extends BaseDaoTestCase {
    JobBoardDao jobBoardDao;
    @Autowired
    public void setJobBoardDao(JobBoardDao jobBoardDao) {
        this.jobBoardDao = jobBoardDao;
    }
    @Test
    public void test_findAllJobOffer() {
        List<JobOffer> result = jobBoardDao.findAllJobOffer();
        assertEquals(4, result.size());
        // controlla l'ordine dei risultati
        assertEquals(4L, result.get(0).getId().longValue());
        // ...
    }
}

Notiamo subito alcuni particolari:

  • estendere org.appfuse.dao.BaseDaoTestCase ci permette di sfruttare l'auto-wiring di Spring, replicando ciò che succede a runtime. In questo modo i test vengono eseguiti in un ambiente del tutto simile a quello "vero";
  • l'annotazione @Autowired per l'appunto indica a Spring che desideriamo ricevere l'istanza di JobBoardDao già configurata;
  • l'istruzione import static org.junit.Assert.* ci permette di utilizzare i vari metodi di asserzione di questa classe, come assertEquals(), assertTrue(), eccetera.

Inoltre il test sembra dare per scontato che ci siano dei dati di prova, già inseriti da qualche parte nel database: questi sono nel file src/test/resources/sample-data.xml, utilizzato dal plugin DbUnit durante il build per popolare le tabelle. Aggiungiamo quindi le sezioni <table name="companies"> e <table name="job_offers"> a questo file, che purtroppo comincerà ad essere davvero molto verboso e un po' difficile da seguire:

[...]
<table name="companies">
  <column>id</column>
  <column>name</column>
  <row>
    <value description="id">1</value>
    <value description="name">Company A</value>
  </row>
  <row>
    <value description="id">2</value>
    <value description="name">Company B</value>
  </row>
</table>
<table name="job_offers">
  <column>id</column>
  <column>creationDate</column>
  <column>company_id</column>
  <column>contractType</column>
  <column>country</column>
  <column>title</column>
  <column>description</column>
  <row>
    <value description="id">1</value>
    <value description="creationDate">2012-02-01 11:36:55.0</value>
    <value description="company_id">1</value>
    <value description="contractType">PERMANENT</value>
    <value description="country">Italia</value>
    <value description="title">Job Offer 1</value>
    <value description="description">Descrizione Job Offer 1</value>
  </row>
  [...]
</table>

Implementazione del DAO

Ecco infine l'implementazione del DAO, it.html.jobboard.dao.hibernate.JobBoardDaoImpl:

package it.html.jobboard.dao.hibernate;
@Repository
public class JobBoardDaoImpl extends HibernateDaoSupport implements JobBoardDao {
    @Autowired
    public void init(SessionFactory factory) {
        setSessionFactory(factory);
    }
    public List<JobOffer> findAllJobOffer() {
        return getHibernateTemplate().find("from JobOffer o order by o.creationDate desc");
    }
}

Da notare che abbiamo inserito questa classe in un sotto-package hibernate; in questo modo la suddivisione fra le varie (eventuali) altre implementazioni come jdbc, EJB, eccetera, è ancora più esplicita.

Alcune osservazioni:

  • l'annotazione @Repository istruisce Spring sul fatto che questo componente sia un DAO, e che va inizializzato con un'opportuna SessionFactory;
  • l'inizializzazione della SessionFactory tramite il metodo init();
  • l'uso del metodo getHibernateTemplate() per ottenere un HibernateTemplate, classe di Spring che fa da tramite con le API di Hibernate;
  • la query HQL (Hibernate Query Language), in questo caso molto semplice e facilmente intuibile;

Hibernate template

HibernateTemplate è una classe di utilità di Spring che facilita l'utilizzo di Hibernate, nascondendone a sua volta la complessità e permettendoci di scrivere solo il codice strettamente necessario, come la query dell'esempio.

Realizzazione del Service

Siamo giunti allo strato Service, che nel nostro caso sarà solamente un passacarte, vista la semplicità quasi banale dei metodi realizzati finora. Avremo quindi l'interfaccia JobBoardService:

package it.html.jobboard.service;
public interface JobBoardService {
	/**
	 * Ricerca offerte di lavoro, mostra prima le più recenti.
	 * @return lista di {@link JobOffer}
	 */
	List<JobOffer> findAllJobOffer();
}

Il test JobBoardServiceTest:

package it.html.jobboard.service;
public class JobBoardServiceTest extends BaseManagerTestCase {
    JobBoardService jobBoardService;
    @Autowired
    public void setJobBoardService(JobBoardService jobBoardService) {
        this.jobBoardService = jobBoardService;
    }
    @Test
    public void test_findAllJobOffer() {
        List<JobOffer> result = jobBoardService.findAllJobOffer();
        assertTrue(result.size() >= 0);
    }
}

L'implementazione JobBoardServiceImpl:

package it.html.jobboard.service.spring;
@Service
@Transactional
public class JobBoardServiceImpl implements JobBoardService {
    private JobBoardDao jobBoardDao;
    @Autowired
    public void setJobBoardDao(JobBoardDao jobBoardDao) {
        this.jobBoardDao = jobBoardDao;
    }
    public List<JobOffer> findAllJobOffer() {
        return jobBoardDao.findAllJobOffer();
    }
}

Ora è sufficiente eseguire un build per verificare che tutto sia perfettamente funzionante:

mvn package

Sviluppi

Come stiamo verificando, sviluppare i vari servizi applicativi in maniera indipendente ci permette di concentrarci sul buon funzionamento dei singoli componenti, facendo addirittura risparmiare tempo perché non vi è la necessità di mettere in piedi tutta la filiera per avere l'applicazione completa sul browser (database, servizi, controller web, pagine jsp, javascript, ecc.). Nei prossimi appuntamenti vedremo come, arrivati a questo punto, sia molto facile realizzare la parte web e aggiungere altre funzionalità.

Ti consigliamo anche