5/31/2007

Markaby:惡趣味的逆襲

有些東西你明知道是錯的,但是還是會去做,只是因為那樣做很爽。

不是說你很邪惡,而只是因為有時候作壞事本身就是一件很有趣的事情。像是蹺課出去打撞球,像是上班偷懶,像是捉弄朋友....等等雖然明知道本身是不對的事情,但是依舊無法抗拒這種快感。第一次看到 Markaby,心中的 OS 是「妖孽呀,這是網頁設計的邪道呀」。但是最近有機會碰到高手寫的 Markaby code ,頓時發覺有點沈溺其中無法自拔(果然是邪道,才會有毒品的快感)。或許這又是一個明知不對,但是又令人忍不住去碰的小誘惑。

敝人小弟在下我曾經寫過HTML?New Template System ?,裡面有提到
Ruby 統一全世界當然是一個美好的夢想,畢竟我是100%原汁的 Ruby 派。但是當你在商業應用上,版面設計通常交給美工,他們只需要會 Dreamweaver 之類的東西
這時候,難道你要教美工 Ruby 程式設計?
所以,除非 Dreamweaver 或是 Frontpage 支援 Ruby @@!,不然大家還是先把 HTML 當成網頁程式設計的最大公約數好了。
因為考慮到美工人員不太會去管程式,程式也沒這個 sense 去碰美工,所以所謂的 template 正道,就是「美工歸美工,程式歸程式」。基本要求是 HTML 版面跟 Rails 程式分開,美工好做事,程式也好修改。

但是 Markaby 就是一個相當詭異的存在,他是一個使用 Ruby Code 來撰寫 html 的方式。他是一個 Rails Plugin ,安裝方式是
 script/plugin install http://code.whytheluckystiff.net/svn/markaby/trunk
rhtml 跟 markaby 是可以兩者並行的,請先將你的 markaby file 都從 .rhtml 改成 .mab 的名字,這樣 rails 就可以知道這是 markaby 的檔案了。

撰寫範例是這樣

h1 'Listing products'

table.editor.classic do
tr do
for column in Product.content_columns
th column.human_name
end
end

for product in @products
tr do
for column in Product.content_columns
td product.send(column.name)
end
td { link_to 'Show', :action => 'show', :id => product }
td { link_to 'Edit', :action => 'edit', :id => product }
td { link_to 'Destroy', { :action => 'destroy', :id => product }, :confirm => 'Are you sure?' }
end
end
end

link_to 'Previous page', { :page => @product_pages.current.previous } if @product_pages.current.previous
link_to 'Next page', { :page => @product_pages.current.next } if @product_pages.current.next

br

link_to 'New product', :action => 'new'
裡面有很多 html tag 跟 Ruby code,Rails helper。裡面看不到煩人的 <% %> 等符號,只有讓人興奮的 Ruby code。

好處呢?

就是一個爽字形容,還有另外一種自己已經成為高手的錯覺。

壞處呢?

你沒辦法用美工改好給你的 HTML code ,你必須要求美工使用 CSS 來做所有排版的工作,不然合作起來也非常累。另外一個壞處是,交接的時候交接的人會很不高興,這是哪門子的版面 code ,要我怎麼看呀。

該不該用呢?

就實際面來說,沒有任何實用價值。但是你也知道,有些東西你明知道是錯的,但是還是會去做,只是因為他很爽。所以夜深人靜的某個夜晚,我依舊偷偷使用 Markaby 來寫我的某個小網頁 :p

5/28/2007

給寄信問我問題的朋友們

我先說一下我現在 Email inbox 裡面的情況,我每天會收到 10 ~ 50封左右的問 Ruby on Rails 問題 Email,x 封左右的 Start up 合作信件,還不包含我工作相關信件。也就是說,你們寄給我的信件通常會在Email 大海中飄盪,運氣好我會看到,而且就算看到了,我通常抽不出時間解決。最慘的是,通常 10個問題有五個是重複的....................。

這個情況已經持續了三個月有了,我也不知道為了晚回 Email 道歉過幾百次了,為了解決這個情況,希望每個看我 Blog 朋友,如果有問題的話,可以到 JavaWorld 的 Ruby 版上面發問,我會定期去那邊回答問題的,非常感謝。


Event Driven Mongrel

Mongrel 就大家所知道的,是採取 Thread 的方式來運作的,但是multi thread 在concurrent 大時 loading 會很重,要花很多時間處理很多 thread sync 問題。所以現在很多 Web Server ,包含 Lighty,Zeus 都是採用 Event Driven 的方式來實做,Apache 現在也有 Event Driven 的運作方式。

Mongrel 現在也有人做 Event Driven 的實做方式了,不過你得跟 Swiftiply 一起安裝。

gem i swiftiply

然後啟動 Mongrel 就始用

EVENT=1 mongrel_rails start

就可以啟動了。

快不快的部份我現在還沒發現,但是至少跑到現在還沒掛點是個很好的事情。


5/23/2007

[ 書評 ] Ruby on Rails 專業網站案例實作


對於我這樣不認真的人來說,要我寫 Testing 簡直是要了我的命,所以我從一開始看 Ruby on Rails 的時候,就完全沒碰 Testing 的部份。上次 UbiSunrise 的活動結束後,我還記得有觀眾告訴我

Ruby on Rails 比 Java 好的地方就是 Testing

讓我霎時間一個大大的 shock 。到底 Ruby on Rails 的 Testing 有何精妙之處呢?






