softdev

บล็อคใน Ruby

posted on 01 Apr 2009 23:38 by wonam in softdev

(ไม่ใช่การบล็อคเว็บนะครับ)

มีหลายสาเหตุที่ทำให้ผมชอบภาษา Ruby ทั้ง ๆ ที่หลายคนคิดว่าไวยากรณ์มันช่างประหลาดสิ้นดี

ขอยกตัวอย่างเล่น ๆ ตัวอย่างหนึ่ง สมมติผมเขียนโปรแกรมสำหรับพิมพ์ค่าในรายการไว้ เป็นดังด้านล่าง

def print_array(a)
  a.each do |x|
    puts "#{x}"
  end
end

อธิบาย: 1. คำสั่ง each จะเรียก block ด้านหลัง (ตั้งแต่ do ถึง end) โดยส่งสมาชิกของรายการไปให้ทีละตัว block ด้านหลังนี่จะรับตัวแปรหนึ่งตัวคือ x เขียนในกรอบขีดตั้ง
2. การใส่ #{x} ในสตริงคือการให้เอาค่าตัวแปร x ไปแทนในส่วนนั้นของสตริง

เวลาใช้ก็ไม่มีอะไรครับ เรียก print_array([1,2,3,4]) ก็จะพิมพ์ผลลัพธ์ออกมา

แต่สมมติว่าผมอยากให้เมท็อดดังกล่าวเรียกแบบนี้ได้เลย [1,2,3,4].print ผมก็เอาเมท็อดดังกล่าวนี่ไปเพิ่มในคลาส Array ได้เลยครับ ดังด้านล่าง

class Array
  def print
    self.each do |x|
      puts "#{x}"
    end
  end
end

(จริง ๆ ไม่ควรเพิ่มอะไรในคลาสมาตรฐานแบบพร่ำเพรื่อแต่ว่า อันนี้แสดงให้ดูเป็นตัวอย่างนะครับ)

ทีนี้ ถ้าผมอยากจะให้เมท็อดนี้ปรับวิธีในการพิมพ์ได้ เช่นถ้าผมอยากจะพิมพ์เลขสุ่มตามหลังเลขในรายการ ใน Ruby มีวิธีในการส่งส่วนของโปรแกรม (ซึ่งเรียกว่า block) เข้าไปในเมท็อดได้ โดยจะเรียกประมาณนี้

[1,2,3,4].print do |x|
  puts "#{x} #{rand(100)}"
end

ก็สามารถทำได้ โดยเมท็อดข้างต้นก็จะเปลี่ยนไปเป็น

  def print
    self.each do |x|
      if block_given?
        yield x
      else
        puts "#{x}"
      end
    end
  end

เมท็อดด้านบนถ้าไม่ส่ง block มาก็จะพิมพ์ด้วยวิธีธรรมดาปกติครับ

ตัวอย่างนี้อาจจะดูเป็นของเล่นสักหน่อย แต่ว่าความสามารถในการ "จิ้ม" เอา block เป็นพารามิเตอร์ในเมท็อดได้แบบแนบเนียนสนิทนี้ ทำให้เราทำอะไรน่าสนุกใน Ruby ได้มากมาย เช่นการพัฒนา Domain Specific Language บน Ruby ครับ (จะเขียนถึงภายหลังถ้ามีเวลาครับ)

ปกติเวลาทำ automated testing เรามักหวังว่าจะเห็นผลดังกราฟด้านล่าง

 

นั่นคือ เหนื่อยหน่อยตอนเริ่ม เสร็จแล้วก็จะสบายขึ้น มีส่วนประหยัดแรงได้ตามมา

ที่มารูป: ดัดแปลงจากสไลด์โดย Uwe Guhl ที่นำมาจากเว็บ  http://xunitpatterns.com/

เมื่อคืนออกข้อสอบ ข้อหนึ่งเกี่ยวกับ automated testing ให้กราฟด้านล่างมา

แล้วถามว่าเกิดอะไรขึ้น และน่าจะมาจากสาเหตุอะไร?

HtmlUnit

posted on 15 Feb 2009 15:00 by wonam in softdev

การโต้เถียงที่ไม่มีทีท่าว่าจะจบเกี่ยวกับหลักการทดสอบซอฟต์แวร์เรื่องหนึ่งก็คือประเด็นเกี่ยวกับการทดสอบแบบอัตโนมัติ (automated testing) ว่าควรจะทำแค่ไหน ทำในประเด็นใดบ้าง

