solid design for rails

39
Solid Design for Rails applications Matteo Vaccari [email protected] www.xpeppers.com Italian Ruby Day, 2011/06/10 (cc) Some rights reserved

Upload: matteo-vaccari

Post on 06-May-2015

3.325 views

Category:

Technology


0 download

DESCRIPTION

My presentation at the Italian Ruby Day

TRANSCRIPT

Page 1: Solid design for Rails

Solid Design for Rails applications

Matteo Vaccari

[email protected]

Italian Ruby Day, 2011/06/10(cc) Some rights reserved

Page 2: Solid design for Rails

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

Page 3: Solid design for Rails

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

Page 4: Solid design for Rails

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

Page 5: Solid design for Rails

What’s the problem?

• Maintainability

• Long-term maintainability

• But, in general, maintainability

Page 6: Solid design for Rails

In a good project...

• The cost of delivering features decreases over time

Page 7: Solid design for Rails

Idea #0: embrace REST

Page 8: Solid design for Rails

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

Page 9: Solid design for Rails

REST and CRUD

• GET

• POST

• PUT

• DELETE

• index, show, new, edit

• create

• update

• destroy

• SELECT

• INSERT

• UPDATE

• DELETE

Page 10: Solid design for Rails

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

Page 11: Solid design for Rails

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.

Page 12: Solid design for Rails

Connect the dots...

• REST thinking reduces complex domains to CRUDs

• Rails makes it easy to do CRUDs

• Rails makes it easy to do REST

➡ €€€ !!!

Page 13: Solid design for Rails

Idea #1: embrace OOP

Page 14: Solid design for Rails

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

Page 15: Solid design for Rails

OK. Nessuno te l’ha detto finora ma...

Aggiungere IF è il male.

Page 16: Solid design for Rails

COMODO ≠ EFFICACEhttp://www.antiifcampaign.com/

Francesco Cirillo

Page 17: Solid design for Rails

http://pierg.wordpress.com/2009/08/05/anti-if-campaign/

Page 18: Solid design for Rails

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

Page 19: Solid design for Rails

Fat models

class Product < ActiveRecord::Base # ... 363 lines ...end

Page 20: Solid design for Rails

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

Page 21: Solid design for Rails

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

Page 22: Solid design for Rails

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

Page 23: Solid design for Rails

Eventually...

class Product < ActiveRecord::Base extend ProductFinders include ProductCategoryMethods include TranslationEnumerator include ProductImages end

Vedi Rails Antipatterns by Pytel & Saleh

Page 24: Solid design for Rails

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

Page 25: Solid design for Rails

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>

Page 26: Solid design for Rails

<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!!

Page 27: Solid design for Rails

<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!!!

Page 28: Solid design for Rails

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.

Page 29: Solid design for Rails

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"!

Page 30: Solid design for Rails

<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%>

Page 31: Solid design for Rails

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%>

Page 32: Solid design for Rails

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

Page 33: Solid design for Rails

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!!!

Page 34: Solid design for Rails

Want to know more?

Page 35: Solid design for Rails

Want to know more?

The Clean Code Talks #2

The Clean Code Talks - Don't Look For Things!

Miško Hevery

Page 36: Solid design for Rails

Want to know more?

Read chapter one!

Page 37: Solid design for Rails

Want to know more?

http://www.antiifcampaign.com/

Page 38: Solid design for Rails

Want to know more?

http://matteo.vaccari.name/blog/

[email protected]: @xpmatteo

This presentation can be downloaded from http://slideshare.net/xpmatteo

Page 39: Solid design for Rails

Extreme Programming:development & mentoring

Grazie dell’attenzione!