Friday, September 07, 2007

before_filter

ช่วงนี้อ่านที่พี่ Conductor กับ อาจารย์ธวัชชัยกำลัง Tune performance
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'
...

Related link from Roti

No comments: