本系列其他文章:
===
Helper AntiPatterns
Helper (輔助方法)的存在目的是用來輔助整理 View 中內嵌的複雜 Ruby 程式碼。設計得當的 Helper 可以加速專案的開發,以及增進程式的可讀性。然而設計不好的 Helper 卻可能造成嚴重的反效果。
以下列舉常見的幾種糟糕的 Helper 設計模式:
1. 矯往過正:用 Helper 作 partial 該做的事
有些開發者以為 partial 效率是低下的,刻意不使用 partial,而使用 Helper 完成所有的動作。就將需要重複使用的 HTML 通通寫成了 Ruby code,串接成 HTML:
def show_index_block ( block_name , post , is_show_game )
block_title = content_tag ( :h3 , block_name )
section_header = content_tag ( :div , block_title , :class => "section-header" )
game_name = is_show_game ? "【 #{ post . games . first . name } 】" : ""
title = content_tag ( :h4 , link_to ( " #{ game_name } #{ post . title } " , post_path ( post )))
image = content_tag ( :div , render_post_image ( post ), :class => "thumbnail" )
content = content_tag ( :p , truncate ( post . content , :length => 100 ))
section_content = content_tag ( :div , " #{ title }#{ image }#{ content } " , :class => "section-content" )
section_footer = content_tag ( :div , link_to ( "閱讀全文" , post_path ( post )), :class => "section-footer" )
return content_tag ( :div , " #{ section_header }#{ section_content }#{ section_footer } " , :class => "article-teaser" )
end
Helper 的作用只是協助整理 HTML 中的邏輯程式碼,若有大片 HTML 需要重複使用,應該需要利用 partial 機制進行 HTML 的重複利用。這樣的寫法,非但效率低下(可以用 HTML 產生,卻使用 Ruby 呼叫 Tag Helper,且製造大量 Ruby Object),而且更降低程式的可讀性,其他維護者將難以對這樣的 DOM 進行後續的維護翻修。
2. 容易混淆:在 Helper 裡面穿插 HTML tag
這也是另外一個矯枉過正的例子,不過剛好相反,因為覺得使用 Ruby code 產生 HTML tag 可能浪費效能,而直接插入 HTML 在 Helper 裡面與 Ruby Code 混雜。結果造成維護上的困難。因為 Ruby 中的字串是使用雙引號"
,而 HTML 也是使用雙引號"
,,所以就必須特別加入 \"
跳脫,否則就可能造成 syntax error。
錯誤
def post_tags_tag ( post , opts = {})
# ....
raw tags . collect { | tag | "<a href= \" #{ posts_path ( :tag => tag ) } \" class= \" tag \" > #{ tag } </a>" }. join ( ", " )
end
大量的 "
混雜在程式碼裡面,嚴重造成程式的可閱讀性,而且發生 syntax error 時難以 debug。
錯誤
def post_tags_tag ( post , opts = {})
# ....
raw tags . collect { | tag | "<a href=' #{ posts_path ( :tag => tag ) } ' class='tag'> #{ tag } </a>" }. join ( ", " )
end
即便換成 '
單引號,狀況並沒有好上多少。
正確
def post_tags_tag ( post , opts = {})
# ...
raw tags . collect { | tag | link_to ( tag , posts_path ( :tag => tag )) }. join ( ", " )
end
正確的作法應該是妥善使用 Rails 內建的 Helper,使 Helper 裡面維持著都是 Ruby code 的狀態,並且具有高可讀性。
3. 強耦合:把 CSS 應該做的事綁在 Ruby Helper 上。
錯誤
def red_alert ( message )
return content_tag ( :span , message , :style => "font-color: red;" )
end
def green_notice ( message )
return content_tag ( :span , message , :style => "font-color: green;" )
end
開發者不熟悉 unobtrusive 的設計手法,直接就把 design 就綁上了 Ruby Helper。造成將來有例外時,難以變更設計或擴充。
正確
def stickies ( message , message_type )
content_tag ( :span , message , :class => message_type . to_sym )
end
< span class = "alert" > Please Login !! < /span>
樣式應該由 CSS 決定,使用 HTML class 控制,而非強行綁在 Helper 上。
4. 重複發明輪子
Rails 已內建許多實用 Helper,開發者卻以較糟的方式重造輪子。在此舉幾個比較經典的案例:
如何設計 table 的雙色效果?
劣
< % count = 0 >
< table >
< % @items.each do | item | %>
<% if count % 2 == 0 %>
< % css_class = "even " %>
< % else %>
< % css_class = "odd" %>
< % end %>
< tr class = "<%= css_class %>" >
< td > item < /td>
</ tr >
< % count += 1 %>
< % end %>
< /table>
一般的想法會是使用兩種不同 HTML class : event 與 odd,上不同的顏色。
劣
< table >
< % @items.each_with_index do | item , count | %>
<% if count % 2 == 0 %>
< % css_class = "even " %>
< % else %>
< % css_class = "odd" %>
< % end %>
< tr class = "<%= css_class %>" >
< td > item < /td>
</ tr >
< % count += 1 %>
< % end %>
< /table>
這是一般初心者會犯的錯誤。實際上 Ruby 中有 each_with_index
,不需要另外需要宣告一個 count。
優
< table >
< % @items.each do | item | %>
<tr class="<%= cycle("odd", "even") %> ">
<td>item</td>
</tr>
<% end %>
</table>
但 Rails 其實內建了 cycle
這個 Helper。實際上只要這樣寫就好了...
常用你可能不知道的 Helper
限於篇幅,直接介紹幾個因為使用機率高,所以很容易被重造輪子的 Helper。開發者會寫出的相關 AntiPattern 部分就跳過了。
5. Ask, Not Tell
這也是在 View 中會常出現的問題,直接違反了 Law of Demeter 原則,而造成了效能問題。十之八九某個 View 緩慢無比,最後抓出來背後幾乎都是這樣的原因。
不少開發者會設計出這樣的 helper:
劣
def post_tags_tag ( post , opts = {})
tags = post . tags
tags . collect { | tag | link_to ( tag , posts_path ( :tag => tag )) }. join ( ", " )
end
< % @posts.each do | post | %>
<%= post_tags_tag(post) %>
< % end %>
這種寫法會造成在 View 中,執行迴圈時,造成不必要的大量 query (n+1),以及在 View 中製造不確定數量的大量物件。View 不僅效率低落也無法被 optimized。
優
def post_tags_tag ( tags , opts = {})
tags . collect { | tag | link_to ( tag , posts_path ( :tag => tag )) }. join ( ", " )
end
< % @posts.each do | post | %>
<%= post_tags_tag(post.tags) %>
< % end %>
def index
@posts = Post . recent . includes ( :tags )
end
正確的方法是使用 Tell, dont ask 原則,主動告知會使用的物件,而非讓 Helper 去猜。並配合 ActiveRecord 的 includes 減少不必要的 query( includes 可以製造 join query ,一次把需要的 posts 和 tags 撈出來)。
且在 controller query 有 object cache 效果,在 view 中則無。
小結
Helper 是 Rails Developer 時常在接觸的工具。但可惜的是,多數開發者卻無法將此利器使得稱手,反而造成了更多問題。在我所曾經參與的幾十個 Rails 專案中,很多設計和效能問題幾乎都是因為寫的不好的 View / Helper 中的 slow query 或伴隨產生的大量 object 所造成的 memory bloat 導致的。但參與專案的開發者並沒有那麼多的經驗,能夠抓出確切的病因,卻都將矛頭直接是 Rails 的效能問題,或者是沒打上 Cache 的關係。這樣的說法只是把問題掩蓋起來治標,而非治本。
下次若有遇到 performance issue,請先往 View 中瞧看看是不是裡面出現了問題。也許你很快就可以找到解答。
===
接下來兩章我將會介紹:
自用 Helper 的設計整理原則、如何將常用 Helper 抽取出來可以複用。
本篇文章將會收錄在 Essential Rails Pattern ,目前已有部分章節已可預覽 ,歡迎預購 支持我的寫作,謝謝!