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

Maintainable_Rails_View.pdf

Yi-Ting Cheng
October 26, 2013
2.3k

 Maintainable_Rails_View.pdf

Yi-Ting Cheng

October 26, 2013
Tweet

Transcript

  1. Rails Developer since 2007 Speaker of RubyTaiwan Conf 2010,2011 Speaker

    of RubyChina Conf 2012, 2013 Speaker of Reddot RubyConf Singapore 2013 Grand Prize of Facebook World Hack (12 countries) 2012 13年10月27⽇日星期⽇日
  2. Agenda • Concept: What’s good view? • Helper best practices

    • Partial best practices • Beyond Helper & Partial • Object-oriented View 13年10月27⽇日星期⽇日
  3. Why best practices? • Large & complicated application • Team

    & different coding style 13年10月27⽇日星期⽇日
  4. Your View • Complex UI with logic UI 與 邏輯糾纏

    • Too long to maintain 冗⻑⾧長難以維護 • Low performance 效能低落 • Security issues 容易產⽣生安全性問題 13年10月27⽇日星期⽇日
  5. What’s Good code? • Readability 易讀,容易了解 • Flexibility 彈性,容易擴充 •

    Effective 效率,撰碼快速 • Maintainability 維護性,容易找到問題 • Consistency ⼀一致性,循慣例無須死背 • Testability 可測性,元件獨⽴立容易測試 13年10月27⽇日星期⽇日
  6. Move logic to helper <% if current_user && current_user ==

    post.user %> <%= link_to("Edit", edit_post_path(post))%> <% end %> Before ✘ 13年10月27⽇日星期⽇日
  7. Move logic to helper <% if editable?(post) %> <%= link_to("Edit",

    edit_post_path(post))%> <% end %> After 13年10月27⽇日星期⽇日
  8. Use Ruby in Helper ALL THE TIME Best Practice Lesson

    #3 全程在 Helper 裡⾯面使⽤用 Ruby 13年10月27⽇日星期⽇日
  9. Use Ruby in Helper ALL THE TIME def render_post_taglist(post, opts

    = {}) # .... raw tags.collect { |tag| "<a href=\"#{posts_path(:tag => tag)}\" class=\"tag\">#{tag}</a>" }.join(", ") end Before double quote issue ✘ 13年10月27⽇日星期⽇日
  10. Use Ruby in Helper ALL THE TIME def render_post_taglist(post, opts

    = {}) # .... raw tags.collect { |tag| "<a href='#{posts_path(:tag => tag)}' class='tag'>#{tag}</a>" }.join(", ") end Before single quote issue ✘ 13年10月27⽇日星期⽇日
  11. Use Ruby in Helper ALL THE TIME def render_post_taglist(post, opts

    = {}) # ... raw tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ") end After ✔ 13年10月27⽇日星期⽇日
  12. mix Helper & Partial Best Practice Lesson #4 混合使⽤用 Helper

    與 Partial 13年10月27⽇日星期⽇日
  13. mix Helper & Partial def render_post_title(post) str = “” str

    += “<li>” str += link_to(post.title, post_path(post)) str += “</li>” return raw(str) end Before XSS vulnerability ✘ 13年10月27⽇日星期⽇日
  14. def render_post_title(post) render :partial => "posts/title_for_helper", :locals => { :title

    => post.title } end After mix Helper & Partial ✔ 13年10月27⽇日星期⽇日
  15. Tell, Don’t ask def render_post_taglist(post, opts = {}) tags =

    post.tags tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ") end <% @posts.each do |post| %> <%= render_post_taglist(post) %> <% end %> Before Performance issue ✘ 13年10月27⽇日星期⽇日
  16. Tell, Don’t ask def render_post_taglist(tags, opts = {}) tags.collect {

    |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ") end <% @posts.each do |post| %> <%= render_post_taglist(post.tags) %> <% end %> def index @posts = Post.recent.includes(:tags) end After ✔ 13年10月27⽇日星期⽇日
  17. Wrap into a method Best Practice Lesson #6 資料儘量包裝成 method

    ⽽而⾮非放在 Helper 13年10月27⽇日星期⽇日
  18. Wrap into a method def render_comment_author(comment) if comment.user.present? comment.user.name else

    comment.custom_name end end Before 13年10月27⽇日星期⽇日
  19. Wrap into a method def render_comment_author(comment) comment.author_name end class Comment

    < ActiveRecord::Base def author_name if user.present? user.name else custom_name end end end After 13年10月27⽇日星期⽇日
  20. Best Practice Lesson #7 Move code to Partial view code

    超過兩⾴頁請注意 13年10月27⽇日星期⽇日
  21. Move Code to Partial • highly duplicated 內容⾼高度重複 • independent

    blocks 可獨⽴立作為功能區塊 13年10月27⽇日星期⽇日
  22. Use presenter to clean the view Best Practice Lesson #8

    使⽤用 Presenter 解決 login in view 問題 13年10月27⽇日星期⽇日
  23. Use presenter to clean the view <%= if user_rofile.has_experience? &&

    user_rofile.experience_public? %> <p><strong>Experience:</strong> <%= user_profile.experience %></p> <% end %> Before 13年10月27⽇日星期⽇日
  24. <% user_profile.with_experience do %> <p><strong>Experience:</strong> <%= user_profile.experience %></p> <% end

    %> <% user_profile.with_hobbies do %> <p><strong>Hobbies:<strong> <%= user_profile.hobbies %></p> <% end %> Use presenter to clean the view After 13年10月27⽇日星期⽇日
  25. Use presenter to clean the view class ProfilePresenter < ::Presenter

    def with_experience(&block) if profile.has_experience? && profile.experience_public? block.call(view) end end end After 13年10月27⽇日星期⽇日
  26. Cache Digest <% @project do %> aaa <% @todo do

    %> bbb <% @todolist do %> ccc <% end %> <% end %> <% end %> 13年10月27⽇日星期⽇日
  27. Cache Digest <% cache @project do %> aaa <% cache

    @todo do %> bbb <% cache @todolist do %> ccc <% end %> <% end %> <% end %> Difficult to invalid cache 13年10月27⽇日星期⽇日
  28. Cache Digest <% cache [v15,@project] do %> aaa <% cache

    [v10,@todo] do %> bbb <% cache [v45,@todolist] do %> zzz <% end %> <% end %> <% end %> invalid cache manually 13年10月27⽇日星期⽇日
  29. Cache Digest <% cache @todolist do %> zzz <% end

    %> md5_of_this_view 13年10月27⽇日星期⽇日
  30. Cache Digest <% cache @project do %> aaa <% cache

    @todo do %> bbb <% cache @todolist do %> ccc <% end %> <% end %> <% end %> Auto invalid 13年10月27⽇日星期⽇日
  31. Cells class UsersController < ApplicationController def show @user = User.find(params[:id])

    @recent_posts = @user.recent_posts.limit(5) @favorite_posts = @user.favorite_posts.limit(5) @recent_comments = @user.comments.limit(5) end end <%= render :partial => "users/recent_post", :collection => @recent_posts %> <%= render :partial => "users/favorite_post", :collection => @favorite_posts %> <%= render :partial => "users/recent_comment", :collection => @recent_comments %> Before 13年10月27⽇日星期⽇日
  32. What if ....? • each block cache expire in 3

    / 5 / 7 hours? • each block need to do different tweaks? 13年10月27⽇日星期⽇日
  33. Huge Mess • Long & Ugly Controller code • Bad

    performance in controller & view • Hard to cache 13年10月27⽇日星期⽇日
  34. Cells class UsersController < ApplicationController def show @user = User.find(params[:id])

    end end <%= render_cell :user, :rencent_posts, :user => @user %> <%= render_cell :user, :favorite_posts, :user => @user %> <%= render_cell :user, :recent_comments, :user => @user %> After 13年10月27⽇日星期⽇日
  35. Cells class UserCell < Cell::Rails cache :recent_posts, :expires_in => 1.hours

    cache :favorite_posts, :expires_in => 3.hours cache :recent_comments, :expires_in => 5.hours def recent_posts(args) ... end def favorite_posts(args) ... end def recent_comments(args) ... end end After ⽂文字 13年10月27⽇日星期⽇日
  36. content_for / yield Best Practice Lesson #11 jump to right

    location 13年10月27⽇日星期⽇日
  37. <%= stylesheet_link_tag "application" %> <%= yield %> <%= javascript_include_tag "application"

    %> yield Best Practices for Speeding Up Your Web Site put javascript at bottom 13年10月27⽇日星期⽇日
  38. yield main content here <script type= "text/javascript"> your script here

    </script> undefined javascript 13年10月27⽇日星期⽇日
  39. yield <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application" %> <%=

    yield %> <%= stylesheet_link_tag "application" %> <%= yield %> <%= javascript_include_tag "application" %> T_T 13年10月27⽇日星期⽇日
  40. yield <%= stylesheet_link_tag "application" %> <%= yield %> <%= javascript_include_tag

    "application" %> <%= yield :page_specific_javascript %> <%= stylesheet_link_tag "application" %> <%= yield %> <%= javascript_include_tag "application" %> ^_^ 13年10月27⽇日星期⽇日
  41. yield main content here <%= content_for :page_specific_javascript do %> <script

    type= "text/javascript"> your script here </script> <% end %> 13年10月27⽇日星期⽇日
  42. content_for / yield <div class="main"> main content </div> <div class="sidebar">

    <% case @ad_type %> <% when foo %> <%= render "ad/foo"%> <% when bar %> <%= render "ad/bar"%> <% else %> <%= render "ad/default"%> <% end %> </div> Before 13年10月27⽇日星期⽇日
  43. content_for / yield <div class="main"> <%= yield %> </div> <div

    class="sidebar"> <%= yield :sidebar %> </div> After 13年10月27⽇日星期⽇日
  44. content_for / yield main content <%= content_for :sidebar do %>

    <%= render "ad/foo"%> <% end %> After 13年10月27⽇日星期⽇日
  45. Decoration in Controller <%= content_for :meta do %> <meta content="xdite's

    blog" name="description"> <meta content="Blog.XDite.net" property="og:title"> <% end %> Before 13年10月27⽇日星期⽇日
  46. Decoration in Controller def show @blog = current_blog drop_blog_title @blog.name

    drop_blog_descption @blog.description end <%= stylesheet_tag "application" %> <%= render_page_title %> <%= render_page_descrption %> After 13年10月27⽇日星期⽇日
  47. Decoration in I18n def render_user_geneder(user) if user.gender == "male" "男

    (Male)" else "⼥女 (Female)" end end Before 13年10月27⽇日星期⽇日
  48. Decoration using Decorator class Article < ActiveRecord::Base def human_publish_status if

    published? "Published at #{article.published_at.strftime('%A, %B %e')}" else "Unpublished" end end end Before 13年10月27⽇日星期⽇日
  49. Decoration using Decorator class Article < ActiveRecord::Base def human_publish_status end

    def human_publish_time end def human_author_name end ........ end Before 13年10月27⽇日星期⽇日
  50. Decoration using Decorator class ArticleDecorator < Draper::Decorator delegate_all def publication_status

    if published? "Published at #{published_at}" else "Unpublished" end end def published_at object.published_at.strftime("%A, %B %e") end end After def show @article = Article.find(params[:id]).decorate end 13年10月27⽇日星期⽇日
  51. Decoration using View Object <dl class="event-detail"> <dt>Event Host</dt> <dd> <%

    if @event.host == current_user %> You <% else %> <%= @event.host.name %> <% end %> </dd> <dt>Participants</dt> <dd> <%= @event.participants.reject { |p| p == current_user }.map(&:name).join(", ") %> </dd> </dl> Before 13年10月27⽇日星期⽇日
  52. class EventDetailView def initialize(template, event, current_user) @template = template @event

    = event @current_user = current_user end def host if @event.host == @current_user "You" else @event.host.name end end def participant_names participants.map(&:name).join(", ") end private def participants @event.participants.reject { |p| p == @current_user } end end Decoration using View Object Before 13年10月27⽇日星期⽇日
  53. Form Builder <%= form_for @user do |form| %> <div class="field">

    <%= form.label :name %> <%= form.text_field :name %> </div> <div class="field"> <%= form.label :email %> <%= form.text_field :email %> </div> <% end %> Before 13年10月27⽇日星期⽇日
  54. Form Builder class HandcraftBuilder < ActionView::Helpers::FormBuilder def custom_text_field(attribute, options =

    {}) @template.content_tag(:div, class: "field") do label(attribute) + text_field(attribute, options) end end end 13年10月27⽇日星期⽇日
  55. Form Builder <%= form_for @user, :builder => HandcraftBuilder do |form|

    %> <%= form.custom_text_field :name %> <%= form.custom_text_field :email %> <% end %> After 13年10月27⽇日星期⽇日
  56. Form Object Best Practice Lesson #17 wrap logic in FORM,

    not in model nor in controller 13年10月27⽇日星期⽇日
  57. Form Object <%= simple_form_for @registration, :url => registrations_path, :as =>

    :registration do |f| %> <%= f.input :name %> <%= f.input :email %> <label class="checkbox"> <%= check_box_tag :terms_of_service %> I accept the <%= link_to("Terms of Service ", "/ pages/tos") %> </label> <%= f.submit %> <% end %> Before 13年10月27⽇日星期⽇日
  58. Form Object def create if params[:agree_term] if @registration.save redirect_to root_path

    else render :new end else render :new end end Before 13年10月27⽇日星期⽇日
  59. Form Object <%= simple_form_for @form :url => registrations_path, :as =>

    :registration do |f| %> <%= f.input :name %> <%= f.input :email %> <label class="checkbox"> <%= f.input :terms_of_service %> I accept the <%= link_to("Terms of Service ", "/ pages/tos") %> </label> <%= f.submit %> <% end %> After 13年10月27⽇日星期⽇日
  60. Form Object class RegistrationForm < Reform::Form property :name property :email

    property :term_of_service validates :term_of_service, :presence => true end After 13年10月27⽇日星期⽇日
  61. Policy Object / Rule Engine Best Practice Lesson #18 centralize

    permission control last one!! 13年10月27⽇日星期⽇日
  62. def render_post_edit_option(post) if post.user == current_user render :partial => "post/edit_bar"

    end end Policy Object / Rule Engine Before 13年10月27⽇日星期⽇日
  63. def render_post_edit_option(post) if post.user == current_user || current_user.admin? render :partial

    => "post/edit_bar" end end Policy Object / Rule Engine Before 13年10月27⽇日星期⽇日
  64. def render_post_edit_option(post) if post.user == current_user || current_user.admin? || current_user.moderator?

    render :partial => "post/edit_bar" end end Policy Object / Rule Engine Before 13年10月27⽇日星期⽇日
  65. class PostController < ApplicationController before_filter :check_permission, :only => [:edit] def

    edit @post = Post.find(params[:id]) end end Policy Object / Rule Engine Before 13年10月27⽇日星期⽇日
  66. Policy Object ( Pundit ) class PostPolicy attr_reader :user, :post

    def initialize(user, post) @user = user @post = post end def edit? user.admin? || user.moderator? end end After 13年10月27⽇日星期⽇日
  67. Policy Object ( Pundit ) <% if policy(@post).edit? %> <%=

    render :partial => "post/edit_bar" %> <% end %> After 13年10月27⽇日星期⽇日
  68. Rule Engine ( CanCan ) <% if can? :update, @post

    %> <%= render :partial => "post/edit_bar" %> <% end %> After 13年10月27⽇日星期⽇日
  69. class Ability include CanCan::Ability def initialize(user) if user.blank? # not

    logged in cannot :manage, :all elsif user.has_role?(:admin) can :manage, :all elsif user.has_role?(:moderator) can :manage, Post else can :update, Post do |post| (post.user_id == user.id) end end end Rule Engine ( CanCan ) 13年10月27⽇日星期⽇日
  70. Recap • Always assume things need to be decorated •

    Extract logic into methods / classes • Avoid perform query in view/helper • When things get complicated, build a new control center 13年10月27⽇日星期⽇日