site gotoknow.org ที่เขียนด้วย Rails
ในชื่อ opensource project ว่า knowleageVolution หรือย่อๆว่า kv
ผมก็เลยฉวยโอกาส checkout เจ้า kv ออกมาเรียนรู้เสียเลย
สำหรับผมการอ่าน code ของคนอื่น ถือเป็นการเรียนรู้ที่ดีมาก
เพราะจะทำให้เราเห็นมุมมองอะไรบางอย่างที่เราไม่เคยเห็น
(ยิ่งของอาจารย์ธวัชชัย ยิ่งต้องควรอ่าน)
พอเปิด code ของ Controller มา
สิ่งแรกที่สะดุดสายตา ก็คือ การ reuse code โดยใช้ filter feature ของ rails
class BlogController < ApplicationController
before_filter :authenticate_user, :only => [:create, :post, :edit, :theme, :enable, :disable, :blog_post_edit, :blog_post_delete, :blog_post_delete_comment]
before_filter :get_blog_and_owner, :except => [:index, :tag_index, :tag, :create, :enable, :rss20, :rss20_redirect]
before_filter :authenticate_owner_as_user, :only => [:post, :edit, :theme, :disable, :blog_post_edit, :blog_post_delete, :blog_post_delete_comment]
before_filter :get_post, :only => [:blog_post_view, :blog_post_edit, :blog_post_delete, :blog_post_comment, :blog_post_delete_comment]
before_filter :make_feed, :except => [:index, :create, :tag_index, :tag, :enable, :rss20, :rss20_redirect, :blog_post_newer, :blog_post_older, :recently_commented_posts]
before_filter :set_icon
before_filter :set_theme, :except => [:index, :tag_index, :tag, :create, :edit, :theme, :enable, :rss20, :rss20_redirect, :recently_commented_posts]
before_filter :process_page, :only => [:recently_commented_posts]
before_filter :set_blog_context_menu, :only => [:blog, :toc, :blog_post_view]
after_filter :expire_blog_rss, :only => [:post, :blog_post_edit, :blog_post_delete]
after_filter :expire_planet_rss, :only => [:post, :blog_post_edit, :blog_post_delete]
after_filter :expire_blogs_fragments, :only => [:create, :edit, :enable, :disable]
after_filter :expire_blog_fragments, :only => :edit
after_filter :expire_post_fragments, :only => [:blog_post_edit, :blog_post_delete, :blog_post_comment, :blog_post_delete_comment]
after_filter :expire_home_index_posts, :only => [:edit, :enable, :disable, :post, :blog_post_edit, :blog_post_delete, :blog_post_comment, :blog_post_delete_comment]
after_filter :expire_post_related_fragments, :only => [:post, :blog_post_edit, :blog_post_delete]
caches_page :blog_post_rss20
def index
@subject = _("Recent Blogs")
@pages, @blogs = paginate(:blogs, :conditions => "disabled = false", :order => "created_at DESC")
@feed = [_("Recent Blogs"), url_for(blog_rss20_url)]
render :template => "blog/blog_list"
end
...
ใน rails ทุกๆ action ที่ request เข้ามา เราสามารถ attach filter เข้ากับ
request นั้นได้ โดยมี filter ให้เลือก 3 แบบคือ before, after, around
(เหมือน AOP ไหม)
ปกติที่ใช้กัน ก็มักจะเอาไปทำพวก logging, authenticate, authorize
แต่ของ kv ไปไกลกว่านั้น ก็คือเอามา reuse logic ส่วนที่ใช้ร่วมกันด้วย
ยกตัวอย่าง สมมติเราขอ request ไปที่ http://gotoknow.org/blog/periphery/125115
ตัว route.rb จะ map url request ของเราเข้ากับ blog_controller.rb และเรียกใช้ method ที่ชื่อ blog_post_view
# route.rb
map.blog_post_view "blog/:address/:id", :controller => "blog", :action => "blog_post_view", :id => /\d+/
# blog_controller.rb
def blog_post_view
@post.increase_hit(session.session_id, request.remote_ip)
process_comment_page @post
@context_menu << :post
end
จาก code จะเห็นว่า logic ใน blog_post_view มันดูน้อยเสียเหลือเกิน จนไม่น่าจะทำงานอะไรได้
ที่เป็นเช่นนี้เพราะ logic ส่วนใหญ่ถูกดึงออกไปอยู่ใน fiter หมดแล้ว
ถ้าเราไปไล่ดู code ในส่วน filter เราก็จะเห็นว่า ก่อนที่จะเข้า blog_post_view
มันจะมี sequence ดังนี้
+ get_blog_and_owner
+ get_post
+ make_feed
+ set_icon
+ set_theme
+ set_blog_context_menu
=> blog_post_view
จุดอ่อนของวิธีนี้ ก็คือตอนที่ไล่ code ครั้งแรกมันอาจจะทำให้สับสนว่า logic ต่างๆมันเป็นอย่างไรบ้าง
เพราะวิธีการ declare filter มันมีทั้งแบบที่บอกว่า ฉันครอบคลุมเฉพาะอันนี้นะ (only)
กับ ฉันไม่ครอบคลุมเรื่องนี้นะ (exclude) ทำให้ต้องสลับ logic ไปมา เวลาไล่สายตา
(โชคดีอย่างหนึ่งที่ rails มันสามารถรวม code ของ filter ไว้ใน file เดียวกันได้
ทำให้การไล่ code ไม่ยากจนเกินไป)
แต่หลังจากคุ้นกับ code แล้ว การแยก logic ใน pattern แบบนี้
ก็ไม่ได้ทำให้เกิดอุปสรรคอะไร
ข้อสังเกต: rails implement feature Filter ในแบบ recursive
ดังนั้นเวลาเกิด error ขึ้นมา, stack trace ก็เลยยาวเหยียดแบบนี้
...
.//app/controllers/application.rb:275:in `add_object_comment'
.//app/controllers/blog_controller.rb:275:in `blog_post_comment'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/base.rb:1095:in `perform_action_without_filters'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:632:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:438:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:634:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:638:in `call_filter'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:449:in `call'
/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.3/lib/action_controller/filters.rb:637:in `call_filter'
...