Blog.XDite.net

Ruby / Rails / Web Development

Rails 3.2 新的 Route Recognition 引擎 : Jounery

| Comments

Rails 3.2 四天前 Release 了。這次主要的改進幾乎都在效能部分。

最大的改版應該屬於 Route recognition 這部分。原本這部分是由 rack-mount 擔綱,Aaron Patterson (a.k.a. @tenderlove) 將之抽換成他自己寫的 Gem : jounery。速度快了非常多倍。

但相關的原理並沒有 jounery 的 About 頁面並沒有被詳加敘述,

SYNOPSIS: Too complex right now. :(

不過根據有限的線索,我還是從 @tenderlove 的 slideshare 上挖出來了。

jounery 的原理是用 FSM ( Fininte State Machine ) 實做的。有興趣的可以從投影片裡面繼續挖。

其他豆知識:

ActiveRecord 背後的 SQL 生成引擎 Arel 背後原理是用 Relational Algerbra 生成的,可以生成非常複雜的 SQL Query 但又兼顧到效能問題。

農家樂 (Agricola) - 創業者都應該玩過的一套 Lean Startup 桌遊

| Comments

今天看到 Techorange 的 創投包心菜-vc-專欄:創投最害怕的那些人-1 引用了我之前寫的一篇關於創業資金與創業策略的文章: Startup 需不需要一開始注意資金的問題?

剛好晚上香港推友要來台灣一遊,問我家裡有沒有 農家樂 (Agricola) 這套遊戲。突然掉進時光機…

若是 2008 的我,當時應該寫不出這種關於精實創業的文章。我也可能會像其他人一樣天真,「覺得」作任何網站一定要募很多錢,作一個完美的產品,堅持「純真的想法」到最後,就算遭到嚴重挫敗,也會堅信是自己運氣不好。若有機會重來,絕對再次重蹈覆轍。

後來在事業想法和決策性格上有了大轉變,全因為我玩到了一套遊戲 農家樂 (Agricola)。當時我在玩了十場之後,寫下了遊戲感想。

這篇文章被埋在灰塵裡,我覺得很可惜,於是決定再貼一遍。這個遊戲教會我的最重要一件事,就是「只想放完美的大絕,不懂見機行事,絕對會被天譴到死

從農家樂 ( Agricola ) 看 Startup / Website 發展現況與策略

最近迷上打一款 BoardGame:農家樂「Agricola」,玩這個遊戲其實非常有助於我對一些想法的印證,因此花點時間整了下來。

這個遊戲其實並不複雜,也算蠻快結束的。不過新手講解規則可能要很久就是…

大概講解一下遊戲場景:

每個玩家經營一對夫妻,起始發給一片農莊,兩間木屋,兩份食物。隨著季節的演變,玩家可以輪流派人出去執行工作。工作大概會有:犁田、種稻、養牲口、生小孩、蓋房 / 翻修、蓋建築物(主要發展卡、次要發展卡)、學習技能(職業卡)、擷取資源(拿木、磚、蘆葦)…這幾類。

執行動作是大家輪流的,如果有人在你之前先搶先犁田了,這一回合其他人就不能犁田。遊戲分為 14 個月 ( 六季 ),每季結束可以繁殖牲畜、收成、但也要支付勞動所得。值得注意的是,通常如果你季節結束,要是每付不出一份糧食(夫妻二人共要支付四份,以此類推),就會得到一張乞討卡( -3 分),通常拿到乞討卡的人幾乎必輸無疑就是了。

計分標準偏向希望你不能偏廢,少一個達成條件扣一分。但是若達成一個條件,就幾乎加一分以上。最後比計分高低。

( 建議各位有興趣的話,買一套回家玩,大致上就能理解我下面說的是什麼。不過這一套不便宜就是了,要 NT 2620 )

而我非常喜歡這個遊戲的原因,並非最近興起的正夯種田樂。而在於這個遊戲非常考驗玩家隨機的應變能力,以及「不貪心」的忍耐功夫。而這些重點,練起來讓我受益良多…

有這麼誇張?

該怎麼說起呢,我在第一次學習打農家樂時,其實牌拿的不差,但分數卻低的相當難看的…直到回家上了桌遊版,參考了 chenglap 大大的 Agricola 心得,我才體悟到在這款遊戲中犯了哪些基本人性錯誤。

而回去找朋友打,更從他們的每個犯錯的 move 中學到了更多道理。印證到一些網站發展策略,跟幾年來見聞的心得所見不遠,甚至看到更深的面向,更覺得本款遊戲惠我良多。

下面是整理的一些重點:

( 以下並沒有意思對陪我打牌的朋友有不敬之意,純粹只是拿來舉例佐證)

1. 別人上一場獲勝的策略,並非就能成為這一場你的致勝主軸,甚至抄襲會成為害死自己的主因。

農家樂其實是非常注重隨機應變的,通常一個玩家的布局方法會隨著他執行動作的優先順位、場上資源以及 職業 / 次要發展卡而有所變化。有可能這一場我拿的局就適合耕田種菜,但貪心硬要蓋牧場養滿羊反倒害死我自己。

曾經打過一場對手,因為上一局見我生了四個人拿了不少分數(四人 12 分),開局就猛蓋屋生人,結果差一點因為每季糧食湊不到,每到收穫季節就抓襟見肘的局,雖然比其他玩家多一個人,但卻沒多少明顯優勢。

2. 手上拿了資源,就要適時利用。集了滿山滿谷資源,等到終於想建設時,卻毫無用武之地,也是一種浪費。

有朋友拿磚拿上癮,手上握了十幾顆磚。進行到遊戲後面,想說集了很多磚,來蓋點烤爐好了。卻發現因為大家覺得缺磚,早早就搶先蓋好烤爐。結果雖然拿了滿手磚,但完全沒有東西可蓋。為了集磚的 move 可以說完全都被浪費。

3. 搶先卡位以及 perfect combo 未必是正確的道路,甚至是失敗的主因。

農家樂的特色之一就是,職業卡與次要發展卡相當誘人(每個玩家各發給七張)。有一些卡片組合起來威力無窮,但另一些卡片雖誘人(效果強大)但其廢無比(比如說需要集滿 3 個職業以上才可以施展,但那時候已經接近遊戲尾聲,作用並不大)。

玩家可能費盡心機打出 perfect combo,卻忽略到其實賺的資源以及花費的資源遠不成比例。(因為粗估 28 個 move,可能玩家就花了 7 個以上的 move 在施展卡片,4 個以上的 move 在集施展卡片需要的資源。幾乎佔了一半以上。)

4. 基本建設的紮實,穩定的 income,才能健康的成長。

遊戲的 keypoint 就在於糧食匱乏與否(就跟公司現金流 / 獲利程度 )。養人就是要 做事 + 燒錢,然後用人有效的去賺錢。沒那麼多事可做卻亂燒錢,多養人亂開產品線以為沒差,其實搞得企業上吐下瀉。

因此開場注重的是想辦法建立自己的食物引擎(金流 / 產品健康的運作帶來營收),不虞匱乏才可以隨心所欲有效的施展自己的 move。而非一昧猛攻多人多 move 或者是建造無敵牧場(但其實發現自己到最後什麼動物都抓不到,甚至是養了超多動物物卻買不到烤爐結果做不了食物)。

5. 別買華而不實的東西,別鑽研很炫卻加分有限的技術。加分有限,應該投資在「有效」(分數夠多)的行動上。

蓋石屋(要先蓋磚屋才能升級成石屋,此中資源花費多多)其實投資報酬率非常低。木屋夠用就好。

6. 不會馬上回饋但肯定有正面效益的事,在別人不重視時,可以悄悄地進行。有時候甚至是致勝的關鍵。

因為在農家樂規則中,每多一塊空地上面沒建設,就會扣一分。因此像犁田這種普通粗活,前期沒人搶,但後期大家搶著犁(但很可惜,一個月只能有一個人犁)。而其實越早犁田 / 種田 其實是蠻不錯的選擇,可以有效衝高 小麥 與 蔬菜 的數量(但並非絕對)。

7. 回合數有限,效益最大化。

續上,第五季時有一張不錯的行動:「犁田並播種」。這種就是壓一次 move,卻可以同時做兩個 move 的典型範例。但這時候手上有 麥子 / 蔬菜的人,才多半能顯示押這個 move 的威力。同時,後期其實收穫期非常緊密,因此也只有糧食足夠的人可以安心押這個 move。其他人可能因為糧食緊張在頭疼。

其實做網站的時候,我們是可能犯下一些基本的迷思,更甚是通往地獄卻樂在其中。遊戲中的一些貪婪失恆的場景也反應在網路界現實中

比如:

  1. 覺得發展 XX 是未來卡位之道,結果蓄積了一堆能力,最後卻完全用不上。
  2. YY 正夯,成本低廉,便覺得浪費一個 move 也無所謂。
  3. 為了比敵手更搶先,大幅 hire RD 開發新功能 / 炫功能,卻忽略了自己體質或者是現金流並不健康,結果就是 A 做不好,B 也做不好,RD 累到炸。
  4. 看到敵手 F 領先,便花大錢狂投人力抄襲對方,到最後一刻才發現自己體質完全不適合這樣玩,最後搞得自己噴掉。
  5. ZZ 適合大規模環境使用,深信自己用得上,預先砸了一堆 RD 研究。結果完全達不到那個量,浪費成本研發。
  6. 開心的用策略打出連環 combo 殺招,上了一連串活動宣傳、廣告、上功能。結果其實在營收上入遠不敷出。
  7. 金主的耐心與底線以及 founder 的熱情,通常也只有六個季 ( 18 個月)。隨意在任何一個月亂 move ,看似輕忽,其實是一種嚴重浪費…

特別在熬夜打完兩場農家樂後,用僅存一點的清醒意識記之。

======

感想

兩年多前寫的感觸,放到目前還是一點沒變…

其實關於「創業」主題的桌遊,我還寫過一篇 相當歡樂的桌上遊戲「Burn Rate」。比較偏向人力資源使用的。

Burnrate 這篇文章遠遠比 Agricola 那篇文章有名,我也不懂為什麼。大概是因為所謂的 Lean Startup 這個概念最近才比較紅吧。

廣告一下:大年初五我在我家開桌遊團,限八人。歡迎報名。打桌遊和大食團。

如何運用 / 設計 Rails Helper (3)

| Comments

本系列其他文章:

===

Helper AntiPatterns

Helper (輔助方法)的存在目的是用來輔助整理 View 中內嵌的複雜 Ruby 程式碼。設計得當的 Helper 可以加速專案的開發,以及增進程式的可讀性。然而設計不好的 Helper 卻可能造成嚴重的反效果。

以下列舉常見的幾種糟糕的 Helper 設計模式:

1. 矯往過正:用 Helper 作 partial 該做的事

有些開發者以為 partial 效率是低下的,刻意不使用 partial,而使用 Helper 完成所有的動作。就將需要重複使用的 HTML 通通寫成了 Ruby code,串接成 HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。

錯誤

1
2
3
4
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。

錯誤

1
2
3
4
def post_tags_tag(post, opts = {})
  # ....
  raw tags.collect { |tag| "<a href='#{posts_path(:tag => tag)}' class='tag'>#{tag}</a>" }.join(", ")
end

即便換成 ' 單引號,狀況並沒有好上多少。

正確

1
2
3
4
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 上。

錯誤

1
2
3
4
5
6
7
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。造成將來有例外時,難以變更設計或擴充。

正確

1
2
3
4
5
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 的雙色效果?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<% 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,上不同的顏色。

1
2
3
4
5
6
7
8
9
10
11
12
13
<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。

1
2
3
4
5
6
7
<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:

1
2
3
4
def post_tags_tag(post, opts = {})
  tags = post.tags
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
1
2
3
<% @posts.each do |post| %>
  <%= post_tags_tag(post) %>
<% end %>

這種寫法會造成在 View 中,執行迴圈時,造成不必要的大量 query (n+1),以及在 View 中製造不確定數量的大量物件。View 不僅效率低落也無法被 optimized。

1
2
3
def post_tags_tag(tags, opts = {})
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
1
2
3
<% @posts.each do |post| %>
  <%= post_tags_tag(post.tags) %>
<% end %>
1
2
3
  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,目前已有部分章節已可預覽,歡迎預購支持我的寫作,謝謝!

[進階]使用 Facade Pattern 取代 Model Callbacks

| Comments

What is “callbacks”?

Rails 的 ActiveRecord 提供了相當方便的 callbacks,能讓開發者在寫 Controller 時,能夠寫出更加 DRY 的程式碼:

  • before_crearte
  • before_save
  • after_create
  • after_save

在從前,在 Controller 裡面想要再 object 儲存之後 do_something,直觀的思路會是這樣:

1
2
3
4
5
6
7
8
class PostController
  def create
    @post = Post.new(params[:post])
    @post.save
    @post.do_something
    redirect_to posts_path
  end
end

當時的最佳模式:通常是建議開發者改用 callbacks 或者是 Observer 模式實作。避免 controller 的髒亂。

  • callbacks : after_create
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PostController < ApplicationController
  def create
    @post = Post.new(params[:post])
    @post.save
    redirect_to posts_path
  end
end

class Post < ActiveRecord::Base
  after_create :do_something

  protected

  def do_something
  end
end

或者是使用 Observer

  • Observer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class PostController < ApplicationController
  def create
    @post = Post.new(params[:post])
    @post.save
    redirect_to posts_path
  end
end

class PostObserver < ActiveRecord::Observer

  def after_create(post)
    post.do_something
  end

end

class Post < ActiveRecord::Base

  protected

  def do_something
  end
end

使用 callbacks 所產生的問題

callbacks 雖然很方便,但也產生一些其他的問題。若這個 do_something 是很輕量的 db update,那這個問題還好。但如果是很 heavy 的 hit_3rd_party_api 呢?

在幾個情形下,開發者會遇到不小的麻煩。

  • Model 測試:每次在測試時都會被這個 3rd_party_api 整到,因為外部通訊很慢。
  • do_something_api 是很 heavy 的操作:每次寫測試還是會被很慢的 db query 整到。
  • do_something_api 是很輕微的 update:但是綁定 after_save 操作,在要掃描資料庫,做大規模的某欄位修改時,會不小心觸發到不希望引發的 callbacks,造成不必要的效能問題。

當然,開發者還是可以用其他招數去閃開:

比如說若綁定 after_save 。

可以在 do_somehting 內加入對 dirty object 的偵測,避免被觸發:

1
2
3
4
5
6
def do_somthing
  # 資料存在,且變動的欄位包括 content
  if presisited? && changed.include?("content")
     the_real_thing
  end
end

但這一招並不算理想,原因有幾:

  1. 每次儲存還是需要被掃描一次,可能有效能問題。
  2. 寫測試時還是會呼叫到可能不需要引發的 do_somehting。
  3. if xxx && yyy 這個 condiction chain 可能會無限延伸下去。

Facade Pattern

那麼要怎樣才能解決這個問題呢?其實我們應該用 Facade Pattern 解決這個問題。

設計模式裡面有一招 Facade Pattern,這一招其實是沒有被寫進 Design Pattern in Ruby 中的。Russ Olson 有寫了一篇文章解釋沒有收錄的原因:因為在 Ruby 中,這一招太簡單太直觀,所以不想收錄 XDDD。但他還是在網站上提供當時寫的草稿,供人參考。

What is Facade Pattern?

Facade Pattern 的目的是「將複雜的介面簡化,將複雜與瑣碎的步驟封裝起來,對外開放簡單的介面,讓客戶端能夠藉由呼叫簡單的介面而完成原本複雜的程式演算。」(來源

延伸閱讀: (原創) 我的Design Pattern之旅[5]:Facade Pattern (OO) (Design Pattern) (C/C++)

實際舉例:

在上述的例子中,其實 do_something 有可能只會在 PostController 用到,而非所有的 model 操作都「需要」用到。所以我們 不應該將 do_somehting 丟進 callbacks(等於全域觸發),再一一寫 case 去閃避執行

與其寫在 callbacks 裡。我們更應該寫的是一個 Service Class 將這一系列複雜昂貴的行為包裝起來,以簡單的介面執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PostController < ApplicationController
  def create
    CreatePostService(params[:post])
    redirect_to posts_path
  end
end

class CreatePostService
  def self.create(params)
    post = Post.new(params[:post])
    post.save
    post.do_something_a
    post.do_something_b
    post.do_something_c
  end
end

而在寫測試,只需要對 PostCreateService 這個商業邏輯 class 寫測試即可。而 PostController 和 Post Model 就不會被殃及到。

小結

不少開發者討厭測試的原因,不只是「因為」寫測試很麻煩的原因,「跑一輪測試超級久」也是讓大家很不爽的主因之一。

其實不是這些測試框架寫的爛造成「寫測試很麻煩」、「執行測試超級久」。而是另有其他因素。

許多資深開發者逐漸意識到,真正的主因是在於目前 Rails 的 model 的設計,耦合度太高了。只要沾到 db 就慢,偏偏 db 是世界的中心。只是測某些邏輯,搞到不小心觸發其他不需要測的東西。

ActiveRecord 的問題在於,讓開發者太誤以為 ORM = model。其實開發者真正要寫的測試應該是對商業邏輯的測試,不是對 db 進行測試。

所以才會出現了用 Facade Pattern 取代 callbacks 的手法。

其他

MVC 其實有其不足的部份。坦白說,Rails 也不是真正的 MVC,而是 Model2

目前 MVC 其實是不足的,演化下來,開發者會發現 User class 裡面會開始出現這些東西:

  • current_user.buy_book(book)
  • current_user.add_creadit_point(point)

這屬於 User 裡面應該放的 method 嗎?well,你也可以說適合,也可以說不適合。

適合的原因是:其實你也不知道應該放哪裡,這好像是 User 執行的事,跟他有關,那就放這裡好了!不然也不知道要擺哪裡。

不適合的原因是:這是一個「商業購買行為」。不是所有人都會購物啊。這應該是一個商業購買邏輯。但是….也不知道要放在哪啊。

一直到最近,James Copelin 提出了:DCI 去補充了現有的 MVC 的不足,才算勉強解決了目前浮現的這些問題。

DCI ,與本篇談到的 Facade Pattern 算是頗類似的手法。

有關於 DCI ( Data, Context, Interaction ) 的文章,我會在之後發表。我同時也推薦各位去看這方面的主題。這個方向應該會是 Rails 專案設計上未來演化的方向之一。

關於 Essential Rails Pattern 這本書的近況

| Comments

最近看到不少讀者都在「委婉」的催我最新這一本書「Essential Rails Pattern」的近況。

跟大家報告一下這本書的近況,其實我每個禮拜都還是有在持續撰寫這本書。但是這本書寫著寫著,我越發現一件可怕的事情,內容太大了,大到比我當初想象中的還大概長五倍。到現在我已經為這本書寫超過 10 萬字了。可是還是看不到盡頭。

而且在當初安排初版結構時,在前三章的結構上安排得有些錯誤。原本是要寫 Ruby / Rails 的設計技巧,卻岔遠路去撰寫基本的程式設計技巧。

草稿的字是累積不少,但是還不能見人,於是就一直放著。改先出零散的文章,所以最近各位可能看到我在撰寫一些回歸基本的教學,其實也是為這本書在做準備。

這幾天,我想了很久,預購的讀者每次抓下來都只有空白頁,等久了也會生氣 XD。

終於我做了一個決定,原先的目錄結構太爛了,不砍掉重寫可能就會永遠斷頭了XD。

於是我將原本寫好的幾章全部打散重排,所有已經寫好的內容,可以抽成章的就先整理出來。目前的草稿雖然有點多,但還算整理的完的。我會將草稿放在這裡: http://erp-book.heroku.com

這次也算釋出正式版的全部大綱。我希望可以在今年二月,釋出現在手頭上絕大部分的 beta 內容。

如果有「等的不耐煩」的讀者,或者是「看完大綱覺得這本書不是你想要的」,或者是「這本書覺得不值得這個價錢的」,或者是「就是不爽 xdite 你這個大騙子」的,都可以寫信給我要求退錢。

再次為進度的緩慢,致上我深深的歉意。

===

不過如果你看完這本書的大綱和目前進度,還滿意這樣的內容和主題,想支持我繼續寫作的(其實寫這麼多字也是很累的..orz),也歡迎加入預購支持我的作品,謝謝。

[出售] Macbook Air 攻頂款另贈 NDSi XL

| Comments

蘋果今天在大特賣,我也打算特賣我的 Macbook Air。

打算在過年前把手上這一臺 Macbook Air 13” 賣掉換新的。

這台是 2011/03 從香港買的攻頂款。

配備:

  • CPU: Intel Core2 Duo 2.13GHZ
  • RAM: 4G
  • HDD: 256 GB SSD
  • 鍵盤: 全英文鍵盤

還在 Apple Care 中。沒有貼紙在上面,保護得還算不錯。

當時台灣售價是 $ 62000,我從香港買進花了 $52xxx。

我打算賣 NT $41000。賣了之後會幫你還原到 10.6 出廠預設。

====

售價 41000 怎麼算特賣?

因為之前手上玩具不小心買太多了,有一臺也是當時買的,但是後來沒玩幾次的 NDSi XL 放在家裡沒在打… 應該也還值個幾千塊(這台在美國買的時候也花了 6-7000)。

如果你購買這台 Air 的話,這台 NDSi XL 就也順便送你啦。看你要拿去換現金還是收起來自用。

有興趣的朋友歡迎跟我聯絡。如果你已經是我 FB 好友的話,可以少收你 1000 元。

=== 用 iPhone 4S 拍的現況圖,不知道解析度夠不夠

http://www.flickr.com/photos/xdite/sets/72157628733737777/

[進階] Make ActiveRecord Includable

| Comments

[警告] 這一篇是進階的文章,如果你看不懂可以跳過。

前幾天在 Twitter 看到一條值得慶賀的消息(印象已模糊,忘記誰慶賀,也不知慶賀的原因),是關於 Rails core 上的一串 commit,大意是 ActiveRecord::Base 已經從傳統的繼承使用,變成了可以用 include 的 ActvieRecord::Model。

https://github.com/rails/rails/compare/58f69ba…00318e9#diff-0

也就是從開始有 Rails 以來,傳統的使用方式

1
2
class Post < ActiveRecord::Base
end

以後將變成

1
2
3
class Post
  include ActiveRecord::Model
end

坦白說,我看不懂這一串的意義是什麼。而 commit 下面的留言也有人說他看不懂….XDDDD

所以我就把這則收入記憶倉庫了。直到今天聽 podcast 時不小心觸發…

Ruby Rogues Podcast

Ruby Rogues 是這幾個月才興起的一個新的 Ruby Podcast,與較知名的 Ruby 5The Ruby Show 這兩個 podcast,性質很不同。後兩者著重於介紹本週有什麼 Ruby / Rails 的新消息,或者亮點 gem。Ruby Rouges 的則是每周邀請五個 Ruby 大師,上來針對一個特定主題,無盡的喇賽亂鬥 XD

當然這些大師也不是在講廢話,從他們的喇賽中可以學到不少觀念,甚至是有的時候你還可以聽到有的大師現場被其他人電:「什麼,你不知道可以這樣用?」「什麼,你一直用錯誤的觀念寫 code?」….XD

到目前為止一共有 35 集,每集大概 1 小時。這麼多….所以當然我…沒有聽完 XDDDD

今天找資料時,翻到第 20 集 RR Object Oriented Programming in Rails with Jim Weirich

為了要找其中的一段資料,就耐心的下載了這集,開始聽。結果原先的資料沒找到,倒是意外聽到一段重要的寫 code 觀念,讓我理解 Make ActiveRecord includable 的意義。

Rails 誤導大家的觀念 : Model = DB

這一段觀念可以從 podcast 中的大約 30:00 左右開始聽。

Rails 誕生以來,model 的設計就是長這樣,從 ActiveRecord::Base 繼承。

1
2
3
4
5
6
7
8
9
class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")


end

當專案長大了,開發者免不了會往裡面塞一些 Business Logic,所以會變成這樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")


  def aaa
  end

  def bbb
  end

  def ccc
  end
end

但是 class 再繼續長大之後,大家可能就會受不了了。developer 開始把一些 method ( 所謂的 bussiness logic 抽出去),用 include 的方式去做,然後把這些 logic 放在 lib/ 下。

變成

1
2
3
4
5
6
7
8
9
10
11
12
class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")

  include AAA
  include BBB
  include CCC

end

儘量讓這個 model class 只保持著 db 的 association,logic 抽出去保持整潔。

寫到這裡看起來都沒問題?

繼承自 ActiveRecord::Base 帶來的問題

錯了,問題可大了。

這樣的設計導致了一個現象:因為繼承自 ActivRecord::Base,無可避免的寫測試一定要扯到 DB,於是就帶來了其他頭痛的問題

  1. 寫測試變成在測試 query 和 ORM。寫測試的重點是在測 Business Logic,其實應該要與 DB 資料與 DB 連線無關,結果測試都在測這個…

  2. 因為一定要過 DB,於是 model 測試很慢。

ORM 是配角,被誤以為主角,反客為主!!!

在 MVC 裡面,Model 的定義-主要負責應用程式中的商業邏輯(Business Logic)。

看看這個範例,你覺得這個 model 寫法是正確的嗎!?

1
2
3
4
5
6
7
8
9
10
11
12
class Post < ActiveRecord::Base

  has_many :comments
  belongs_to :user

  scope :recent , order("id DESC")

  include AAA
  include BBB
  include CCC

end

這是一個 ORM 行為為主的 model,不是 Bussiness Logic 為主的 Model 啊 XD。真正的主角好像被趕走了,被趕到 lib/ 去。

podcast 中 30:00 - 33:00 主要討論的議題就是:

===

你將 Bussiness Logic 放在哪裡?你稱 Bussiness Model 為 Model 還是 SuperModel。然後 James Edward Gray 在這裡回答他放在 lib 下,就被電了 XDDDDD

===

事實上正確的寫法應該通通都是要放在 app/models 下。

而設計手法應該是

class Post 本身就放自己的 bussiness logic,然後去 claim 自己有用 ORM。至於過多複雜的邏輯就整理起來放在 Module 裡。

1
2
3
4
5
6
7
class Post
  include ActiveRecord::Model

  include AAA
  include BBB
  include CCC
end

而寫測試應該就是測 model 本身的商業邏輯而不是測 DB !!

小結

Rails 原生的機制讓開發者非常容易以為 Model 與 ORM 是一對一的關係。並且在此架構中,要將 code 整理得乾淨,就會無可避免的演變到反客為主的寫法。

這個 commit 其實只是剛起步,還沒有能夠完全避免一定要測到 DB 的問題。不過至少邁開了一大步。

聽完了這集 podcast,讓我完全看懂這個 commit 和下面的留言,還順便澄清了一個錯誤觀念…

(不過對現狀沒什麼幫助,因為這是 4.0 feature,所以目前還是不能用的 XD,開發者還是只能按照舊的寫法繼續當鴕鳥)

有空還是要多聽大師喇賽才能進步啊…

[Ruby][教學] 如何打包一個 Asset Gem

| Comments

What is Asset Gem

Asset Pipeline的概念興起,不只是推動了 SASS 與 CoffeeScript 的廣泛流行。其實造成更重大的影響是 assets ( CSS / JavaScript / Images ) 不再被視為專案中難以「整理」與「管理」的頭痛元件。透過 Asset Pipeline 的架構,我們可以把 assets 包裝成一個 gem ,在其他專案中重複使用。

在以往,如果想使用 bootstrap 這個 CSS / JS Framework,我們必須將所有靜態檔案 COPY 一份到專案的靜態目錄中。當專案使用到大量 3rd party vendor assets,整個靜態目錄就會被這種拷貝行為弄得髒亂不堪,難以整理。

而透過 Asset Pipeline 的架構,開發者就可以停止這種草率但不得不為之的動作。要引用 3rd party vendor assets,只要在 application.css 或者 application.js 進行 require 就可以輕鬆使用了。

1
2
//= require jquery
//= require bootstrap

引用 asset gem 很簡單,但不少人想知道的是:如何把手上想整理的 asset 包裝成一個 gem 進行使用

Asset Pipeline 的 mount 位置

談到這裡,就要稍微提一下 Asset Pipeline 對於 assets 位置的定義。by default,你可以把 assets 放在以下三個資料夾內:

  • app/assets
  • lib/assets
  • vendor/assets

理論上,你把 assets 丟在這三個資料夾內,在 application.cs|js 內 require 都可以動。

如何整理目前專案中的 assets

這其實是另外一個主題,不過我在這裡也順便整理出來。

如何整理歸類現在手頭上的 assets 呢?

  • app/assets

在 Rails 3.1.x 之後的版本,rails g controler posts,會自動在 assets/styelsheets/ 和 assets/javascripts/ 中產生對應的 scss 與 coffeescript 檔案。

所以 app/assets 是讓開發者放「自己為專案手寫的 assets」的地方。

  • lib/assets

lib 是 library 的簡寫,這裡是放 LIBRARY 的地方。所以如果你為專案手寫的 assets 漸漸形成了 library 規模,比如說 mixin 或者是自己為專案整理了簡單的 bootstrap,應該放在 lib/ 下。

  • vendor/assets

verdor 是「供應商」的意思,也就是 「別人寫的」assets 都應該放在這裡。比如說:

  • jquery.*.js
  • fanfanfan icons
  • tinymce / ckeditor

等等…

透過 Rails Engine 機制實作

為什麼剛剛要扯這麼大一圈去解釋如何整理手頭的 assets 呢?

因為 asset gem 其實就是透過 Rails Engine 的機制去實作出來的。

拿一個前幾個月幫 @evenwu 寫的 asset gem 作為示範好了。

https://github.com/xdite/compass-ggs-framework/tree/rails-engine

作法是將你整理好的 lib/assets 扔到 vendor/assets 裡(你寫的給別人用,你就變成 vendor 了),再宣告一個「空的」Rails Engine Class 讓 Rails 可以將這個 gem 視為網站的一部分「掛起來」裡面的 vendor/assets。

沒錯,就是這麼簡單。

而宣告自己是一個 Rails Engine 的方式也很簡單:只要把 Rails Engine 塞進自定義的 module 就好了。 (還是看不懂的可以看我的 code…)

1
2
3
4
5
6
module Ggs
  module Rails
    class Engine < ::Rails::Engine
    end
  end
end

剩下來的流程就跟一般包 Gem 的流程差不多了。

=====

現在我每週都固定有在回答一些問題,發現不少朋友對 Ruby / Rails 的一些疑惑,都大同小異。這些問題有一些我有寫過文件但沒有公開披露,有一些沒有寫過文件但有答案。所以順手把這些回答過的答案整理到 blog 上讓大家參考。

如果你在 Ruby / Rails 在使用有任何問題,都歡迎貼到 http://ruby-taiwan.org

[Ruby][教學] 如何打包一個 Gem

| Comments

What is Gem

RubyGems 是 Ruby 的 Package 管理系統。它的作用類似 Linux 系統下的 apt-get 或者是 yum。不同的是:RubyGems 是提供「打包」好的 Ruby Library 讓開發者能夠重複利用別人已造好的輪子,提高開發效率。

而目前 Rails 3.0+ 起,幾乎都也推薦使用 RubyGems 的方式,將 Plugin 打包成 Gem 的方式搭配 Bundler 使用。

打包 Gem

隨著時代進步,打包和發佈 Gem 的方式一直在進步。

最早以前大家都是手工製造 ( RailsCast #135 ),後來 Jeweler( RailsCast #183 ) 被發明出來,讓打包變得非常容易。

而到最後,更演變成了 Bundler 內建 ( Rails 245 )。

包裝一個 Gem 變得越來越容易。

Gem 的基本結構

若以 Bundler 內建的指令 bundle gem GEM_NAME 自動生出來的檔案。其實 Gem 的結構也相當簡單。

1
2
3
4
5
6
7
8
    [~/projects/exp] $ bundle gem my_plugin
          create  my_plugin/Gemfile
          create  my_plugin/Rakefile
          create  my_plugin/.gitignore
          create  my_plugin/my_plugin.gemspec
          create  my_plugin/lib/my_plugin.rb
          create  my_plugin/lib/my_plugin/version.rb
    Initializating git repo in /Users/xdite/projects/exp/my_plugin
  • Gemfile # 描述 dependency
  • Rakefile # 發佈和打包的 rake tasks
  • GEM_NAME.gemspec # gem 的 spec
  • GEM_NAME/lib/GEM_NAME.rbGEM_NAME/lib/GEMNAME/ # gem 裡的 library
  • GEM_NAME/lib/GEM_NAME/version.rb # 版本紀錄

主要的 Library 需放置在 lib/ 底下。

若需使用到相依套件的話,需在 Gemfile 以及 .gemsepc 定義。

Bundler 提供的基本 Task

Bundler 基本上算是提供半自動的打包,只提供非常基本的三個 Task:

  • rake build # Build my_plugin-0.0.1.gem into the pkg directory
  • rake install # Build and install my_plugin-0.0.1.gem into system gems
  • rake release # Create tag v0.0.1 and build and push my_plugin-0.0.1.gem to Rubygems

Jeweler

若你有更多懶人需求,不妨 check Jeweler 這個 gem,它提供了更多 rake tasks 讓打包更加方便。

Best Practices

Rails Core Team member 「Josh Peek」曾經在 Rails 官方 blog 寫過一篇文章 Gem Packaging: Best Practices 講解如何寫出比較乾淨正確的 Gem。

如何在專案中使用開發中的 gem

以往的想法可能都是打包之後,在 local 安裝開發中的 gem 版本,或者是直接先放在 vendor/plugins 中測試。在有了 Bundler 的時代其實不需要這麼麻煩。

只要在 Gemfile 內加入這樣一行

1
gem 'my_plugin', :path => "~/projects/exp/my_plugin"  # your local gem path 

就可以引用開發中的 gem,等到真的開發完。再換成 git repo 或 rubygems.org 上的版本。

=====

現在我每週都固定有在回答一些問題,發現不少人對 Ruby / Rails 的一些疑惑,都大同小異。這些問題有一些我有寫過文件但沒有公開披露,有一些沒有寫過文件但有答案。所以順手把這些回答過的答案整理到 blog 上讓大家參考。

如果你在 Ruby / Rails 在使用有任何問題,都歡迎貼到 http://ruby-taiwan.org