ประกาศ Rails 4.2 มีข่าวน่าสนใจเกี่ยว Rails 5: น่าจะ require Ruby 2.2 จะเป็น Rails version แรกที่ใช้ประโยชน์จาก Ruby 2 ทั้งหมด
โพสต์ mention garbage collected symbols และ keyword arguments แตสิ่งที่น่าสนใจที่สุดสำหรับผมคือ Module#prepend
The Good (และ Bad) ของ alias_method_chain
ตอนเรียน Rails alias_method_chain ทำให้หลุดลม ทำให้เห็น Ruby ยืดหยุ่นแค่ไหน
เพียงบรรทัดเดียว เปลี่ยนการทำงานของ method เปลี่ยนไปเลย ไม่ต้อง hack libraries เพิ่มโค้ด เพิ่ม on the fly ได้ alias_method_chain> นำไปสู่ patches แรกของผมไป gems → pull requests แรก → open source contributions แรก
แตเหมือน monkey patching alias_method_chain ใช้มากเกินไป ปัญหาเริ่มชัดเจน:
- Method names ที่ generate สับสน ทำให้ errors ยากหาและ debug เช่น:
class Person def greeting "Hello" end end module GreetingWithExcitement def self.included(base) base.class_eval do alias_method_chain :greeting, :excitement end end def greeting_with_excitement "#{greeting_without_excitement}!!!!!" end end Person.send(:include, GreetingWithExcitement) ถ้า error ใน Person#greeting backtrace บอกว่า error ใน Person#greeting_without_excitement แตเมธอดนั้น define ที่ไหน? ไม่เห็นที่ไหนเลย รู้ยังไงว่า greeting method ไหนมี bug? และชื่อเมธอดสับสนขึ้นเรื่อยๆ ตอน chain มากขึ้น
ถ้าเรียก alias_method_chain สองครั้ง params เดียวกัน class เดียวกัน อาจ cause stack overflow (เห็นไหมว่าทำไม?) ไม่ปกติเกิด ถ้า requires consistent paths แตขุ่นถ้า paste code ใน Rails console บ่อย
และสิ่งอื่นๆ ที่ Yehuda Katz blog post อธิบาย โพสต์นี้ convince หลาย Rails devs ทิ้ง alias_method_chain ใช้ module inheritance แทน
ทำไมยังใช้อยู่?
แทนที่ alias_method_chain ส่วนใหญ่ได้ด้วย override methods ใน module และ include modules เข้า child classes แตทำงานได้แค่ตอนอยาก override superclass ไม่ใช่ class เอง นั่นคือ:
class ParentClass def log puts "In parent" end end class ChildClass < ParentClass def log puts "In child" super end def log_with_extra_message puts "In child, with extra message" log_without_extra_message end alias_method_chain :log, :extra_message end รัน ChildClass.new.log จะเห็น:
In child, with extra message In child In parent ลอง modules แทน alias_method_chain ได้ output:
In child In child, with extra message In parent แต ไม่สามารถ match original output ได้โดยไม่เปลี่ยน log method ใน ChildClass Ruby inheritance ไม่ทำงานแบบนั้น ทำไม่ได้ ตอนก่อน
อะไรเปลี่ยนใน Ruby 2.0?
จน Ruby 2.0 ไม่มีทางเพิ่มโค้ด ใต้ class แค่ เหนือ มัน แตกับ prepend สามารถ override method ใน class ด้วย method จาก module และยังเข้าถึง class implementation ด้วย super ดังนั้น ตัวอย่างล่าสุด ได้ original output ด้วย:
class ParentClass def log puts "In parent" end end module ExtraMessageLogging def log puts "In child, with extra message" super end end class ChildClass < ParentClass prepend ExtraMessageLogging def log puts "In child" super end end In child, with extra message In child In parent Perfect
ถ้า prepend ยังยากจินตนาการ คิดว่ามันทำแบบนี้:
class NewChildClass < ChildClass include ExtraMessageLogging end ChildClass = NewChildClass แตไม่ mess class names และ affect objects ที่มีอยู่แล้ว
(ใช่ สามารถ reassign class names ใน Ruby ได้ ไม่ ควรทำบ่อย)
What Does This Mean for Rails?
Excuse สุดท้ายสำหรับ alias_method_chain หายใน Ruby 2.0 แทนที่ตัวอย่าง alias_method_chain ที่เหลือใน Rails:
require 'active_support/core_ext/module/aliasing' class Range #:nodoc: def each_with_time_with_zone(&block) ensure_iteration_allowed each_without_time_with_zone(&block) end alias_method_chain :each, :time_with_zone def step_with_time_with_zone(n = 1, &block) ensure_iteration_allowed step_without_time_with_zone(n, &block) end alias_method_chain :step, :time_with_zone private def ensure_iteration_allowed if first.is_a?(Time) raise TypeError, "can't iterate from #{first.class}" end end end ด้วย module แทน:
require 'active_support/core_ext/module/aliasing' module RangeWithTimeWithZoneSupport #:nodoc: def each(&block) ensure_iteration_allowed super(&block) end def step(n = 1, &block) ensure_iteration_allowed super(n, &block) end private def ensure_iteration_allowed if first.is_a?(Time) raise TypeError, "can't iterate from #{first.class}" end end end Range.send(:prepend, RangeSupportingTimeWithZone) สะอาดกว่า Range#each ไม่ถูก rename และ ensure_iteration_allowed ไม่ monkey-patched เข้าไป
Use Inheritance, Not Patches
Ruby ให้ flexibility เยอะ นั่นเป็นเหตุผลผมชอบ แตยังมี object model powerful ตอนอยาก inject โค้ด ลองพึ่ง modules และ inheritance ก่อน hack โค้ดจะเข้าใจและ debug ง่ายกว่า และหลีกเลี่ยง side effects ยากจับของ alias_method_chain
alias_method_chain เป็นเมธอดเก๋มที่สุดที่รู้จักใน Rails แตวันมันนับถอยหลัง เรา outgrow มันแล้ว และผมไมจะคิดถึงมันตอนมันหาย