命運是好玩的,很快的這本 「Ruby on Rails 專業網站案例實作」就出現在我的身邊。這是 Beginning Ruby on Rails E-Commerce: From Novice to Professional 的翻譯本,從書名來看,這本書是介紹用 Ruby on Rails 來寫 E-Commerce 的書,但是翻到一半,他給了我一種「Beyound Java」的感覺,沒錯,又是一本內容不錯,但是書名取怪的書。如果要我重新定一個書名,我想取「使用 Ruby on Rails學習 Test-Driven Development」應該是一個很好的書名。

沒錯,這是一本講解 Test-Driven Development 的良好教材,應該說,裡面有很多很棒的 Ruby on Rails Testing 範例 Code。並且開發方式真的是遵照 TDD 原則,先寫 Testing code ,再開始寫 real code。我從裡面的範例從頭到尾看到完,我享受了一頓真的很不錯的 Testing Tutorial。我從裡面學習很多很多 Testing 的技巧跟觀念。書的後半段還有介紹 Selenium 這個驗收測試的工具使用方式。真的是 Testing 的全套都 run 過一次,整本書算是一個完整的 Testing Tutorial。

再來,最後面的部份其實收錄了不少 Enterprise 才需要的問題,像是 deploy,I18n,效能最佳化等等的議題,而且整理的還很不錯。

最後,翻譯者功力很好,翻譯的不差,我那麼討厭看中文電腦書的人都覺得翻譯的真的順眼。「Convention over Configuration」 翻成「慣例優於組態」這個翻譯是我看過目前為止,把 Convention over Configuration 翻譯的最簡短,也最不失真的翻譯方式。

要講缺點的話,當然也有。他不是一個初學者的書本,裡面的講解方式都是要對 Ruby on Rails 有一定基礎的人才能夠輕鬆的閱讀,講解基礎觀念的部份不多。再來就是書名很容易造成誤導,最好改個書名比較好。

我還蠻喜歡這本書的。


5/17/2007

把你的 Rails App 打包成 RPM Package

剛剛看到 gigix 寫的 Create RPM For Your Rails Application,這是一個叫做 rpmpackager 的 rails plugin,目的是將你的 Rails App 打包成 RPM Package。我覺得這個東西假設配合 RubyWorks ,是一個在 Fedora Like Linux 上面還算是不錯的 deploy 的好作法。






前置工作

在使用這個 plugin 之前,系統上面必須安裝而且設定好 rpmbuild 這個工具。

安裝 plugin

ruby script/plugin install http://rubyworks.googlecode.com/svn/trunk/rpmpackager/

設定

設定檔在 vendor/plugins/rpmpackager/config.yml 裡面,內容是

configuration:
app_name: app的名字
description: 敘述
license: Apache
version: 1.2.1
release: 1
# RPM dependencies. separated with commas dependencies: openssl, mysql >= 5.0
# 這裡敘述著 RPM 之間的 dependencies
# 如果我要使用 openssl 跟 MySQL 5.0 以上的版本,就這樣寫
# openssl, mysql>=5.0
# 中間用 , 分開
# gem dependencies and installation indecies
# 0 for gems don't need selection
# gem 之間的 dependencies ,0 代表不用,1 代表需要這個 gem package
gems:
redcloth: 0
rcov: 1

打包 RPM

使用 rake rpm_package 來打包成 RPM ,他會將 RPM Package 放在 /usr/src/redhat/RPMS/i386/ 底下。而打包好的 RPM 會自動安裝到 /usr/local/lib/rails-apps/#{app_name} 底下。


RubyWorks 0.0.1 Release

如果你覺得安裝所有的 Ruby on Rails 相關套件,並且將 Production Server系統設定好是一件很麻煩的事情嗎?或許你可以試試看 RubyWorks

RubyWorks 是一個在 Red Hat Enterprise 或是 CentOS 上面的套件組合,他會幫你把所有 Production 環境下面的相關的 Ruby on Rails 套件跟 Server 套件一次安裝完成,並且提供一個馬上可以跑的 defulat config file,也就是說各位公司的技術長們不需要花時間去看那麼多 Production 設定方式,你已經有一個很堪用的 Production Ruby on Rails Server了。

So,你還有理由不玩 Ruby on Rails嗎?

我們看看怎麼安裝 RubyWorks,因為我沒有 Red Hat Enterprise Server,所以我只 Test CentOS。

前置作業

RubyWorks 因為好像還沒進去 yum 資料庫,所以我們還是得必須告訴 yum 哪裡有 RubyWorks 的 Package,如果已經進去yum repo了,就好像不需要這個動作了。

如果你像我一樣,是個不求甚解,只求可以 Work 的人,就這樣打就好了。
wget http://rubyworks.rubyforge.org/public_key.txt
sudo rpm --import public_key.txt
wget http://rubyworks.rubyforge.org/RubyWorks.repo
cp RubyWorks.repo /etc/yum.repos.d/

YUM 安裝 Ruby Works
直接用 gem 安裝
yum install rubyworks
然後.....裝好了。

請連線到 http://localhost:3001/ 的地方,你一定會看到一個很熟悉的畫面。是的,Ruby on Rails 已經跑起來了,還是用 Haproxy 幫你跑的。不過這裡要講的是,因為他的取向不是 development server,而是 production server,所以他並不會安裝 Rails 的 gem package,而是直接將 rails 放在 /usr/rails/vender/rails 裡面。

Rubyworks 安裝表

RubyWorks 一共會幫你安裝
  • ruby
  • ruby-devel
  • rubygems
  • haproxy
  • monit
  • mongrel
  • rubyworks
並且幫你設定好 HAProxy ,跑在 3001 Port,Mongrel 跑在 3002~3005 Port,monit 跑在 2812 port。

Rubyworks 詳細位置表

