[Code Refactoring#1] หยุดใช้ Model Callbacks ซะ! ถ้าไม่อยากปวดตับ
ผมเชื่อว่าเพื่อนๆที่ใช้เฟรมเวิร์ก MVC ฝั่ง backend ทั้งหลายเช่น Ruby on Rails หรือ Laravel ล้วนผ่านการใช้งาน Model Callbacks กันแล้วทั้งสิ้น มันก็คือเมธอดตระกูล before_save (ก่อนบันทึกข้อมูลลงฐานข้อมูลให้ทำอะไร) หรือ after_save (หลังบันทึกข้อมูลลงฐานข้อมูลแล้วจะให้ทำอะไรต่อ) แต่ละเฟรมเวิร์กอาจเรียก callbacks เหล่านี้ต่างกัน เช่น Laravel เรียก Model Observers ส่วนพ่อทุกสถาบันอย่าง Ruby on Rails เรียก Active Record Callbacks ไม่ว่าจะใช้ชื่ออะไรต่างสื่อถึงสิ่งเดียวกัน
ยิ่งใช้ Callbacks มากเท่าไหร่ โปรแกรมเมอร์อย่างเรายิ่งฟินเท่านั้น นี่หละสวรรค์ของการใช้เฟรมเวิร์ก อยากให้ส่งอีเมล์เพื่อยืนยันตนไปหาผู้ใช้งาน หลังสร้างบัญชีผู้ใช้ใช่ไหม? ง่ายมากยัดโค๊ดส่งอีเมล์ลงไปใน after_create
ของโมเดล User ซะเลยซิ ที่เหลือก็ไปนั่งจิบกาแฟ เล่นไพ่ป๊อกกับเพื่อนโต๊ะข้างๆต่อ
ต้องบอกก่อนว่าไม่ได้ตั้งใจจะดับฝันเพื่อนๆนะครับ แต่เชื่อไหม Callbacks ในมือคุณ อาจเป็นซาตานในคราบนักบุญ ที่จะนำพาหายนะมาให้ผู้ใช้ซวยแล้วซวยอีก ยิ่งกว่าโดนตำรวจจับเพราะเล่นไพ่ป๊อกกับเพื่อนข้างโต๊ะซะอีก
หมายเหตุ ตัวอย่างโปรแกรมในบทความนี้เขียนด้วยภาษา Ruby บน Ruby on Rails ผู้อ่านไม่จำเป็นต้องสนใจว่าไวยากรณ์ทางภาษาของ Ruby เป็นอย่างไร อ่านเพื่อเอาคอนเซปต์ไปประยุกต์กับเฟรมเวิร์กที่ใช้อยู่เป็นพอครับ :)
ส่งอีเมล์ยืนยันตนเมื่อสร้างผู้ใช้งานใหม่ในระบบ
โจทย์ข้อแรกของระบบเราคือต้องอนุญาตให้สร้างผู้ใช้งานได้ สร้างยูสเซอร์เมื่อไหร่ช่วยกรุณาส่งอีเมล์ยืนยันให้ด้วยนะ
1class User < ActiveRecord::Base2 # after_create เป็นการบอกว่าหลังสร้าง User ในฐานข้อมูลเรียบร้อยให้ทำอะไร3 # ในที่นี้คือให้เรียกเมธอด send_confirmation_email หลังสร้าง User เสร็จแล้ว4 after_create :send_confirmation_email56 private78 def send_confirmation_email9 # เมื่อสร้าง User ในฐานข้อมูลแล้ว10 # ให้ส่งอีเมล์ยืนยันไปด้วย11 Mailer.confirmation_email(self).deliver12 end13end
เย้ เรียบร้อย จิบกาแฟแล้วหลีสาวข้างโต๊ะต่อ~ สองนาทีต่อมาเจ้านายเดินมาบอกว่า ถ้า user คนนั้นเป็น admin ลื้อจะส่งอีเมล์หาบรรพบุรุษหรอครัช
เราก็เลยต้องมาแก้โค๊ดของเราใหม่ แบบนี้
1class User < ActiveRecord::Base2 after_create :send_confirmation_email34 private56 def send_confirmation_email7 # ส่งอีเมล์ไปเสียเถอะพี่น้อง "ยกเว้น (unless)" คนนั้นเป็น admin8 return if user.admin?9 Mailer.confirmation_email(self).deliver10 end11end
เย้ เรียบร้อย จิบกาแฟแล้วชวนสาวข้างโต๊ะเล่นไพ่ป๊อก~
เนื่องจากระบบที่คุณทำนั้นเป็นการพัฒนาใหม่หมดจากระบบเดิมที่มีอยู่ คุณจึงมีรายชื่อของผู้ใช้งานในระบบเดิมอยู่ในกำมือ อีกสองนาทีต่อมา เจ้านายเดินชิวมาหาคุณ ไอ้ user จากระบบเก่า ลื้อเพิ่มเข้ามาในระบบใหม่ แล้วลื้อจะส่งอีเมล์ยืนยันทำไมวะครัช ในเมื่อพวกเขาอยู่ในระบบมาตั้งแต่ต้นแล้ว
นั่นละฮะ ปัญหาเกิดจากเราผูก send_confirmation_email ของเราไว้กับโมเดล User ดังนั้นแล้วเมื่อไหร่ก็แล้วแต่ที่เราสร้าง User ใหม่ User เหล่านั้นทุกตัวไม่เว้นแม้แต่ admin หรือ user เก่า ก็จะโดนยัดเยียดให้ทำ send_confirmation_email ทั้งหมด เริ่มเห็นปัญหาแล้วใช่ไหมครับ!
ปัญหาของการใช้ Model Callbacks แบบผิดวิธี
Model Callbacks นั้นมีประโยชน์ แต่ตอนนี้ขอชี้หน้าด่าเพื่อความสะใจส่วนตัวก่อนครับ
Callbacks นั้นเป็นสิ่งที่เรียกว่า persistence logic หรือการทำงานที่เดี่ยวข้องกับฐานข้อมูล ส่วน send_confirmation_email ของเรานั้นเป็น business logic หรือการทำงานหลักที่เป็นความต้องการของเรา การใช้งาน callbacks จึงเป็นการผูก logic สองส่วนนี้เข้าด้วยกัน
ทุกครั้งที่เราสร้าง user เราหลีกเลี่ยงไม่ได้ที่จะต้องส่งอีเมล์ นั่นเป็นเพราะโค๊ดเราบอกว่าให้ทำสิ่งนี้ทุกครั้งหลังสร้าง user ถ้าเราต้องการหยุดพฤติกรรมที่ผูกกันแน่นเช่นนี้เราก็แค่เอา callbacks ออกไปซะ!
อีกปัญหาของการใช้ Callbacks ที่จะไม่พูดถึงไม่ได้คือ การใช้ callbacks ทำให้เราทำ unit test ยากขึ้น ลองจินตนาการดูนะครับ หากเรามีการใช้ callbacks เราจะทดสอบโปรแกรมของเราโดยปราศจากการเชื่อมต่อฐานข้อมูลไม่ได้เลย เว้นแต่จะ Mock/Stub เอาไว้ แถมยังมี logic ที่ซับซ้อนให้ทดสอบเพิ่มขึ้นอีกด้วย
หยุดความสัมพันธ์ที่ลึกซึ้งด้วยการยุติการใช้ Model Callbacks
เพื่อทำให้ตัวอย่างนี้สมบูรณ์แบบ เราจึงเลิกใช้ callbacks ดังนี้
1# Model2class User < ActiveRecord::Base3 # ลาก่อยย after_create4 def send_confirmation_email5 return if user.admin?6 Mailer.confirmation_email(self).deliver7 end8end910# Controller11class UsersController < BaseController12 def create13 user = User.create(user_params)14 user.send_confirmation_email15 # จะทำอะไรต่อก็ทำโล๊ด16 end17end
จากตัวอย่างข้างบนเราเปลี่ยน send_confirmation_email จาก private ให้เป็น public เพื่อให้ภายนอกเข้าถึงได้ นอกจากนั้นเรายังเลิกผูกเมธอดนี้กับ callbacks แล้ว มาดูกันซิเราได้ประโยชน์อะไรกันบ้างจากการทำแบบนี้
- เราทราบดีว่า model ของเรามักทำ unit testing เราจึงลดความซับซ้อนของโค๊ดใน model เพื่อให้ง่ายต่อการทดสอบ
- แต่ controller นั้นต่างออกไป เรามักทำ integration testing เราจึงมาเพิ่มความซับซ้อนให้ส่วนนี้แทนได้อย่างไม่ลังเล
โค๊ดแบบไหนควรงดใช้ Callbacks
จากตัวอย่างเพื่อนๆคงเห็นนะครับว่า User และ Mailer ที่ทำหน้าที่ส่งอีเมล์ไม่ใช่สิ่งเดียวกัน มันอยู่คนละ domain problem กันเลย user ไว้จัดการเรื่องของผู้ใช้งานระบบ ส่วน Mailer นั้นใช้จัดการงานส่งอีเมล์ เมื่อทั้งสองสิ่งไม่ได้อยู่บนพื้นฐานของปัญหาเดียวกัน เราจึงไม่ควรผูกมันเข้าด้วยกันผ่าน callbacks
ผมจึงสรุปให้เพื่อนๆหลีกเลี่ยงการผูกสองสิ่งที่ไม่มีปัญหาร่วมกัน เข้าด้วยกันผ่าน callbacks ครับ
แล้ว callbacks ควรใช้กับอะไรหละ?
มาถึงตรงนี้เพื่อนๆบางคนคงรีบเปิดคอมไปเอา callbacks ออกใหญ่เลยใช่ไหมหละ แต่ช้าก่อนครับอ่านตรงนี้ก่อนครับสำคัญมาก มีอีกหลายกรณีที่ callbacks ยังสำคัญอยู่ นั่นคือการใช้ callbacks เพื่ออัพเดทตัวเองในบางกรณี ขอให้เพื่อนๆพิจารณาตัวอย่างต่อไปนี้ดูครับ
1class Money < ActiveRecord::Base2 # เมธอดสำหรับเปลี่ยนค่าของเงินเป็นจำนวนบวก3 def to_positive!4 # เปลี่ยนให้เป็นค่าบวกผ่าน absolute (abs)5 self.money = money.abs6 end7end89# เรามีเงินอยู่ -99 บาท ซึ่งเป็นไปไม่ได้ที่เงินจะติดลบ10money = Money.new(-99)11# จึงเรียก to_positive เพื่อการันตีว่าเงินต้องเป็นบวก12money = money.to_positive!13# จากนั้นถึง save ได้อย่างสบายใจ14money.save
ตัวอย่างข้างต้นเพื่อนๆเริ่มเห็นความไม่ฉลาดของคลาส Money แล้วใช่ไหมครับ แทนที่ตัวคลาสจะจัดการเองอัตโนมัติว่า ถ้าเราใส่เลขลบเข้าไปให้แปลงเป็นเลขบวกอัตโนมัติ แต่เรากลับต้องมาเรียกเมธอด to_positive! ไม่ฉลาดเอาซะเลย แถมยังเป็นการออกแบบคลาสที่ไม่ดีด้วยเพราะทำให้ภายนอกรับรู้กลไกภายในมากเกินไป
เพื่อความสบายใจของทุกคน เรามาเปลี่ยนโค๊ดของเราด้วยการใช้ callbacks กันเถอะ!
1class Money < ActiveRecord::Base2 # ก่อน save ช่วยแปลงเป็นบวกทีเถอะนะ ข้าขอร้องง3 before_save :to_positive!45 def to_positive!6 self.money = money.abs7 end8end910money = Money.new(-99)11money.save
ไม่ยากอย่างที่คิดใช่ไหมครับเรื่องการจัดการกับ callbacks เพื่อนๆคนไหนมีข้อคิดเห็นอย่างไรพิมพ์ทิ้งไว้ล่างบทความเลยนะฮะ
สารบัญ
- ส่งอีเมล์ยืนยันตนเมื่อสร้างผู้ใช้งานใหม่ในระบบ
- ปัญหาของการใช้ Model Callbacks แบบผิดวิธี
- หยุดความสัมพันธ์ที่ลึกซึ้งด้วยการยุติการใช้ Model Callbacks
- โค๊ดแบบไหนควรงดใช้ Callbacks
- แล้ว callbacks ควรใช้กับอะไรหละ?