Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Solid design for Rails applications

Solid design for Rails applications

My presentation at the Italian Ruby Day 2011

Matteo Vaccari

June 10, 2011
Tweet

More Decks by Matteo Vaccari

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. 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
  4. Verbs and nouns • GET • POST • PUT •

    DELETE mailto:[email protected] http://matteo.vaccari.name/blog http://matteo.vaccari.name/blog/123 http://matteo.vaccari.name/blog/2007-05
  5. REST and CRUD • GET • POST • PUT •

    DELETE • index, show, new, edit • create • update • destroy • SELECT • INSERT • UPDATE • DELETE
  6. RPC cart: show, add_item, remove_item, add_coupon, remove coupon, increase_quantity, decrease_quantity

    REST cart: show, create, update, destroy cart_items: show, create, update, destroy cart_coupons: show, create, update, destroy Move variation from verbs to nouns Embrace REST
  7. 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.
  8. Connect the dots... • REST thinking reduces complex domains to

    CRUDs • Rails makes it easy to do CRUDs • Rails makes it easy to do REST ➡ €€€ !!!
  9. 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_something else do_something_else end
  10. STORES_CONFIGURATION[:foo] = { :tracking_email_enabled => true, :simple_agency_tracking_enabled => true, :remote_user_login

    => false, ... } if current_store_config[:tracking_email_enabled] do_something else do_something_else end 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
  11. 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(k ], :joins => join_with_translations_table } end } private def self.whitelist_characters_for_search(keywords) # ... end
  12. 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
  13. 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
  14. Eventually... class Product < ActiveRecord::Base extend ProductFinders include ProductCategoryMethods include

    TranslationEnumerator include ProductImages end Vedi Rails Antipatterns by Pytel & Saleh
  15. Tedium it "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" end end <table> <tr class="even"> <td><strong>First Name</strong></td> <td id="user_first_name"><%= @user.first_name %></td> </tr> </table> Red Green
  16. 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" end end 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>
  17. <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]" end end Red Green Red Green Red Green aaaaaaaaagh!!
  18. <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!!!
  19. it "displays information for a given user" do Factory.create(:user, :id

    => "1234", :first_name => "Arthur", :last_name => "Fonzarelli", :email => "[email protected]", :street => "123 foob :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" end end Duplication!!! Boredom!!! No objects emerge. No abstraction. No creativity. How sad.
  20. 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"!
  21. <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 %>
  22. 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_html end <%= AdminTable.new(@user.attributes_for_administration).to_html %>
  23. 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
  24. 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!!!
  25. Want to know more? The Clean Code Talks #2 The

    Clean Code Talks - Don't Look For Things! Miško Hevery