solid design for rails
DESCRIPTION
My presentation at the Italian Ruby DayTRANSCRIPT
Solid Design for Rails applications
Matteo Vaccari
Italian Ruby Day, 2011/06/10(cc) Some rights reserved
Revelations
• 1977 - listato BASIC su rivista di elettronica
• 1984 - prima login su Unix
• 1994 - lezione di Dijkstra
• 2004 - Extreme Programming!
• 2005 - Ruby on Rails
• 2009 - Object-Oriented Design
There are a few things I look for that are good predictors of whether a project is in good shape.
Once and only once ...
Lots of little pieces - Good code invariably has small methods and small objects. Only by factoring the system into many small pieces of state and function can you hope to satisfy the “once and only once” rule. ...
Replacing objects - Good style leads to easily replaceable objects. In a really good system, every time the user says “I want to do this radically different thing,” the developer says, “Oh, I’ll have to make a new kind of X and plug it in.” ...
Kent Beck – Smalltalk Best Practice Patterns
Objects in a Rails project:
• Models (one per DB table)
• Helpers (one per controller)
• Controllers (~ one per DB table)
• Views (~ 7 * controller)
The number of objects is somehow fixed
Objects are rarely reusable
What’s the problem?
• Maintainability
• Long-term maintainability
• But, in general, maintainability
In a good project...
• The cost of delivering features decreases over time
Idea #0: embrace REST
Verbs and nouns
• GET
• POST
• PUT
• DELETE
mailto:[email protected]://matteo.vaccari.name/bloghttp://matteo.vaccari.name/blog/123http://matteo.vaccari.name/blog/2007-05
REST and CRUD
• GET
• POST
• PUT
• DELETE
• index, show, new, edit
• create
• update
• destroy
• SELECT
• INSERT
• UPDATE
• DELETE
RPCcart: show, add_item, remove_item, add_coupon, remove coupon, increase_quantity, decrease_quantity
RESTcart: show, create, update, destroycart_items: show, create, update, destroycart_coupons: show, create, update, destroy
Move variation from verbs to nouns
Embrace REST
Embrace REST
Scott Raymond, Refactoring to REST, 2006/6/20
Before refactoring, IconBuffet had 10 controllers and 76 actions. Now, without adding or removing any features, IconBuffet has 13 controllers and 58 actions.
There are seven standard Rails actions: index, new, create, show, edit, update, and destroy. Everything else—oddball actions—are usually a clue that you’re doing RPC.
Connect the dots...
• REST thinking reduces complex domains to CRUDs
• Rails makes it easy to do CRUDs
• Rails makes it easy to do REST
➡ €€€ !!!
Idea #1: embrace OOP
Boolean configurations bring IFs
STORES_CONFIGURATION[:foo] = { :tracking_email_enabled => true, :simple_agency_tracking_enabled => true, :remote_user_login => false, ...}
if current_store_config[:tracking_email_enabled] do_somethingelse do_something_elseend
OK. Nessuno te l’ha detto finora ma...
Aggiungere IF è il male.
COMODO ≠ EFFICACEhttp://www.antiifcampaign.com/
Francesco Cirillo
http://pierg.wordpress.com/2009/08/05/anti-if-campaign/
STORES_CONFIGURATION[:foo] = { :tracking_email_enabled => true, :simple_agency_tracking_enabled => true, :remote_user_login => false, ...}
if current_store_config[:tracking_email_enabled] do_somethingelse do_something_elseend
STORES[:foo] = Store.new( :email_tracker => EmailTracker.new, :agency_tracker => SimpleAgencyTracker.new ...)
STORES[:bar] = Store.new( :tracking_email => NullEmailTracker.new, ...)
current_store.email_tracker.do_something
Fat models
class Product < ActiveRecord::Base # ... 363 lines ...end
class Product < ActiveRecord::Base # ... named_scope :full_text_search, lambda { |keywords| if keywords.blank? { :conditions => "0 = 1"} else keywords = whitelist_characters_for_search(keywords) { :conditions => [ " products.code LIKE ? or match (product_translations.name_actual) against (? in boolean mode) or product_translations.name_actual regexp ? ", keywords + '%', expand_search_aliases(keywords), prepare_for_regexp_search(keywords) ], :joins => join_with_translations_table } end }
private
def self.whitelist_characters_for_search(keywords) # ... end
def self.prepare_for_regexp_search(keywords_string) # ... end
def self.expand_search_aliases(keywords_string) # ... end def self.expand_alias keywords, key, value # ... end
def self.join_with_translations_table # ... end
end
class Product < ActiveRecord::Base # ... named_scope :full_text_search, lambda { |keywords| if keywords.blank? { :conditions => "0 = 1"} else keywords = whitelist_characters_for_search(keywords) { :conditions => [ " products.code LIKE ? or match (product_translations.name_actual) against (? in boolean mode) or product_translations.name_actual regexp ? ", keywords + '%', expand_search_aliases(keywords), prepare_for_regexp_search(keywords) ], :joins => join_with_translations_table } end }
private
def self.whitelist_characters_for_search(keywords) # ... end
def self.prepare_for_regexp_search(keywords_string) # ... end
def self.expand_search_aliases(keywords_string) # ... end def self.expand_alias keywords, key, value # ... end
def self.join_with_translations_table # ... end
# ...
end
class Product < ActiveRecord::Base # ...
named_scope :full_text_search, lambda { |keywords| FullTextSearch.new(keywords).to_scope }
# ...end
class FullTextSearch def to_scope { :conditions => ... } end private
def whitelist_characters_for_search # ... end
# ... end
Cure: use composition
Eventually...
class Product < ActiveRecord::Base extend ProductFinders include ProductCategoryMethods include TranslationEnumerator include ProductImages end
Vedi Rails Antipatterns by Pytel & Saleh
Tediumit "displays information for a given user" do Factory.create(:user, :id => "1234", :first_name => "Arthur") get "/users/display?id=1234" assert_select "table" do assert_select "td#user_first_name", "Arthur" endend
<table> <tr class="even"> <td><strong>First Name</strong></td> <td id="user_first_name"><%= @user.first_name %></td> </tr></table>
Red
Green
it "displays information for a given user" do Factory.create(:user, :id => "1234", :first_name => "Arthur", :last_name => "Fonzarelli") get "/users/display?id=1234" assert_select "table" do assert_select "td#user_first_name", "Arthur" assert_select "td#user_last_name", "Fonzarelli" endend
Red
Green
<table> <tr class="even"> <td><strong>First Name</strong></td> <td id="user_first_name"><%= @user.first_name %></td> </tr> <tr class="odd"> <td><strong>Last Name</strong></td> <td id="user_last_name"><%= @user.last_name %></td> </tr></table>
<table> <tr class="even"> <td><strong>First Name</strong></td> <td id="user_first_name"><%= @user.first_name %></td> </tr> <tr class="odd"> <td><strong>Last Name</strong></td> <td id="user_last_name"><%= @user.last_name %></td> </tr> <tr class="even"> <td><strong>Email</strong></td> <td id="user_email"><%= @user.email %></td> </tr></table>
it "displays information for a given user" do Factory.create(:user, :id => "1234", :first_name => "Arthur", :last_name => "Fonzarelli", :email => "[email protected]") get "/users/display?id=1234" assert_select "table" do assert_select "td#user_first_name", "Arthur" assert_select "td#user_last_name", "Fonzarelli" assert_select "td#user_email", "[email protected]" endend
Red
Green
Red
Green
Red
Green
aaaaaaaaag
h!!
<table> <tr class="even"> <td><strong>First Name</strong></td> <td id="user_first_name"><%= @user.first_name %></td> </tr> <tr class="odd"> <td><strong>Last Name</strong></td> <td id="user_last_name"><%= @user.last_name %></td> </tr> <tr class="even"> <td><strong>Email</strong></td> <td id="user_email"><%= @user.email %></td> </tr></table>
Duplication!!!
Useless IDs!!!
it "displays information for a given user" do Factory.create(:user, :id => "1234", :first_name => "Arthur", :last_name => "Fonzarelli", :email => "[email protected]", :street => "123 foobar lane", ) :city => "Milwaukee", :zip => "99911", :phone => "1-234-5678")
get "/users/display?id=1234" assert_select "table" do assert_select "td#user_first_name", "Arthur" assert_select "td#user_last_name", "Fonzarelli" assert_select "td#user_email", "[email protected]" assert_select "td#user_street", "123 foobar lane" assert_select "td#user_city", "Milwaukee" assert_select "td#user_zip", "99911" assert_select "td#user_phone", "1-234-5678" endend
Duplication!!!Boredom!!!
No objects emerge.
No abstraction.
No creativity.
How sad.
The original definition of TDD says:
1.Quickly add a test.2.Run all tests and see the new one fail.3.Make a little change.4.Run all tests and see them all succeed.5.Refactor to remove duplication.
Kent Beck, Test Driven Development: By Example
Not "refactor at will"!
<table> <tr class="even"> <td><strong>First Name</strong></td> <td id="user_first_name"><%= @user.first_name %></td> </tr> <tr class="odd"> <td><strong>Last Name</strong></td> <td id="user_last_name"><%= @user.last_name %></td> </tr> <tr class="even"> <td><strong>Email</strong></td> <td id="user_email"><%= @user.email %></td> </tr></table>
<table> <%= admin_table_row "even", "First Name", @user.first_name %> <%= admin_table_row "odd", "Last Name", @user.last_name %> <%= admin_table_row "even", "Email", @user.email %></table>
<%= AdminTable.new(@user.attributes_for_administration).to_html%>
it "produces an html table" do model = [["Label 0", "value 0"]] expected = <<-EOF <table> <tr class="even"> <td><strong>Label 0</strong></td> <td>value 0</td> </tr> </table> EOF assert_dom_equal expected, AdminTable.new(model).to_htmlend
<%= AdminTable.new(@user.attributes_for_administration).to_html%>
A nonobvious conclusion
Solving a slightly more general problem than strictly necessary is often easier, simpler and cleaner!
See also George Pólya, How to solve it
Use every weapon
• Use objects in place of IFs
• Split models, delegate to objects and modules
• Refactor views! Use helpers everywhere
• Use plugins
• Develop your own DSL
• Think general! Not specific! Abstract!
• Have fun!!!
Want to know more?
Want to know more?
The Clean Code Talks #2
The Clean Code Talks - Don't Look For Things!
Miško Hevery
Want to know more?
Read chapter one!
Want to know more?
http://www.antiifcampaign.com/
Want to know more?
http://matteo.vaccari.name/blog/
[email protected]: @xpmatteo
This presentation can be downloaded from http://slideshare.net/xpmatteo
Extreme Programming:development & mentoring
Grazie dell’attenzione!