Molte persone imparano Ruby partendo dal framework Rails e, purtroppo, questo è il peggior modo possibile di imparare questo linguaggio. Non fraintendetemi: Rails è ottimo, aiuta a costruire applicazioni web in modo rapido ed efficiente senza dover entrare in molti dettagli tecnici.
Piacere di conoscerti!
Molte persone imparano Ruby partendo dal framework Rails e, purtroppo, questo è il peggior modo possibile di imparare questo linguaggio. Non fraintendetemi: Rails è ottimo, aiuta a costruire applicazioni web in modo rapido ed efficiente senza dover entrare in molti dettagli tecnici. Fornisce un sacco di "magia Rails" che fa funzionare le cose in modo semplice. E per un programmatore alle prime armi questo è davvero fantastico, perché il momento più piacevole del processo è quando si può dire "è vivo!", e vedere che tutte le parti si incastrano e che le persone usano la vostra applicazione. Ci piace essere "creatori" 🙂 Ma c'è una cosa che distingue i bravi programmatori dalla media: quelli bravi capiscono come funzionano gli strumenti che usano. E per "capire gli strumenti" non intendo conoscere tutti i metodi e i moduli forniti da un framework, ma capire come funziona, capire come avviene la "magia Rails". Solo così potrete sentirvi a vostro agio nell'uso degli oggetti e nella programmazione con Rails. Il fondamento della programmazione orientata agli oggetti, e l'arma segreta che rende più semplice la complicata applicazione Rails, è il già citato titolo PORO, ovvero Plain Old Ruby Object
Cosa si nasconde davvero sotto questo nome? Qual è questa grande arma segreta? È una semplice classe Ruby che non eredita da nulla. Sì, proprio questo e molto altro.
classe AwesomePoro
fine
Come posso aiutarvi?
Sviluppate continuamente la vostra applicazione e aggiungete nuove funzionalità, mentre il numero di utenti e le loro aspettative crescono. Si arriva al punto in cui si incontrano sempre più luoghi oscuri di logica estremamente contorta, luoghi che vengono evitati come la peste anche dagli sviluppatori più coraggiosi. Più sono questi luoghi, più è difficile gestire e sviluppare l'applicazione. Un esempio standard è l'azione di registrazione di un nuovo utente, che innesca un intero gruppo di altre azioni associate a questo evento:
- controllare l'indirizzo IP in un database di spam,
- inviare un'e-mail al nuovo utente,
- aggiungere un bonus all'account di un utente raccomandante,
- creazione di account nei servizi correlati,
- e molti altri...
Un campione codice responsabile della registrazione dell'utente potrebbe essere così:
classe RegistrationController < ApplicationController
def creare
user = User.new(registration_params)
if user.valid? && ip_valid?(registration_ip)
user.save!
utente.add_bonus
user.synchronize_related_accounts
utente.invia_email
fine
fine
fine
Ok, il codice è stato codificato, tutto funziona, ma... tutto questo codice va davvero bene? Forse potremmo scriverlo meglio? Prima di tutto, infrange il principio di base della programmazione: la responsabilità singola, quindi sicuramente potremmo scriverlo meglio. Ma come? È qui che viene in aiuto il già citato PORO. È sufficiente separare una classe RegistrationService, che sarà responsabile di una sola cosa: notificare tutti i servizi correlati. Per servizi intendiamo le singole azioni che abbiamo già individuato in precedenza. Nello stesso controllore è sufficiente creare un oggetto RegistrationService e richiamare su di esso il metodo "fire!". Il codice è diventato molto più chiaro, il nostro controllore occupa meno spazio e ciascuna delle classi appena create è ora responsabile di una sola azione, per cui possiamo facilmente sostituirle in caso di necessità.
classe RegistrationService
def fire!(params)
user = User.new(params)
se user.valid? && ip_validator.valid?(registration_ip)
user.save!
dopo_registrazione_eventi(utente)
fine
utente
fine
privato
def after_registered_events(utente)
BonusCreator.new.fire!(utente)
AccountsSynchronizator.fire!(utente)
EmailSender.fire!(utente)
fine
def ip_validator
@ip_validator ||= IpValidator.new
end
fine
classe RegistrationController < ApplicationController
def creare
utente = RegistrationService.new.fire!(registration_params)
fine
fine
Tuttavia, i Plain Old Ruby Object possono rivelarsi utili non solo per i controllori. Immaginate che l'applicazione che state creando utilizzi un sistema di fatturazione mensile. Il giorno esatto della creazione di tale fatturazione non è importante per noi, abbiamo solo bisogno di sapere che riguarda un mese e un anno specifici. Naturalmente è possibile impostare il giorno per il primo giorno di ogni mese e memorizzare questa informazione nell'oggetto della classe "Date", ma non si tratta di un'informazione vera e propria, né è necessaria nella vostra applicazione. Utilizzando PORO è possibile creare una classe "MonthOfYear", i cui oggetti memorizzeranno esattamente le informazioni necessarie. Inoltre, applicandovi il modulo "Comparable", sarà possibile iterare e confrontare i suoi oggetti, proprio come quando si utilizza la classe Date.
classe MeseAnno
include Comparabile
attr_reader :year, :month
def initialize(mese, anno)
raise ArgumentError unless month.between?(1, 12)
@anno, @mese = anno, mese
fine
def (altro)
[anno, mese] [altro.anno, altro.mese]
fine
fine
Presentatemi Rails.
Nel mondo Rails, siamo abituati al fatto che ogni classe è un modello, una vista o un controllore. Esse hanno anche una loro precisa collocazione nella struttura delle directory, quindi dove si può mettere il nostro piccolo esercito PORO? Consideriamo alcune opzioni. Il primo pensiero che viene in mente è: se le classi create non sono né modelli, né viste, né controllori, dovremmo metterle tutte nella directory "/lib". In teoria, è una buona idea, ma se tutti i file PORO finiscono in un'unica directory e l'applicazione è di grandi dimensioni, questa directory diventerà rapidamente un luogo oscuro che si ha paura di aprire. Pertanto, senza dubbio, non è una buona idea.
AwesomeProject
├──app
│ ├─controllori
│ ├─modelli
│ └─visualizzazioni
│
└─lib
└─servizi
1TP63Poro alto qui
Si possono anche chiamare alcune classi non-ActiveRecord Models e metterle nella cartella "app/models", mentre quelle responsabili della gestione di altre classi possono essere chiamate services e metterle nella cartella "app/services". Questa è una soluzione abbastanza buona, ma ha un inconveniente: quando si crea un nuovo PORO, ogni volta si deve decidere se si tratta più di un modello o di un servizio. In questo modo, si può arrivare a una situazione in cui si hanno due luoghi oscuri nella propria applicazione, solo più piccoli. Esiste anche un terzo approccio, ovvero l'uso di classi e moduli namespace. È sufficiente creare una cartella con lo stesso nome di un controllore o di un modello e inserirvi tutti i file PORO utilizzati dal controllore o dal modello in questione.
AwesomeProject
├──app
│ ├─controllori
│ │ ├─controller_di_registrazione
│ │ │ └─registration_service.rb
│ │ └─registration_controller.rb
│ ├─modelli
│ │ ├─settlement
│ │ │ └─month_of_year.rb
│ │ └─settlement.rb
│ └─views
│
└─lib
Grazie a questa disposizione, quando la si usa, non è necessario far precedere il nome di una classe da uno spazio dei nomi. Si ottiene un codice più breve e una struttura di cartelle più logicamente organizzata.
Guardatemi!
È una piacevole sorpresa che, utilizzando PORO, i test unitari dell'applicazione siano più veloci e facili da scrivere e, in seguito, più facilmente comprensibili da altri. Poiché ogni classe è ora responsabile di una sola cosa, è possibile riconoscere prima le condizioni limite e aggiungere facilmente scenari di test appropriati.
descrivere MonthOfYear do
soggetto { MonthOfYear.new(11, 2015) }
it { should be_kind_of Comparable }
descrivere "creare una nuova istanza" do
it "inizializza con l'anno e il mese corretti" do
aspettarsi che { described_class.new(10, 2015) }.to_not raise_error
fine
it "solleva un errore quando il mese dato non è corretto" do
expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
fine
fine
fine
Spero che ci incontreremo di nuovo!
Gli esempi presentati mostrano chiaramente che l'uso di PORO migliora la leggibilità delle applicazioni e le rende più modulari e, di conseguenza, più facili da gestire ed espandere. L'adozione del principio della responsabilità unica facilita lo scambio di particolari classi, se necessario, e lo fa senza interferire con altri elementi. Inoltre, il collaudo diventa una procedura più semplice e veloce. Inoltre, in questo modo è molto più facile mantenere corti i modelli e i controllori di Rails, che come tutti sappiamo tendono a diventare inutilmente grandi nel corso dello sviluppo.