詳細的安裝位置是在
  1. /usr/bin/ruby/:Ruby 所在地
  2. /usr/lib/ruby/ :Ruby libraries
  3. /usr/lib/ruby/1.8/ :Ruby standard library
  4. /usr/lib/ruby/gems/1.8/gems/ :所有安裝的 Ruby gems Package
  5. /usr/bin/monit :monit 執行檔
  6. /etc/init.d/monit:monit的啟動 script,所有的 server 都會在 monit 啟動的時候,也順便幫你啟動好了(Mongrel,Haproxy)
  7. /usr/bin/haproxy :HAProxy 執行檔
  8. /usr/bin/mongrel_rails :Mongrel
  9. /etc/rails/ :configuration files
  10. /var/rails/ :所有 Deamon 的 .pid files
  11. /usr/rails/ :Rails app code 所在地。
  12. /var/log/monit.log :Monit 的 log
  13. /var/log/haproxy.log :HAProxy log
  14. /usr/rails/log/:Mongrels and Rails 的 log

延伸閱讀

5/15/2007

心愛的 Object 變心啦

剛剛發現一個很有趣的事情,寫 code 的時候,有兩個 Model :Person 跟 Setting ,他們呈現 1: 1 關係。結果當我想要創立一個新的 Person 的時候,為了希望能夠簡化設計,我就將其中一個人 a 當作預設值,每個新增 Person 的 Setting 直接 copy 一份過去,所以我就這樣寫




a = Person.find(1)
b = Person.new( :name => 'lala' )
b.setting = a.setting
b.save

我真的沒想太多,但是慘劇就發生了。a.setting 就變心到 b 去了。

>> a = User.find 1
=> #<Person:0x371ee38 @attributes={"updated_at"=>"2007-05-15 22:55:45", "nname"=>"hemidemi lala", "type"=>"GroupAdmin", "id"=>"1", "password"=>"123", "created_at"=>"2007-05-09 00:46:35"}>
>> a.setting
=> nil

也就是他自動的幫你把 a.setting 的 model 裡面的 foreign key 指定到新增的 b ,然後一去不回頭。要預防 object 變心方法也很簡單,就是幫新人找一份完全一模一樣的新伴侶即可。

a = Person.find(1)
b = Person.new( :name => 'lala' )
b.setting = a.setting.clone
b.save

Clone 是 Ruby object 裡面的 method ,作法就是 copy 一份新的 instance。雖然在實際使用上, 其實 clone 並不會直接 new 一份真的 instance,而是 new 一個 object ,然後將裡面 attribute 直接 reference 過去[註1]。但是在 ActiveRecord 裡面使用,因為是直接寫回資料庫,所以就完全沒副作用,所以可以盡量大方的使用。

註1

irb(main):001:0> class Klass
irb(main):002:1> attr_accessor :str
irb(main):003:1> end
irb(main):004:0> s1 = Klass.new => #<Klass:0x89e7c>
irb(main):007:0> s1.str = 'Hello' => "Hello"
irb(main):008:0> s2 = s1.clone => #<Klass:0x79d38 @str="Hello">
irb(main):009:0> s1.object_id => 282430
irb(main):010:0> s2.object_id => 249500
irb(main):011:0> s1.str.object_id => 261360
irb(main):012:0> s2.str.object_id => 261360

我們可以發現到 s1 跟 s2 其實是兩個不同的 object ,但是裡面的 attribute str 卻是同一個 object。




jRuby 代表的意義

最近看到這篇文章,裡面有一個問題

請問在JVM上跑Rails要幹嘛?

我在這邊講一下為什麼我對 jRuby 的期望那麼深。




我們看一下一段 code ,這是從 Ruby Cookbook 抓下來的範例

#!/usr/bin/env jruby
# random.jrb
require 'java'
include_class 'java.util.Random'
r = Random.new(123)
puts "Some random number #{r.nextInt % 10}"
r.seed = 456
puts "Another random number #{r.nextInt % 10}"

啟動的時候會出現這樣的結果

$ jruby random.jrb
Some random number 9
Another random number 0

我們仔細看到這段 jruby code,他是完完全全的 Ruby 程式,但是他使用 Random Number lib 是使用 JAVA lib。這代表 jRuby 讓 Ruby 可以隨意呼叫 Java Lib

Ruby 被 chanllenge 的其中一個地方就是「Third Party Lib 不足」,這個通常需要時間跟社群的持續的累積,但是如果直接使用 Java Lib,那不就是一個「別重新造輪子」的最好例子嗎?jRuby 不也就很直接的解決掉 Ruby Third Party Lib 不足的問題嗎?當然啦,jRuby 還有很多好處,但是最直接,也是最令人興奮的好處就是可以直接使用 Java Lib。

technorati tags:


5/10/2007

Twitter , Rails , Scalibility...More

Twitter 是一個最近非常熱的 Web Site,他們主要是可以利用簡訊,網頁更新自己的近況。Twitter 的開發者 Alex Payne 在接受訪問的時候,拋出了一個震撼性的議題
Rails Scalibility 到底好不好的問題
夠震撼吧。我發現到很多人都開始發表了 「Rails 遇到效能上的問題...」。看到只能說,這是 Rails 社群第一個大挑戰,但是請不要太過武斷就直接認為是 Rails 的問題。如果大家仔細了解這個事情的情況,就可以大略推估應該是 Twitter Team 成功的太快,整個 team 的成長跟不上網站的成長速度。我們來看看到底事情的始末。

故事開始

Alex Payne 是 Twitter Team 其中一員,他接受了 Radical Behavior 的訪問,當被問到 Ruby on Rails 如何應付高速成長的流量時,他指出他認為 Ruby on Rails 有不少 Scability 的議題,Alex 的論點如下
  1. Ruby is slow
  2. Rails 一次不能 connection multi database
  3. Rails 有些東西 component,性能消耗太大,必須不去使用