อย่างไรก็ตาม ในหลาย ๆ สถานการณ์ทางเลือกอื่นนอกจากการทดสอบแบบอัตโนมัตินั้นแทบจะไม่มี เช่น ในการทดสอบประสิทธิภาพ หรือการทดสอบการรับภาระ (load testing) ยกเว้นทีมทดสอบจะใหญ่พอที่จะหาคนมาหลายสิบคนให้เข้ามาใช้ระบบพร้อม ๆ กันได้

เมื่อวันสองวันก่อน มีคนมาให้ทดลองทำการทดสอบภาระของระบบที่เขาพัฒนาขึ้น ผมไปทดลองเล่น ๆ ดูก็พบปัญหาหลายอย่างในการทดสอบที่ทำให้เครื่องมือเก่าที่ผมเคยเขียนไว้มันทำงานไม่ได้ เช่น หน้าเว็บเต็มไปด้วย javascript หรือที่เหนื่อยหน่อย ก็คือ javascript ที่เขียนมันใช้ได้กับเฉพาะ IE 6 แต่เครื่องผมดันเป็น ubuntu แล้วก็มีแต่ firefox (แน่นอนว่าผมคงต้องหาทางเกลี้ยกล่อมให้เขาค่อย ๆ ปรับให้มันเข้ามาตรฐานมากขึ้น แต่คงไม่ใช่สิ่งที่เป็นความสำคัญอันดับแรก)

ผมก็ลองกดนิด ๆ หน่อย ๆ ไปตามเรื่อง ปุ่มไหนกดไม่ได้ก็ช่างมันก่อน พอทดลองได้นิดหน่อยก็ถึงเวลาเลือกใช้เครื่องมือสำหรับสร้างหุ่นยนต์ที่เรียกใช้เว็บ

ผมพยายามเลือกเครื่องมือที่เหมาะกับงานนี้ เช่น สามารถทำงานกับหน้าเว็บที่มี javascript ได้  เขียนได้ไม่ยาก  และยิ่งไปกว่านั้นก็คือ เมื่อผมทำเสร็จแล้ว ผมสามารถเอาชุดทดสอบที่ผมทำมาคร่าว ๆ ไปให้กับทีมพัฒนาไปดัดแปลงและใช้ต่อได้

ถือว่าผมโชคดีมากที่ผมไปเจอ HtmlUnit

ตอนแรกไม่ได้รู้สึกว่าโชคดีขนาดนี้ เพราะว่าเห็นว่ามันน่าจะไม่ต่างจากไลบรารีอื่น ๆ มากนัก นอกจากที่เรียกทำงาน javascript ได้ด้วย หลังจากลองได้สักพักก็พบว่า HtmlUnit มันแกร่งมาก (อาจเป็นเพราะ Rhino javascript library ที่มันใช้ก็ได้) แข็งแกร่งขนาดรัน javascript สำหรับ IE ได้ด้วย!! (ผมกด Firefox เล่นมันยังไม่ไปให้)

ขอมาเล่าคร่าว ๆ แค่นี้ก่อน ไว้เขียนหุ่นยนต์ได้เป็นเรื่องเป็นราว (และใช้ง่าย) เมื่อไหร่จะมาเล่าต่อครับ ขอไปเมามันต่อก่อน

เขียน blog บน Merb

posted on 28 Dec 2008 16:20 by wonam in softdev

ผมได้ทดลองใช้ merb เมื่อสองสามวันก่อน อย่างไรก็ตามหลังจากได้ทดลองหาตัวอย่างที่จะได้หัดตามจากอินเทอร์เน็ต กลับพบว่าตัวอย่างพวกนั้นเก่าเกินไปแล้ว หรือไม่ก็ไม่ได้ใช้ Datamapper เป็น ORM เหมือนที่ผมต้องการทดลองเล่น

แน่นอนว่าผมทราบข่าว Merb 2 = Rails 3 แล้ว และนี่ก็เป็นสาเหตุที่เอา Merb มาหัด เพราะว่าอยากเล่น Merb สักหน่อยก่อน จะได้ทราบว่าอะไรที่มันน่าจะน่าสนใจบ้าง

