Introduzione
È nostra intenzione descrivere i punti di forza di RoR, anche con qualche confronto con il mondo Java, e fare un esempio di applicazione realizzata con Rails. L'esempio utilizzerà ActiveRecord, lo strumento di Object to Relational Mapping a disposizione per mappare i dati della nostra banca dati, nonchè alcuni script di generazione automatica del codice disponibili nello strumento.
Le origini di RoR
Il framework Ruby on Rails proviene dall'applicazione Basecamp, uno strumento di project management di 37signals. Padre del framework può essere considerato David Heinemeier Hansson. È stato rilasciato per la prima volta in luglio 2004. Nascendo da un contesto produttivo/commerciale tiene conto delle necessità che sorgono in tale ambito. Risulta quindi "produttivo".
Alcune considerazioni di base
Al fine di comprendere la filosofia del framework mettiamo in campo alcuni concetti e cerchiamo di capire quali sono gli obiettivi che RoR cerca di raggiungere.
DRY (Don't Repeat Yourself). Si riferisce al concetto secondo il quale non ci debbano essere duplicazioni in una applicazione Rails. Ogni idea deve essere espressa una sola volta. Questo obiettivo viene raggiunto utilizzando il linguaggio Ruby.
Convention Over Configuration. Questo concetto è forse il più importante tra quellli qui elencati. Si riferisce al fatto che Rails ha "sensible default" per pressoché ogni componente dell'applicazione. Questo implica che il lavoro principale si riduce a mettere insieme i pezzi piuttosto che programmare. Negli esempi che seguiranno cercheremo di mettere in evidenza le implicazioni di tale concetto.
Agility
Il concetto fa parte della struttura di Rails e quindi non si parlerà, come avremmo fatto nel caso di un contesto Java, del ricorso alle metodologie e agli strumenti necessari a raggiungere tal fine (vedi TDD, Ant, JUnit e quant'altro). Nella struttura del framework il concetto è già implicito con una serie di strumenti che orientano (e spingono quasi) il programmatore a questo approccio.
Dal punto di vista architetturale RoR non si discosta troppo da una classica applicazione Web basata su di un framework Java tipo Struts. Se a questo aggiungiamo poi uno strumento ORM (tipo Hibernate) per il mapping del DB, le differenze si limitano al linguaggio e ad alcune convenzioni specifiche del framework. Ma vediamo in dettaglio.
Installiamo RoR
Supponendo di avere Ruby installato e funzionante sulla nostra macchina, apriamo una finestra DOS (alternativamente, una shell e i comandi sono equivalenti) e digitiamo:
gem --version
Se come output ci viene restituita, senza alcun errore, la versione di RubyGem (p.e. 0.9.0) possiamo utilizzarlo per installare Rails. Digitiamo (eventualmente con un utente con i necessari permessi):
gem install rails --include-dependencies
Attendiamo che l'installazione termini ed è tutto fatto. Digitiamo:
rails -v
Hello World Application
Muoviamoci nella cartella nella quale vogliamo creare la nostra prima applicazione con RoR e digitiamo:
rails hello
L'operazione crea, automaticamente, una cartella di nome hello e una serie di sottocartelle e files che andremo ad analizzare nel corso della trattazione.
<< figura1.ext - Figura 1 - File di progetto generati >>
<< figura1.ext - Figura 1 - File di progetto generati >>
Così facendo abbiamo reso disponibili tutte le risorse necessarie allo sviluppo dell'applicazione. Editando il file README immediatamente otteniamo un discreto numero di informazioni relative sia alla struttura ed utilizzo della directory del progetto creato, sia delle varie opzioni utilizzabili in ambiente di sviluppo, test e produzione. Concentriamoci sulla cartella app. All'interno di questa è contenuto il source code dell'applicazione. Strutturata in sottocartelle dal nome parlante, qui ritroviamo:
1. controllers che contiene i controller dell'applicazione, classi che dovrebbero estendere ApplicationController 2. models dove sono contenuti i model, mapping delle tabelle del database generate con il tool ORM ActiveRecord e che discendono da ActiveRecord::Base 3. views che contiene i file .rhtml per la view dell'applicazione 4. helpers in cui troviamo gli helpers della componente view
I binari si iniziano ad intravedere. Torniamo al livello superiore della gerarchia di cartelle e focalizziamoci su config. All'interno troviamo il file database.yml. Editiamo il file.
<< figura2.ext - Figura 2 - File database.yml in config >>
<< figura2.ext - Figura 2 - File database.yml in config >>
Troviamo di default la configurazione dei tre ambienti classici (development, test, production) per MySQL. Rails pensa già a gestire le tre alternative classiche del processo. Lo vedremo a seguire quando utilizzeremo un database per la nostra applicazione. Ovviamente possiamo cambiare DBMS in funzione delle nostre esigenze. Nel prosieguo utilizzeremo MySQL in quanto sono presenti nell'installazione base di Rails dei driver (non particolarmente performanti ma adatti allo sviluppo e alla didattica, ma da sostituire per ambienti di esercizio) che possiamo utilizzare per essere velocemente produttivi. Nella cartella script presente nella root del progetto è presente lo script server. Questo consente di lanciare un Web Server scritto in Ruby (WEBrick), embedded nei pacchetti base, ideale per l'ambiente di sviluppo. Quindi digitiamo:
ruby script/server
Vedremo in output qualcosa del tipo:
=> Booting WEBrick... => Rails application started on http://0.0.0.0:3000 => Ctrl-C to shutdown server; call with --help for options ...
Apriamo il browser e puntiamo a http://localhost:3000 e otteniamo qualcosa del tipo:
<< figura3.ext - Figura 3 - Pagina di default >>
<< figura3.ext - Figura 3 - Pagina di default >>
Quindi il nostro web server è utilizzabile per la nostra prima demo. Creiamo un controller di nome Say. A tal fine utilizziamo lo script generate (presente nella cartella script):
ruby script/generate controller Say
<< figura4.ext - Figura 4 - Output dell'esecuzione script >>
<< figura4.ext - Figura 4 - Output dell'esecuzione script >>
Nella cartella app/controllers troviamo un file say_controller.rb che contiene il seguente source:
class SayController < ApplicationController end
Aggiungiamo la nostra prima action. Come?
class SayController < ApplicationController def helloworld end end
Per invocare ciò che abbiamo prodotto, digitiamo nel browser http://localhost:3000/say/helloworld e richiamiamo la pagina. Otteniamo un errore in quanto non ci siamo ancora occupati del front end.
<< figura5.ext - Figura 5 - Non è presente la pagina helloworld.rhtml >>
<< figura5.ext - Figura 5 - Non è presente la pagina helloworld.rhtml >>
Andiamo nella cartella app/views/say e creiamo un file helloworld.rhtml con il seguente più che basico contenuto HTML:
<html> <body>Hello World</body> </html>
Invochiamo di nuovo http://localhost:3000/say/helloworld. A questo punto tutto dovrebbe essere a posto e dovremmo poter visualizzare la nostra "interessante" pagina html.
A questo punto risultano essere d'obbligo alcune considerazioni relativamente ai binari imposti da Rails. Notiamo che il controller si chiama say e la action helloworld. La classe controller sarà SayController e il metodo in questa contenuto si chiama helloworld. La view sarà a questo punto helloworld.rhtml e sarà contenuta nella cartella views\say . Nulla da eccepire, anzi molto comodo in quanto ci fa concentrare sull'implementazione delle funzionalità lasciando al framework ogni altro dettaglio relativo all'architettura e all'interazione tra le varie componenti. Se vogliamo fare un veloce confronto con Struts, il framework per applicazioni web Java più utilizzato, notiamo come in Rails non sono presenti file (xml) di configurazione (struts.xml) con il fine di mettere insieme le varie componenti dell'applicazione (realizzate in maniera indipendente) ma ci si basa piuttosto sulla semantica. Ovviamente tutte le convenzioni utilizzate sono suscettibili di override.
Scaffold application
Passiamo ad un esempio un po' più interessante. Andiamo a realizzare un'applicazione Web che interagisce (in modalità CRUD) con i dati presenti in una tabella di una base dati rendendo dinamiche le pagine .rhtml, al momento utilizzate come se fossero statiche. Utilizzeremo per far questo una delle opzioni tra quelle disponibili nello script generate.
Creiamo un nuovo progetto ripetendo l'operazione già fatta in precedenza:
rails mydynamicapplication
In output avremo ottenuto la creazione della solita alberatura con tutti i file all'interno. Usiamo MySQL per creare i nostri tre database (sviluppo, test, produzione) necessari all'applicazione. Li chiameremo countriesdevelopment, countriestest, countries_production. Queste informazioni, con relative user e password di accesso, saranno configurati in config/database.yml come accennato in precedenza. Nella cartella DB vanno gli script del database. Creiamo un file create.sql con il seguente contenuto e poniamolo nella cartella DB:
DROP TABLE countries IF EXISTS;
CREATE TABLE countries (
id int(11) NOT NULL auto_increment,
name varchar(128) collate utf8_bin NOT NULL,
continent enum('Asia','Europe','Africa','Oceania','America') collate utf8_bin NOT NULL,
PRIMARY KEY (id)
);
Lanciamo lo script per creare la tabella nel database di sviluppo.
Quindi facciamo generare la nostra "scaffold application". Di che cosa si tratta? Si tratta di una vera e propria applicazione generata dal framework a cui stiamo dicendo "voglio un controller che mi gestisca un model". Eseguiamo l'operazione di generazione:
ruby script/generate scaffold Country Admin
che corrisponde al più generico:
ruby script/generate scaffold [model] [controller]
Se l'operazione è andata a buon fine, abbiamo generato tutto il necessario per gestire il contenuto della tabella Country via web. Lanciamo WEBrick e invochiamo tramite browser http://localhost:3000/admin. Al di là della gradevolezza del front-end con cui interagiamo, abbiamo disponibile un'applicazione web con le 4 operazioni di base di interfacciamento alla base dati già disponibili.
<< figura6.ext - Figura 6 - Pagina di default della scaffold application >>
<< figura6.ext - Figura 6 - Pagina di default della scaffold application >>
Clicchiamo su "New Country" e provvediamo ad inserire la nostra prima nazione. Questa, dopo aver effettuato l'operazione di inserimento, comparirà nella lista delle nazioni unita ai relativi link di "Show", "Edit", "Destroy" del record. Velocissimo! Andiamo a vedere cosa è accaduto dietro le quinte. Partiamo dal core e analizziamo il file admin_controller.rb:
class AdminController < ApplicationController
def index
list
render :action => 'list'
end
# GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
verify :method => :post, :only => [ :destroy, :create, :update ],
:redirect_to => { :action => :list }
def list
@country_pages, @countries = paginate :countries, :per_page => 10
end
def show
@country = Country.find(params[:id])
end
def new
@country = Country.new
end
def create
@country = Country.new(params[:country])
if @country.save
flash[:notice] = 'Country was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
def edit
@country = Country.find(params[:id])
end
def update
@country = Country.find(params[:id])
if @country.update_attributes(params[:country])
flash[:notice] = 'Country was successfully updated.'
redirect_to :action => 'show', :id => @country
else
render :action => 'edit'
end
end
def destroy
Country.find(params[:id]).destroy
redirect_to :action => 'list'
end
end
Ovviamente il nostro controller AdminController estende ApplicationController e contiene i metodi per effettuare le varie operazioni previste dall'applicazione (list, show, create, update, destroy). Inoltre troviamo il metodo invocato di default index che ridireziona all'utilizzo di list. Proseguiamo con country.rb presente in model:
class Country < ActiveRecord::Base end
Questo è quanto troviamo nel file. Questo è quanto è necessario per mappare la tabella del database di nome Country e rendere disponibili le operazioni di base (CRUD). Dal punto di vista architetturale Rails utilizza ovviamente il pattern ActiveRecord. Questo significa che si tratta di oggetti che, oltre a contenere le informazioni presenti in una tabella o vista del database, incapsulano anche la logica di accesso al DB nonchè tutte le operazioni di recupero/aggiornamento. Se vogliamo proprio trovare un neo all'approccio, potrebbe essere il fatto che questa operazione viene effettuata in maniera "implicita". Questo significa che se voglio conoscere i campi presenti nella base dati devo accedere alla base dati e non posso far riferimento al codice generato. A questo punto risulta d'obbligo qualche considerazione in merito alla scaffold application. Ovviamente questa utility presente in Rails non consentirà di risolvere in automatico i problemi degli sviluppatori facendogli credere che "pensa a tutto la generazione automatica del codice". L'utility risulta essere molto... utile, in particolare per applicazione di backend finalizzate alla manutenzione di tabelle della base dati e per dare un feeDBack immediato (ma tutt'altro che definitivo) al cliente. In tutti gli altri casi sarà necessario provvedere alla realizzazione di componenti più personalizzate e meno automatizzabili.
Testing
Passiamo ad un altro punto di forza. Dalla root del progetto ci spostiamo nella cartella test. Vediamo cosa ci ha messo a disposizione Rails per la fase di testing.
<< figura7.ext - Figura 7 - Il contenuto della cartella test >>
<< figura7.ext - Figura 7 - Il contenuto della cartella test >>
Ogni volta che lanciamo lo script generate, Rails integra gli stub con unit test (per il model) e functional test (per il controller).
Occupiamoci di unit test. Editiamo country_test.rb:
require File.dirname(__FILE__) + '/../test_helper'
class CountryTest < Test::Unit::TestCase
fixtures :countries
# Replace this with your real tests.
def test_truth
assert true
end
end
Abbiamo la nostra classe che estende Test::Unit::TestCase già pronta e in attesa del codice dei nostri test. A questo punto dobbiamo effettuare due operazioni: predisporre il database di test e introdurre la verifica da effettuare nel codice.
A riguardo della prima operazione lanciamo lo script di creazione delle tabelle DB/create.sql per il database countries_test. Oppure ricorriamo all'utilizzo di rake, strumento più che idoneo a tal fine:
rake db:test:clone_structure
La struttura del DB di sviluppo risulterà così copiata nel DB di test. È molto importante che il DB di test sia configurato con attenzione in quanto l'esecuzione dei test elimina i dati immessi nel corso della verifica riportando il DB alla situazione originaria. Popoliamo il database di test. Utilizziamo lo strumento dei fixture. Ci spostiamo nella cartella test/fixtures. Qui troviamo un file countries.yml nel quale immettiamo i dati che utilizzeremo nel test. Come esempio il seguente potrebbe essere il contenuto del file:
one: id: 1 name: Albania continent: Europe
A questo punto modifichiamo il file country_test.rb aggiungendo il codice di setup per il recupero del record dalla tabella Country:
def setup @country = Country.find(1) end
e un test di verifica della creazione record:
def test_create assert_kind_of Country, @country assert_equal 1, @country.id end
Salviamo il tutto e eseguiamo il test:
ruby test/unit/country_test.rb
Il risultato sarà, se tutto va a buon fine:
Loaded suite test/unit/country_test Started ... 1 tests, 2 assertions, 0 failures, 0 errors
Non ci resta che continuare con tutti i test di cui abbiamo bisogno.
Conclusioni
Cosa abbiamo fatto oggi? Siamo passati da Ruby a Ruby On Rails installandolo con RubyGem, abbiamo creato un primo progetto statico, quindi un'applicazione dinamica mediante lo strumento di generazione del codice presente nel framework (creando una scaffold application), abbiamo analizzato una parte dell'alberatura disponibile nel progetto, abbiamo accennato a rake, eseguito degli unit test su una delle operazioni oggetto della generazione. Abbiamo quindi molti spunti per provare e approndire... in attesa del prossimo articolo. E i codici sorgenti degli esempi sono disponibili nell'allegato in download a partire dal menu in alto a sinistra.
Riferimenti
- [1] Dave Thomas, "Agile Software Development with Rails. The Pragmatic Programmer's Guide", Pragmatic Bookshelf, 2005 http://www.pragmaticprogrammer.com
- [2] Patrick Peak, "Hibernate vs Rail: La sfida sulla persistenza" (traduzione di Carlo Possati) http://www2.mokabyte.it/cms/article.run?articleId=C1Q-6IT-KV5-JQ5