此話一出,當然引起了 Rails 社群的積極辯護,跟其他語言社群基於「良心」的建議。當然,我們 Rails 社群當中的老大DHH ,也不落人後的提出相關的建議。DHH 似乎覺得 Twitter 有點太過於懶惰了點,Twitter 比很多人幸運,有機會碰到那麼大的流量,那就該好好的想辦法處理相關的問題。而不是等著別人幫你解決你應該解決的問題。Open source 的成功,是來自使用者遇到相關的問題,並且解決他,回報給社群,這樣社群才會繼續壯大起來。
Second, when you work with open source and you discover new requirements not met by the software, it's your shining opportunity to give something back. Rather than just sit around idle waiting for some vendor to fix your problems, you get the unique chance of being a steward of your own destiny. To become a participant in the community rather than a mere spectator. This is especially true with frameworks like Rails that are implemented in high-level languages like Ruby. The barriers to contribution are exceptionally low
至於 Rails 一次連結到多個 DB 的問題,老實說,這根本不是問題,Dr. Nick 早就提出了Magic Multi Connections,可以有效解決這個問題。至於 Twitter 團隊是不知道這個東西,還是試過這個 Plugin 之後發現不夠用呢?InfoQ 訪問了 Twitter 另外一個開發者 Blaine Cook,說到 Dr. Nic 的 Plugin 很棒,很有幫助, Blaine Cook 表示目前 Twitter 的 DB Connection 是 600 req/sec,雖然很高,但是現在 Twitter 沒有暫時 DB 的問題。(那之前 alex 不是質疑說他們的問題是在 DB ? 到底 Twitter Bottleneck 在那裡?有點不了解他們的 Point 。
"Dr. Nic's approach is a great first step, and adds some welcome helpers to selecting from a number of database connections." but noted that "Twitter isn't currently database bound, and won't be for a while yet"
這時 Dr. Nic 也出來打圓場了,他說「Twitter 已經貢獻了 Jabber API 了」,其實不用苛求太多啦。
The guys at Twitter have already contributed code to Ruby community (Jabber API)
話說回來,Twitter Team 也似乎感受到 Rails 社群對他們處於制高點的期待,也開始對社群做出了幫助。Blaine Cook 在 SDForum 發表了 「Scaling Twitter」,裡面提到許多他們的問題以及解決方式。

進入討論

OK ,我們已經把故事講完了,現在討論正經點的事情。
到底是 Twitter 技術上不夠厲害,或是 Rails 不夠 Scalibility?
我認為這個 case 無法認定那一個結論是正確的。我們看看 twitter 的流量成長
很明顯的,Twitter 在 2007 年的流量是一個暴發戶的成長方式,通常網站遇到了這樣突然爆增的流量,原本編制的 RD 跟網管應該都完全無法應付吧,那麼一時之間無法解決也是非常正常的事情。所以,他們應該也不是太懶惰,只是成功的太突然,沒辦法吃下來。

再來,根據Blaine Cook 在 SDForum 發表的「Scaling Twitter」投影片,他們一共有 180 Mongrel Instances,但是卻只有使用兩個 DB Server,這似乎完全不符合一般大網站所遇到的情況(就是一海票的 DB Server,每個 table 都是橫切縱切隨便切)。當然我們不排除 twitter 的 application 型態其實並不需要太多 DB 的 request ,不過180 Mongrel Instant 居然只有 兩台 DB Server ,未免也太少了點,也不符合比例原則。

Twitter 是 host 在 joyent 上面的,像這種有一定規模的網站公司居然沒有自己專屬的系統管理者,光是這點這就看出 Twitter 成長真的太快了。一般來說,Web Host 很難做到專門為某個 Service 最佳化吧。Scalibility 本來就是軟體開發者跟系統管理者並肩合作的工程,Twitter 該多請幾個系統設計師摟。

至於 Ruby 是不是真的太慢,Rails 是不是真的不夠 Scalibilty呢?

這個問題我也不知道,畢竟我沒 run 過那麼大的 site,而我看到的 site 都是效率很不錯的。LAMP 發展了很久,Java 跟 Python 發展了很久,他們擁有 Ruby and Rails 社群無法比擬的成熟度,這是不爭的事實。但是,為什麼 Rails 會讓那麼多人趨之若騖,而非那些其他的語言呢?
那是因為 Ruby on Rails 擁有一些別人沒有的東西。而那些東西是很難被取代的。

今天這件事情沒有誰對誰錯,Twitter 點出這個議題,並且強調這個議題的重要性,Rails 社群接收到 Twitter 的訊息,大家一起幫忙解決,這是一個良性的溝通。我相信 Scalibility 是可以被克服的議題,也是值得一起去加油的議題。


延伸閱讀

5/06/2007

coderay

事情很簡單,就是每次我貼程式碼都覺得有很大的困擾,因為不只排版很麻煩,highlight 的問題更是折騰。我之前曾經改寫過一個用 ruby 寫成的 nopaste, 他是呼叫外部 highlight 的程式,查了一下,是這個:http://www.andre-simon.de/. 關於那個 nopaste 我就不多提了,寫得很爛,沒什麼營養。(這也是我想改寫的原因)

後來你也知道,我三分鐘熱度很嚴重,所以只改到一半就沒繼續做下去了。

過了一段時間,我看到了 Lighty RoR 上的〈Syntax Highlight 套件〉,讓我重新想起這件事,希望以後能有個好 highlight 方式。於是我灌了 syntax gem, coderay gem, 還有 highlight gem. 結果那次到底發生了什麼事,其實我也忘了。只記得好像是稍微試過之後就放到一邊去了吧。

今天我又想起這件事,所以又重新找起套件來了。看來看去,syntax 和 highlight 好像都不怎麼好用。最後我終於又試到了 coderay. 這次發現 coderay 相當好用,應該不用繼續找下去了。

gem install coderay
coderay -ruby -span < input.rb > output.rb.txt
這樣可以產生適合直接貼到支援 HTML/CSS 文章中的 HTML+CSS code.
coderay -ruby -page < input.rb > output.rb.html
這樣會直接產生一個完整的 XHTML 1.0 的頁面,相當方便。
coderay -ruby -html < input.rb > output.rb.html
如此產生出來的 CSS 會是用 class 的形式,適合原本就有 CSS 檔的地方。

(補充:另外除了 page 以外,span 和 html 視情況需要在前後加上 pre tag, 也許這部份可以再稍微 hack 一下使之不用加 pre tag, 省得事情變得更麻煩。)

沒錯,最大的好處就是直接用 command line 就好了,不用寫 ruby 程式。其他所支援的語法,在 lib/coderay/scanners 中可以找到,就我現在手上的 0.7.4.215 版中有:

c, delphi, html, nitro_xhtml, plaintext, rhtml, ruby, xml

擴充容易,自己去寫 you_want.rb 丟到 scanners 中就可以使用了。至於輸出格式,在 lib/coderay/encoders 中可以找到,同版本中有:

count, div, html, null, page, span, statistic, text, tokens, xml, yaml

別問我是什麼意思,沒試過我也不知道。不過 statistic 是產生報表,這不小心試過 XD 還有這也和上面一樣,擴充容易,自己寫 encoders 就可以用自己要的輸出格式。至於在 ruby 程式中呼叫 coderay, 這當然沒問題了,用法自己找,我懶得試了。唯一可惜的是 license 是 GPL 吧,我不確定我可不可以把他拉到 ludy 而不用讓 ludy 用 GPL 釋出。不過沒差,這程式內容還滿多的,我應該沒時間好好去改他。乖乖地用
就好了。

除此之外,還找到 rhighlight, 是上面提到的那個 highlight 程式的 binding. 我想這也應該滿值得一試的,但既然先發現 coderay 堪用了,也懶得繼續試,就先 note 起來就好。除此之外,還有 sourcecode2html, 不過這個看來還在開發中。

2007.05.06 godfat 真常

[News] Rails IDE 近況

Apatana

Apatana 自從跟 RadRails 整合後,最新出的第一個整合版本終於出了,下載點在這裡。不過 JavaEye 上面的群眾哀鴻遍野,大家發現整合的版本好像還不是很好,畢竟是 First Beta 版本,有很多問題是必然的:p 想當初 Lighty 1.5 的前幾個 Beta 版本不知道讓我出現多少次 CPU 100 %的情況。所以還是建議,可以先玩玩 Apatana Rails ,但是如果牽涉到正事的話,還是先用 RadRails 好了。

Eclipse

Eclipse DLTK for Ruby 方便 Eclipse 使用者寫 Ruby,而道喜的技術日記也發表了一篇 Ruby语言开发工具:Eclipse官方开发Ruby语言Editor,裡面講解 Windows 下面如何使用 Eclipse 開發 Ruby 程式。

5/05/2007

script/plugin

well, 由於我跟 Rails 不熟,所以很多地方只能憑空臆測,如果有誤也望請指點。很多跟 Rails 有直接關係的細節我也難以深究,所以大概只能從 Ruby 的角度看下去。總而言之呢,Rails 的 plugin 比起 rubygems 還更要簡單得多,根本沒有任何需要設定的部份,只要把目錄開好就可以輕易使用了。目錄結構大約是:

init.rb
install.rb
lib/*.rb
test/*.rb

init.rb 是 Rails 在 load up 時會執行的部份,所以 plugin 要把 init up 的程式碼放到這邊,例如最常見的恐怕是:
ActiveRecord::Base.send(:include, OOO)
這邊使用 send 而不是直接用 . 的緣故是 include 本身是 private method, 用 . 的會有 NoMethodError. 不過我建議可能可以考慮使用 __send__ 而非 send, 因為有些 class 的 send 有被 override, 一不小心可能會有未預期的錯誤。(雖然應該一看就知道會是出什麼問題就是了)

install.rb 則是在安裝時才會執行的程式,例如使用 script/plugin install OOO 就會先執行此 install.rb, 而日後不管何時使用此 plugin 時,install.rb 都不會再被執行。這部份可能會放的程式碼也許是會動到目錄結構的程式,例如在 app/models 下寫入 OOO_plugin.rb, 像這種只需要在安裝時執行一次即可。這樣做有什麼好處呢?我想感覺就有點像 script/generate OOO_plugin 的感覺吧。

至於 lib/ 則會自動加入 load path, 這部份跟 rubygems 裡 gemspec 裡的 s.require_path = 'lib' 是相同的概念。test/ 的部份也和 rubygems 相同。

接下來還有什麼好講的嗎?有的,參考〈acts_as_taggable Plugin 使用方式〉一文,可以發現 acts_as_taggable.rb 寫得相當複雜,這當然是有原因的。我參考了官方網站裡跟 plugin 有關的系列文章,發現 ActsAsOOO 是個相當常見的 plugin 模式,其中似乎有某種 pattern 在其中…。就是 ClassMethods, SingletonMethods, 與 InstanceMethods.

這樣做的原因很簡單,就只是加強模組化與避免名稱污染,所以多繞了幾個圈。在這種模式的做法下,ActiveRecord::Base 所多出來的 method 只有 acts_as_taggable. 而當你在寫 class MyModel < ActiveRecord::Base; acts_as_taggable; end 時,只有 MyModel 多出 class method => find_tagged_with, instance methods => tag_with, tag_list. 而不是整個 ActiveRecord::Base 都多出這些 methods.

其實這做法也有點像 meta-programming, 只不過是用 mixin 的形式。在前一陣子裡,由於我搞不太清楚 include 與 extend 的差異,所以調查了一下,並做了些筆記:(我知道多半連不上,所以 copy 一份到這)

=begin
2007.02.13

include 將 module 內的所有 method 原封不動拿過來,extend 則是在原本的 method 前面多加個 self. 即 class C 內,寫 include M 則追加 instance method. 如果寫 extend M 則追加 class method.

module 內也可以使用 include 與 extend, 結果同 class. include 為 instance level, extend 為 class level. 基本上這兩樣東西是很接近的。至於 module level 的 method, 則不受 include 也不受 extend 影響。其實跟 meta-programming 很像。

module M
def m; end # 只能由 include 或 extend 取用
def self.m; end # 只能用 M.m 來呼叫
end

class C
include C # 獲得 instance method m
extend C # 獲得 class method m
end
=end

除此之外,module 還有個特別的東西叫 module_function, 這東西我還沒仔細研究,但初步認識的結果是,他會造成指定的 method 同時定義 def m; end 與 def self.m; end 詳細的內容可以當成讀者練習,呵。(雖然其實可能就只是這樣而已)

回到 acts_as_taggable, 在 module Taggable 中唯一的 method 是:
def self.included(base)
base.extend(ClassMethods)
end
這個相信大家都很熟了,就是指當此 module 被 include 時會執行的 method, 有點 hook 的概念。這裡的 base 就是 includer, who include this module. 在 Taggable 中,當然就是 ActiveRecord::Base 了。extend 上面看到了,會促使該 class 擁有此 module 中所有的 method 成為 class method, 所以 ActiveRecord::Base 多了個 acts_as_taggable 為 class method. 如此一來,就能寫:
class MyModel < ActiveRecord::Base
acts_as_taggable
end

其實我覺得這樣實作有點過於複雜,我會比較喜歡這種形式:
class ActiveRecord::Base
def self.acts_as_taggable
# ...
end
end
這樣就一目了然了。缺點當然就是會比較難將此 method 丟給其他人用,例如假設哪天有了一個 class 是 PassiveRecord, 也想將 act_as_taggable 丟給他使用,可能就必須 copy & paste, 或是一些非常詭譎的方式,這邊不多提。

而在 acts_as_taggable 的最下方,呼叫了:
include ActiveRecord::Acts::Taggable::InstanceMethods
extend ActiveRecord::Acts::Taggable::SingletonMethods
的意思就非常明顯了,將 module InstanceMethods 的 method 變成 MyModel 的 instance method, 將 module SingletonMethods 的 method 變成 MyModel 的 class method.(不太明白為何要取做 SinglethonMethods, 也許只是因為 ClassMethods 用過了吧?)

原因是 :include 與 :extend 的 message receiver 是 MyModel 而不是 ActiveRecord::Base, 所以被擴充的是 MyModel; ActiveRecord::Base 不受影響。

大抵上就是這樣了。

abstract:
init.rb 在 Rails load up 後會執行
install.rb 在 plugin install 後會執行
lib/ 自動加入 load path

下次再研究 rake 要怎麼用,相信以之拿來做 test 會很方便。

2007.05.05 godfat 真常

延伸閱讀

5/04/2007

Beast 中文化成功


介紹完怎麼安裝 Beast ,我們廢話不多說,直接進入 Beast 中文化。Beast 的 I18N 是採用 GETTEXT 來做的,意思就是只要寫一個 po 檔即可處理大部份的中文化東西。不過還是有很多小地方沒有中文化,依舊需要一一 check。右圖就是成品,這裡就是 po檔下載點。實做方式以及po檔均參考 JavaEye 的 Suninny 先生撰寫的 [分享]Beast中文Gettext PO档,感謝他的付出。


中文化方式如下
  1. 採用之前說好的方式安裝 beast。
  2. 安裝 GETTEXT GEM
    gem i gettext
  3. 撰寫轉換rake file,新增一個檔案叫做 lib/tasks/gettext.rake,內容是
    desc "Create mo-files for L10n"
    task :makemo do
    require 'gettext/utils'
    GetText.create_mofiles(true, "po", "locale")
    end
  4. 新增一個資料夾 po/zh_tw/ ,將這個po檔放入這個資料夾裡面
  5. 產生相對應的 mo 檔,請打入
    rake makemo
  6. config/enviroment.rb 裡面,下面這一段 gettext 的部份要註解起來,我也不確定為什麼,反正會產生 error
    begin
    require 'gettext/rails'
    GetText.locale = "zh" # Change this to your preference language
    puts "GetText found!"
    rescue MissingSourceFile, LoadError
    puts "GetText not found. Using English."
    class ActionView::Base
    def _(s)
    s
    end
    end
    end
  7. app/controllers/application.rb 部份加上
    class ApplicationController < ActionController::Base
    #init_gettext "beast" if Object.const_defined?(:GetText)
    require 'gettext/rails'
    init_gettext "beast" if Object.const_defined?(:GetText)
    GetText.locale = 'zh_tw'
    end

    裡面 zh_tw 就是你指定的語系
  8. 重起 server 即可
如此就沒問題了。Beast I18N 做的不錯,唯一缺點就是 doc 太少,花了點時間才找到相關資料。

RIA on Rails ?


M$ 最近推出了 Silverlight 計畫,是一種類似 flash 的 Browser Plugin,目的也是為了搶供 Flash 的 Multimedia 龍頭位置。雖然只支援 Win + MAC 讓人嚴重懷疑他的接受度,畢竟 Flash 對 Linux 支援度算是很好了。當然他的成敗不是我們所 care 的東西,我們 care 的是 Ruby on Rails。

根據 Ruby Inside 說法,M$ 在 Silverlight 裡面放了 CLR 的 subset,而且 silverlight 的 CLR 也支援 Ruby。
Microsoft has officially announced C#, Javascript, VB, Python and Ruby support for Silverlight's CLR.
總之,那一天,或許我們可以在 Sliverlight 上面寫 Ruby 也說不定。這算 Ruby 的一種 RIA Solution嗎 :p 不管如何對 Ruby 都是好事,靜觀其變吧。

另外值得一提,Flex on Rails 好像越來越成熟了,我也是今天才發現這個網站 http://flexonrails.net/ ,裡面的東西還蠻多的。教導的都是 Rails 如何跟 Flex 做一個緊密的結合。應該又是另外一個 Ruby RIA Solution吧。

不過老實說,我對 RIA 以及 Flash 這方面的東西,真的很不熟,所以我講話還是小聲點比較好 :p

5/03/2007

簡單的建立自己的 Gem Package

一不小心就跟 Godfat 寫的議題衝突,不過沒關係,我是走輕鬆簡單取向的。這次要講的就是如何將自己寫的程式包成 Ruby GEM,本文的參考自 JavaEye 上面的创建自己的ruby Gems,程式也是來自計算民國跟學年度的 Plugin的範例。



結構

要建立一個 GEM Package,首先要先搞清楚結構。因為我們是走極簡路線的,所以現在一開始連 unit-test 都不要 test。將你的 GEM Package 設定為下面的結構。

-----gem.spec
|
--README
|
--lib/

裡面 lib 就是放所有程式的所在。

結構檔

根目錄下面有 gem.spec,跟 README 兩個檔案。其中 gem.spec 是描述 GEM Package 的結構檔,也是最重要的檔案,裡面記載了這個 GEM Package 絕大多數的資訊。可以說學會了寫這個檔案,就學會了打包 GEM Package。這裡要講的是其實 gem.spec 檔名是我隨便取的,檔名不限定,而且他的格式可以使用 ruby 或是 YAML 語法的,這裡先使用 Ruby 語法。

require 'rubygems'
SPEC=Gem::Specification.new do |s|
s.name="Taiwan_Year"
s.version='0.01'
s.author='thegiive'
s.email="thegiive at gmail dot com"
s.homepage="http://lightyror.thegiive.net/"
s.platform=Gem::Platform::RUBY
s.summary="為台灣學年度跟民國所寫的Plugin"
condidates =Dir.glob("{bin,lib,docs,test}/**/*")
s.files=condidates.delete_if do |item|
item.include?("CVS")|| item.include?("rdoc")
end
s.require_path="lib"
s.has_rdoc=false
s.extra_rdoc_files=["README"]
end
裡面
  1. s.name 就是這個 gem 的名稱,也是他打包出來的檔名,也就是說這個 gem package 會叫做 Taiwan_Year-0.01.gem。
  2. s.version 部分 godfat 有解釋過,盡量從 0.0.1 而不是從 0.0.0 開始寫起。
  3. s.summary 就是當你打入 gem list 出現的簡單文字,安裝了本 gem ,按下 gem list 這裡會出現
    sources (0.0.1)
    This package provides download sources for remote gem installation

    Taiwan_Year (0.01)
    為台灣的學年度跟民國寫的 Plugin

    vim-ruby (2006.07.11)
    Ruby configuration files for Vim. Run 'vim-ruby-install.rb' to
    complete installation.
    這樣的情況。

再來就是 README ,就是打入一些information進去摟。

放入程式

最後把程式放入到 lib 底下,這裡是使用計算民國跟學年度的 Plugin的範例,把他放入 lib/year.rb 檔案裡面
class Time

def roc_year
self.year -
1911
end

def student_year
if self.month
< 7
self.roc_year -
1
else
self.roc_year
end
end

end
打包 gem

當程式都放好後,開始打包 gem 了。我們這裡使用 gem build 指令來打包
gem build gem.spec
這裡面的 gem.spec 就是你剛剛寫的結構檔的檔名。成功了之後,你就會發現我們已經產生了一個 Taiwan_Year-0.01.gem。

安裝 gem

要安裝這個剛打包好的 gem 就是直接打
gem i TaiwanYear-0.01.gem
即可。

使用 gem

要使用剛剛安裝好的 gem package ,首先要 require 'rubygems' ,告訴程式這個是使用 gem 的程式。

其次要搞清楚程式檔名,像我雖然 GEM Package 叫做 TaiwanYear ,但是我的程式其實是放在 lib/year.rb 底下的,所以使用上還是得必須 require 'year' 。

解決方式很簡單,當我們放入程式到 lib 裡面的時候,請將程式名稱取的跟 GEM Package 名字相同即可。像是當初在打包時,我們就將程式放在 lib/TaiwanYear.rb 底下,以後使用上就可以這樣使用。

require 'rubygems'
require 'TaiwanYear'
puts Time.now.roc_year
如此,我們已經學會如何打包 gem 了。

5/02/2007

require 'rubygems'

寫到一半因為有其他事所以先放著,結果回來就是八小時後了…。
有夠累。

==

大致調查(survey?)了一下要怎麼樣把程式 package 成 rubygem, 主要參考書籍當然是 Programming Ruby 2nd, 這本我看好久還是沒看完的書。(還不都是因為他太厚了…)在 Creating Your Own Gems 這一節中,相當詳細地描述了如何打包(在 MtG 中,不知為何把 entwine 翻譯成打包,瞬間從 spell 的意味變成像外帶一樣)。這邊將簡單示範一次我如何發佈 ludy 的。

首先呢,rubygems 有建議的檔案配置(layout),可以不依照這個規則來做,但一般來說建議使用跟別人相同的方式。所有雜七雜八的東西放在根目錄下,例如 ludy-0.0.1 裡面放的有:ludy.gemspec, LICENSE, NOTICE, README. 其他目錄則是很常見的:bin, doc, lib, test. 我沒有 bin 也沒有 doc, 所以只有放 lib 和 test 兩個目錄。

顧名思義,lib 裡面放的就是 ludy 本身的 source code, 專門拿來給人 require 用。而 test 底下則是所有的 test program, 以 tc_ 開頭的是 TestCase, 可以單獨執行,但主要是由 ts_ 開頭的 TestSuite 去 require 起來,然後 ts_ 會自動執行這些 tc_.

所以我的 ts_ludy.rb 裡面只有兩行:

require(File.join(File.dirname(__FILE__), '..', 'lib', 'ludy'))
require_all_in_dir __FILE__

第一行是把 lib/ludy.rb require 進來,這是使用 ludy 部份元件的必要手續。這樣做的理由是有些 path 問題很煩,所以我把處理 path 的 func 寫在 ludy.rb 中,如此一來需要確保路徑的只有 ludy.rb 這一個檔案。至於為何這裡要使用相對路徑去 require 呢?因為我堅持兩種使用 ludy 的方式,一個是安裝成 gem, 另一個是直接丟到 project 目錄下,使用路徑去 require. 前者不用說,可以有一個很標準的方式使用,但後者就不一定了。

為了處理這個問題,ludy 內部需要某些其他 ludy tool 時,就改用 require_ludy, 集中處理 require 問題,避免重複把相同路徑加入 load path. 使用者當然也可以使用 require_ludy, 或是依然習慣使用 require, 那麼只要確定自己 require 的 path 是正確的就好了,ludy 內部不會有路徑問題。至於 gem version, require 的方式就很單純用 require 'ludy/tool_name' 或是 require_ludy 'tool_name' 就好了。

第二行,則是把所有 tc_ 開頭的 TestCase require 進 TestSuite 裡面,(其實是同個目錄下所有的 .rb 檔,只是現在 test/ 下只有 tc_ 和正在執行的 ts_ 而已)unit test 就會自動執行了。不過這個 require_all_in_dir 的第一個參數卻是檔案名稱,名字好像取得不太好的樣子。

ok, 也就是說我要打包的東西是 ludy.gemspec, LICENSE, NOTICE, README, lib/*.rb, test/*.rb. 其中 lib/ 底下還有其他資料夾,也要一併打包。那麼寫好的 gemspec 就是


require 'rubygems'

spec = Gem::Specification.new{|s|
s.name = 'ludy'
s.version = '0.0.1'
s.author = 'Lin Jen-Shin(a.k.a. godfat)'
s.email = 'strip number: 135godfat7911@246gmail.890com'
s.homepage = 'http://ludy.rubyforge.org/'
s.platform = Gem::Platform::RUBY
s.summary = 'Aims to extend Ruby standard library, providing some useful
tools that\'s not existed in the standard library.'
candidates = Dir.glob '{bin,doc,lib,test}/**/*'
candidates+= Dir.glob '*'
s.files = candidates.delete_if{|item|
item.include?('CVS') || item.include?('rdoc') ||
File.extname(item) == '.gem'
}

s.require_path = 'lib'
s.autorequire = 'ludy'
s.test_file = 'test/ts_ludy.rb'
s.has_rdoc = false
# s.extra_rdoc_files = []
# s.add_dependency 'multi', '>=0.1'
}