บทความนี้เอาขั้นตอนหลัก ๆ มาจากบทความ "merb + datamapper + noob: quick start" แต่ตัดบางอย่างออก และปรับการใช้งานให้ตรงกับ Merb เวอร์ชั่นที่ผมใช้อยู่ (1.0.6) นอกจากนี้ยังคาดว่าผู้อ่านน่าจะมีประสบการณ์การใช้ Rails มาบ้างครับ

ขั้นแรกคือติดตั้ง merb ถ้าเป็น ubuntu ก็ทำประมาณ

$ sudo gem install merb
แล้วก็ติดตั้ง Datamapper และส่วนติดต่อกับ Sqlite3
$ sudo gem install datamapper merb_datamapper do_sqlite3
จากนั้นก็สร้าง Merb application
$ merb-gen app blog
คำสั่ง merb-gen app จะสร้าง application ว่าง ๆ ขึ้นมา ที่ใช้ Datamapper เป็น ORM

ใน blog/ จะมีโครงสร้างของไดเร็กทอรีที่ผู้ใช้ Rails น่าจะคุ้นเคย ไฟล์ที่เกี่ยวข้องกับการตั้งค่าจะอยู่ในไดเร็กทอรี config/ ไฟล์ที่น่าสนใจก็มีเช่น dependencies.rb (ระบุว่าใช้โมดูลไหนบ้าง --- การถอดประกอบเป็นจุดเด่นของ merb), init.rb (ใช้ระบุว่าจะเลือกใช้อะไรเป็น ORM, เป็นระบบทดสอบ และระบบ template), และ database.yml (เช่นเดียวกับใน Rails ที่ระบุข้อมูลเกี่ยวกับฐานข้อมูลที่ใช้).

ในขั้นนี้ของที่ merb สร้างมาให้น่าจะโอเคแล้ว เราก็จะใช้ไปตามนั้นก่อน

สร้างโมเดล

ได้เวลามาสร้างโมเดลกันครับ สั่งคำสั่งด้านล่าง (ในไดเร็กทอรี blog/)

$ merb-gen model post
เพื่อสร้างโมเดล Post ครับ ซึ่งโปรแกรมของโมเดลดังกล่าวจะอยู่ที่ app/models/post.rb

ในการพัฒนาใน Rails เราจะเขียนเพียงแค่ migration จากนั้นข้อมูลเกี่ยวกับโมเดลจะถูกอ่านจากตารางในฐานข้อมูล แต่ในการพัฒนาด้วย Datamapper นั้นเราจะระบุข้อมูลของโมเดล เช่นพวก property ในคลาสเลย ดังนั้นแก้ไฟล์ post.rb ให้เป็น

class Post
  include DataMapper::Resource
  
  property :id, Serial
  property :title, String
  property :body, String
end
หลังจากนั้นเราจะต้องจัดการสร้างตารางของโมเดลในฐานข้อมูล ในกรณีที่เราปรับเปลี่ยน property ง่าย ๆ (เช่น สร้างโมเดลหรือเพิ่ม property) merb สามารถจะปรับตารางให้เราได้โดยไม่ต้องเขียน migration เลย โดยเราสามารถเรียก
$ rake db:autoupgrade
เพื่อปรับโครงสร้างตารางได้ นอกจากนี้ยังมีอีกคำสั่งที่สามารถใช้เพื่อปรับโครงสร้างตารางเช่นกัน แต่การปรับดังกล่าวจะเป็นการปรับแบบทำลาย (ข้อมูลเดิมหายหมด) คือการเรียก rake db:automigrate ดังนั้นผมจะใช้ db:autoupgrade แทน

เมื่อสร้างเสร็จแล้ว เราจะลองเล่นใน interactive console ของ merb ก่อน เพื่อใส่ post ลงไปหลาย ๆ อันให้เรามีข้อมูลแสดงนะครับ เรียก interactive console โดยสั่ง

$ merb -i
irb(main):001:0> p1 = Post.new
irb(main):002:0> p1.title = 'Hello'
irb(main):003:0> p1.body = 'This is my first post'
irb(main):004:0> p1.save

 ~ INSERT INTO "posts" ("title", "body") 
          VALUES ('Hello!', 'This is my first post')
=> true
irb(main):001:0> p2 = Post.new
irb(main):002:0> p2.title = 'Second post'
irb(main):003:0> p2.body = 'Here I post again'
irb(main):004:0> p2.save
 ~ INSERT INTO "posts" ("title", "body") 
          VALUES ('Second post', 'Here I post again')
