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 user story
  • abbiamo preparato la struttura del progetto tramite un Maven Archetype 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 AppFuse

  • modello dei dati
  • strato di accesso al database 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 @Entity @Table
    • il campo ID è un intero auto-incrementale, come definito dalle annotazioni @Id e @GeneratedValue(strategy = GenerationType.AUTO)
    • abbiamo una relazione molti-a-uno (@ManyToOne
    • l'enumerazione ContractType @Enumerated(EnumType.STRING)
    • l'annotazione @Basic(optional = false)

    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

    In ogni caso, questa fase è inserite nel processo di build generale e viene eseguita ogni volta job_board

    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
    • l'annotazione @Autowired JobBoardDao
    • l'istruzione import static org.junit.Assert.* assertEquals() assertTrue()

    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 SessionFactory
    • l'inizializzazione della SessionFactory tramite il metodo init()
    • l'uso del metodo getHibernateTemplate() HibernateTemplate
    • la query HQL Hibernate Query Language

    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:

    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