if $0 == __FILE__
Gem::manage_gems
Gem::Builder.new(spec).build
end


其中 Dir.glob 的部份我還沒研究,所以寫得很爛,居然用補正的方式。反正就是全部抄自 Programming Ruby 2nd, 再做一點修修改改。其中最下面的 if $0 == __FILE__ 可以判斷這個程式是不是直接被 ruby interpreter 執行?如果是的話,就呼叫建立 rubygem 的 method. 不是的話,就只要定義 spec 就好了。

前者就是:
ruby ludy.gemspec
或是如果有加 #! 的話:
./ludy.gemspec
如果 ludy.gemspec 在現在目錄下的話。

後者就是:
gem build ludy.gemspec

這兩種方式都可以產生 ludy-0.0.1.gem. 接著再下:
gem install ludy
就可以正確安裝 ludy gem 了。至於 remote 的部份呢?只要上傳到 rubyforge 的 file release system 上,rubyforge 每日會有數次掃描裡面的 *.gem, 自動放到 rubygems 的 repository. 這件事我已經做完了,所以現在可以直接:

gem install ludy

就能看到我寫的一點程式了 :)
不過其實我只是在測試功能而已,本來是想標版本號 0.0.0, 可惜這樣用:

gem uninstall ludy

時會出一點問題。所以就還是標成 0.0.1, 但未來的更新暫時都不改版本號。正式釋出時大概會是 0.0.2 吧 :p 還是歡迎大家抓回去試試,使用方法全在 unit test 裡面,文件懶得寫了。

homepage:
http://ludy.rubyforge.org/
有空時會弄個應有的版面出來。

2007.05.02 godfat 真常