=> true

สร้างคอนโทรเลอร์

ตอนนี้เราก็มีข้อมูลพอจะให้แสดงได้แล้ว ไปสร้างคอนโทรเลอร์กันครับ เรียกคำสั่ง

$ merb-gen controller posts
เพื่อสร้างคอนโทรเลอร์ Posts ในแฟ้ม app/controllers/posts.rb สังเกตว่าจะไม่มีคำว่า Controller ตามหลังชื่อเหมือนใน rails นะครับ

เข้าไปเพิ่มให้เมท็อด index ค้น post ทั้งหมดออกมาครับ

class Posts < Application
  def index
    @posts = Post.all
    render
  end  
end
สังเกตว่าใน merb ทุกครั้งที่ทำงานเสร็จเราจะต้องเรียกเมท็อด render ให้แสดง view เอง (ใน rails จะทำให้อัตโนมัติ) การเรียกดังกล่าวจะนำ view สำหรับเมท็อดนี้มาแสดง ข้อสังเกตอีกประการคือเราเรียก Post.all แทนที่จะเป็น Post.find(:all) เหมือนตอนที่เขียนด้วย Active Record

จากนั้นไปสร้าง view เพื่อแสดงผล post กันครับ ไปแก้ไฟล์ views/posts/index.html.erb ให้เป็นดังนี้ครับ

<h1>My posts</h1>
<% for post in @posts %>
  <h2><%= post.title %></h2>
  <p><%= post.body %></p>
<% end %>

เราพร้อมที่จะเห็นหน้าเว็บแล้วนะครับ เรียกเว็บบราวเซอร์ที่มากับ merb ด้วยคำสั่ง
$ merb
จากนั้นเข้า browser เปิดไปที่ http://localhost:4000/posts หรือ http://127.0.0.1:4000/posts ได้เลยครับ!

เพิ่ม post ใหม่

ไปสร้างหน้าสำหรับป้อน post ใหม่กันครับ เราจะเพิ่มลิงก์ "add new post" ที่ด้านล่างของหน้า index ครับ โดยเติม คำสั่งด้านล่างเข้าไปใน index.html.erb ครับ

<%= link_to 'add new post', url(:controller => :posts, 
                                :action => :new) %>
เมท็อด url จะคืนค่า url สำหรับ action นั้นออกมา เท่าที่ผมทราบเราสามารถทำอะไรกับการอ้างชื่อ action ได้มากมาย โดยใช้ router ของ merb แต่ตอนนี้เราจะทำในลักษณะเดียวกับใน rails ไปก่อน

จากนั้นไปสร้างเมท็อด new ในคอนโทรเลอร์ Posts ที่แสดง view สำหรับแสดงฟอร์มครับ

class Posts < Application
  # ...
  def new
    @post = Post.new
    render
  end
end
แล้วก็ไปสร้าง view new.html.erb ในไดเร็กทอรี app/views/posts:
<h1>Creating new post</h1>
<%= form_for @post, :action => 'create' do %>
  <%= text_field :title, :label => 'Title' %><br/>
  <%= text_area :body, :label => 'Body' %><br/>
  <%= submit 'New post' %>

<% end =%>
สังเกตว่าเราสั่ง form_for และเหล่าเมท็อด text_field และ text_area ก็จะสร้าง input tag ที่ขึ้นกับตัวแปรที่ระบุใน form_for เอง (ใน rails เราต้องระบุ block parameter)

สุดท้ายเราก็ไปเพิ่มเมท็อด create ลงไปในคอนโทรเลอร์ Posts เพื่อสร้าง post ใหม่ครับ

class Posts < Application
  # ...
  def create
    @post = Post.new params[:post]
    @post.save
    redirect url(:action => 'index')
  end
end
ลองทดลองดูนะครับว่าสามารถเพิ่ม post ได้หรือไม่ครับ

ระบบความเห็น

ระบบ blog คงไม่มีความหมายถ้าไม่สามารถใส่ comment ได้

เราจะสร้างโมเดล comment โดยเรียก

$ merb-gen model comment

แล้วแก้ไฟล์ app/models/comment.rb:
class Comment
  include DataMapper::Resource
  
  property :id, Serial
  property :body, String
end

