แนวคิดเกี่ยวกับ Dependency Injection เป็นแนวคิดที่สำคัญมากในการโปรแกรมสำหรับภาษาเช่น Java ด้วยสาเหตุหลาย ๆ ประการ
สาเหตุหนึ่งก็คือมันทำให้เราสามารถทำ unit test กับโปรแกรมที่มีการขึ้นต่อกันได้ ยกตัวอย่างเมท็อดด้านล่าง
public class RegistrationController {
// ...
void sendConfirmationEmail(User newUser) {
MailSender sender = new MailSender();
String msg = buildEmailMessage(newUser);
sender.send(msg,myemail,newUser.getEmail());
}
}
แทบเราจะไม่สามารถ test ได้เลยว่าเมท็อดดังกล่าวเรียก MailSender ได้ถูกต้องหรือเปล่า ที่ผมนึกออกคงจะต้องเข้าไปจัดการแก้โปรแกรมหลายจุดอยู่
ปัญหาก็มาจากการที่เมท็อดนี้สร้าง MailSender ขึ้นมาเอง ทำให้เราไม่สามารถเข้าไปแก้ไขได้ วิธีการที่นิยมใช้ในการจัดการเรื่องเหล่านี้ก็คือการแยกการขึ้นต่อกันของคลาส MailSender ออกมา โดยทำเป็นเมท็อดให้กำหนดค่าเข้าไป อาจจะที่ constructor หรือเขียนเป็นเมท็อดแยก หรือไม่ก็ใช้ DI framework ต่างๆ
เช่นแก้โปรแกรมเป็นแบบนี้
public class RegistrationController {
// ...
private MailSenderInterface mailSender;
void setMailSender(MailSenderInterface sender) {
mailSender = sender;
}
void sendConfirmationEmail(User newUser) {
String msg = buildEmailMessage(newUser);
mailSender.send(msg,myemail,newUser.getEmail());
}
}
ในปัจจุบัน Java มี dependency injection framework หลายตัว (เท่าที่ผมทราบ) ซึ่งทำให้การ “ร้อย” (ของยืมพี่ป๊อกหน่อย) component ต่าง ๆ เข้าด้วยกันเป็นไปได้สะดวกมาก
แนวคิดดังกล่าวได้รับการตอบรับจากทางฝั่งนักพัฒนา Ruby เช่นเดียวกัน เช่น Jim Weirich ได้เขียนบล็อกเกี่ยวกับเรื่องนี้เอาไว้เมื่อปี 2004 ใน Ruby ก็มี framework ทำ DI อยู่หลายตัวเช่น Needle เขียนโดย Jamis Buck (คนทำ Capistrano) Jamis Buck ถึงขนาดเขียน di framework มาสองตัวเลยทีเดียว (อีกตัวคือ Copland)
อย่างไรก็ตาม ก็เป็นที่น่าสงสัยว่าทำไม DI framework ไม่เป็นที่นิยมใน Ruby
หนึ่งปีถัดมา Jim Weirich ได้ไปพูดที่ OSCON ในหัวข้อว่า “Dependency Injection: Vitally Important or Totally Irrelevant?” โดยสรุปว่าเนื่องจาก Ruby ไม่เหมือน Java ในปัจจุบันยังไม่เห็นความจำเป็นของ DI framework
Jamis Buck เองก็ออกมาเขียนถึงเรื่องดังกล่าวเช่นกัน โดยเขาแก้โปรแกรมในไลบรารี Net::SSH ใหม่ โดยเอา DI (ที่เขาเขียนเอง) ออก แล้วพบว่าโปรแกรมเล็กลงและอ่านง่ายขึ้น
ทำไม DI ดูเหมือนจะยังไม่จำเป็นใน Ruby?
พิจารณาจากตัวอย่างข้างต้น ถ้าเอาเมท็อด sendConfirmationEmail มาเขียนเป็นโปรแกรม Ruby จะได้ประมาณด้านล่างครับ
class RegistrationController
# ..
def send_confirmation_email(new_user)
sender = MailSender.new
msg = build_email_message(new_user)
sender.send(msg, self.myemail, new_user.get_email)
end
end
แล้วจะ test อย่างไร?
สิ่งที่เรามักจะลืมไปก็คือภาษาแต่ละภาษามีลักษณะที่แตกต่างกัน บางอย่างที่ไม่สามารถทำได้เลยในบางภาษา อาจเป็นสิ่งธรรมดามากในบางภาษา
ใน Ruby มีความสามารถ (หรือความบกพร่อง?) อย่างหนึ่งคือ Open Class
นั่นคือเราสามารถแกะคลาสมาแก้ได้ตลอดเวลา (รวมถึงตอน run-time) นอกจากนี้เรายังแก้ไขการทำงานของเมท็อดของแต่ละวัตถุได้โดยง่าย (ในระหว่างที่โปรแกรมทำงานอยู่เช่นกัน)
เมท็อดด้านบนถ้าจะเขียน test case ใน rspec ก็เป็นประมาณนี้ครับ
describe RegistrationController do
#..
it "should send mail to user's address from admin's mail" do
my_email = 'jittat@internet.com'
user_email = 'user@space.com'
user = mock_model(User, :email => user_email)
sender = mock("mock sender")
sender.should_receive(:send).
with(anything, my_email, user_email)
MailSender.should_receive(:new).and_return(mock_sender)
controller = RegistrationController.new :adm_mail => my_email
controller.send_confirmation_email(user)
end
end
สังเกตว่าเนื่องจาก class และ object ใน ruby แก้ได้ตลอดเวลา mock framework จึงสามารถเข้าไปปรับแก้อะไรต่าง ๆ ได้มากมาย โดยไม่ต้องแยก dependency ออกมา
ไม่รู้ว่าผลที่ได้จะดีหรือไม่ดี? แต่ก็ทำให้ความจำเป็นของการใช้ DI framework ใน Ruby ลดลงไป
อ่านเพิ่มเติมได้ใน slide ของ Jim Weirich นะครับ อธิบายเห็นภาพมาก (มีอีกอันที่น่าสนใจเหมือนกันคือ 10 Things Every Java Programmer Should Know About Ruby ลองไปกดเล่นได้ครับ)