Rails Best Practices 2: Spostare la logica nel model

Continuiamo la nostra analisi delle Rails Best Practices.
Nel post precedente abbiamo visto Named Scope, Model Association e Scope Access come metodi per spostare la logica dell’applicazione dai controller ai model.

Proseguendo nella stessa direzione oggi prendiamo in esame l’utilizzo di Model Callback e di Virtual Attribute.

1. Virtual Attribute

Supponiamo di avere una tabella di anagrafica clienti definita come segue

1
2
3
4
5
6

create_table "clients", :force => true do |t|
t.string "first_name"
t.string "last_name"
t.string "street"
t.string "city"
end

ma di voler però definire una maschera di input dove street e city siano raggruppati in uno solo campo chiamato “address”.
La form con il campo “address” sarà definita come segue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<h1>Create Client</h1>
<% form_for @client do |f| %>
<ol class="formList">
<li>
<%= f.label :first_name, 'First Name' %>
<%= f.text_field :first_name %>
</li>
<li>
<%= f.label :last_name, 'Last Name' %>
<%= f.text_field :last_name %>
</li>
<li>
<%= f.label :address, 'Address' %>
<%= text_field_tag :address %>
</li>
</ol>
<% end %>

A questo punto nel metodo create avremo qualcosa del tipo:

1
2
3
4
5
6
7
8
9
10
11
12

class ClientsController < ApplicationController

def create
@client = Client.new(params[:client])
@client.street = params[:address].split(' ', 2).first
@client.city = params[:address].split(' ', 2).last
@client.save
end

...

end

Possiamo ora migliorare questo metodo definendo il campo address come attributo virtuale del model Client.

1
2
3
4
5
6
7
8
9
10
11
12

class Client < ActiveRecord::Base

def address
[street, city].join(' ')
end

def address=(addr)
split = addr.split(' ', 2)
self.street = split.first
self.city = split.last
end
end

Nella form possiamo ora definire il campo address come f.field_tag anzichè come text_field_tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<h1>Create Client</h1>
<% form_for @client do |f| %>
<ol class="formList">
<li>
<%= f.label :first_name, 'First Name' %>
<%= f.text_field :first_name %>
</li>
<li>
<%= f.label :last_name, 'Last Name' %>
<%= f.text_field :last_name %>
</li>
<li>
<%= f.label :address, 'Address' %>
<%= f.text_field :address %>
</li>
</ol>
<% end %>

Infine, il controller diventerà più semplice, ovvero:

1
2
3
4
5
6
7
8
9
10

class ClientsController < ApplicationController

def create
@client = Client.new(params[:client])
@client.save
end

...

end

2. Model Callback
Vediamo ora un metodo, detto Model Callback che ci permette, come con il virtual attribute appena visto, di semplificare form e controller spostando parte della logica nel model.

Supponiamo di voler implementare una funzionalità che associ automaticamente una serie di tag ad un post.
Questa funzione, detta calculate_tags ritornerà un elenco di tag in base alle parole più frequenti contenute nel post.
Diamo per scontato il codice di questa funzione e vediamo invece come e quando invocare la generazione automatica dei tag.

Una prima implementazione può essere la seguente. Data la seguente form:

1
2
3
4

<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= check_box_tag 'calculate_tags' %>
<% end %>

il corrispondente metodo create sarà:

1
2
3
4
5
6
7
8
9
10

class PostController < ApplicationController
def create
@post = Post.new(params[:post])
if params[:calculate_tags] == '1'
@post.tags = TagGenerator.generate(@post.content)
else
@post.tags = ""
end
@post.save
end

Modificando il model Post, vediamo ora come migliorare il codice appena scritto.
Per prima cosa introduciamo nel nostro model un attributo calculate_tags ed un filtro di tipo before_save chiamato generate_tags

1
2
3
4
5
6
7
8
9
10

class Post < ActiveRecord::Base
attr_accessor :calculate_tags
before_save :generate_tags

private
def generate_tags
return unless calculate_tags == '1'
self.tags = TagGenerator.generate(self.content)
end
end

Tornando alla form possiamo ora ridefinire il campo calculate_tags come f.check_box

1
2
3
4

<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= f.check_box 'calculate_tags' %>
<% end %>

Infine il nostro metodo create nel controller tornerà alla sua forma più semplice.

1
2
3
4
5
6

class PostController < ApplicationController
def create
@post = Post.new(params[:post])
@post.save
end
end