จากนั้นเราต้องไประบุในโมเดล Post ถึงการเชื่อมโยงกับโมเดล Comment เราทำโดยระบุ has ลงไปในไฟล์ app/models/post.rb


class Post
  # ...
  has n, :comments
end
การสั่งดังกล่าวจะมีลักษณะคล้าย ๆ กับสั่ง has_many ใน Active Record ถ้าต้องการความสัมพันธ์แบบ one-to-one สามารถสั่ง has 1, :comment แทนได้ครับ

จากนั้นไปปรับ view โดยแก้ app/views/posts/index.html.erb ให้แสดง comment และมี comment form ครับ

<h1>My posts</h1>

<% for post in @posts %>
  <h2><%= post.title %></h2>
  <p><%= post.body %></p>
  <ul>
    <% post.comments.each do |comment| %>
      <li><%= comment.body %></li>
    <% end %>
    <li>
      <%= form_for Comment.new, 
                   :action => url(:controller => 'posts', 
                                  :action => 'create_comment', 
                                  :id => post.id) do %>
        <%= text_field :body %>
        <%= submit 'New comment' %>
      <% end =%>
    </li>
  </ul>

<% end %>

<%= link_to 'add new post', url(:controller => :posts, 
                                :action => :new) %>
สังเกตว่าตรงเมท็อด url ค่อนข้างยาว คิดว่าน่าจะทำได้สั้นกว่านี้ถ้าเข้าใจ merb router (ไว้ค่อยมา update ครับ) นอกจากนี้เนื่องจากเราแสดงผลโดยใช้ลิสต์ แต่ตัว css ของ merb คงไปทำอะไรสักอย่างกับ li ผมเลยต้องไปลบบรรทัด link ของ stylesheet ใน app/views/layout/application.html.erb ด้วย ไม่งั้นไม่เห็นลิสต์

จากนั้นเราไปสร้างเมท็อด create_comment ในคอนโทรเลอร์ Posts


class Posts < Application
  # ...
  def create_comment
    post = Post[params[:id]]
    comment = Comment.new params[:comment]
    post.comments << comment
    post.save
    redirect url(:action => 'index')
  end
end
สังเกตว่าเราสามารถสั่ง Post[params[:id]] เพื่อเรียก post ที่มี id ตามที่ระบุได้

หวังว่าเอนทรีนี้น่าจะเป็นประโยชน์สำหรับผู้กำลังจะหัด merb และ Datamapper บ้างนะครับ ถ้ามีตำแนะนำคำแนะนำอะไรรบกวนใส่ด้านล่างเลยนะครับ

หมายเหตุ: English version

พลังของความร่วมมือ

posted on 24 Dec 2008 15:00 by wonam in softdev

Ruby on Rails เป็นหนึ่งในนวัตกรรมที่สำคัญของการพัฒนาโปรแกรมประยุกต์บนเว็บ

Merb เป็นอีกหนึ่ง web framework ที่เน้นการออกแบบที่ถอดประกอบได้

ทั้งคู่เป็น web framework บน Ruby

แน่นอน Rails มาก่อน ถ้าไม่มี Rails คงไม่มี Merb

แต่ Merb ที่มาทีหลัง ก็ได้เรียนรู้รูปแบบและข้อดี-เสียหลายอย่างทำให้มีการออกแบบที่เอื้อต่อการปรับแต่งมากกว่า

เมื่อไม่นานมานี้ หลายคนคงได้ทราบว่ามีการถกเถียงกันอย่างกว้างขวางว่าเมื่อใดควรจะ Rails เมื่อใดควรจะ Merb การถกเถียงเหมือนจะแรงมากจนหลาย ๆ คนคิดว่าอาจจะเกิดการกระทบกระทั่งกันของคนใน community เพราะว่าสองกลุ่มนี้หลาย ๆ คนก็มีรากมาจากทาง Rails เหมือน ๆ กัน

วันนี้มีข่าวดี!

(ได้ฟังมาจาก @punneng)

จะไม่มีปัญหาว่าจะเลือกอะไรอีกแล้ว เพราะว่านักพัฒนาทั้งสองกลุ่มตัดสินใจที่จะทำงานร่วมกัน

Merb จะเข้ามารวมกับ Rails

Merb 2 คือ Rails 3

สำหรับผมแล้ว นี่เป็นข่าวดีที่สุดข่าวหนึ่งในรอบปีที่กำลังจะจบลงเลยทีเดียว