Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Ancestor Chain ของ Ruby ด้วย method_missing

จับกระเป๋าเดินทางให้แน่นหน่ะครับ เพราะวันนี้เราจะเดินทางขึ้นไปจนถึงปลาย ancestor chain เราจะติดตามการเรียกเมธอดและดูว่ามันขึ้นไปตาม chain อย่างไร รวมถึงดูว่าจะเกิดอะไรขึ้นถ้าเมธอดหายไป และเพราะเราชอบเล่นกับไฟ เราจึงไม่หยุดแค่นั้น แต่อยู่ต่อไปจนถึง การเล่นกับไฟ การ override BasicObject#method_missing ถ้าคุณใส่ใจดู เราอาจจะใช้มันในตัวอย่างปฏิบัติจริง ไม่มีการรับประกันนะครับ เริ่มกันเลย!

Ancestor Chain

มาเริ่มกับกฎพื้นฐานของ ancestor chains ใน Ruby:

  • Ruby รองรับ single inheritance เท่านั้น
  • แต่ยังอนุญาตให้ออบเจ็กต์ include ชุดของ modules ได้

ใน Ruby ancestor chain ประกอบด้วยการ traverse ทุกคลาสและโมดูลที่สืบทอดสำหรับคลาสที่กำหนด

มา看ตัวอย่างเพื่อดูว่า ancestor chain ถูกจัดการใน Ruby อย่างไร

module Auth
end

module Session
end

module Iterable
end

class Collection
  prepend Iterable
end

class Users < Collection
  prepend Session
  include Auth
end

p Users.ancestors

ผลลัพธ์:

[ Session, Users, Auth, # Users
  Iterable, Collection, # Collection
  Object, Kernel, BasicObject # Ruby Object Model ]

ก่อนอื่น เราเรียก class method ancestors เพื่อเข้าถึง ancestor chain ของคลาสที่กำหนด

เราสามารถเห็นได้ว่า การเรียก Users.ancestors ส่งคืนอาร์เรย์ของคลาสและโมดูลที่ประกอบด้วยตามลำดับ:

  • โมดูลที่ prepend ของ Users
  • คลาส Users
  • โมดูลที่ include ของคลาส Users
  • โมดูลที่ prepend ของคลาส Collection — โดยตรงจาก parent ของ Users
  • คลาส Collection
  • โมดูลที่ include ของคลาส Collection — ไม่มี
  • คลาส Object — การสืบทอดเริ่มต้นของทุกคลาส
  • โมดูล Kernel — included ใน Object และเก็บ core methods
  • คลาส BasicObject — root class ใน Ruby

ดังนั้น ลำดับการปรากฏสำหรับคลาสหรือโมดูลที่ traverse ใดๆ จะเป็นดังนี้เสมอ:

  • โมดูลที่ prepend
  • คลาสหรือโมดูล
  • โมดูลที่ include ของมัน

Ancestor chain ถูก traverse หลักโดย Ruby เมื่อมีการ invoke เมธอดบนออบเจ็กต์หรือคลาส

Method Lookup Path

เมื่อส่งข้อความ Ruby จะ traverse ancestor chain ของ message receiver และตรวจสอบว่ามีตัวไหนตอบสนองต่อข้อความที่กำหนดหรือไม่

ถ้าคลาสหรือโมดูลใดใน ancestor chain ตอบสนองต่อข้อความ เมธอดที่เกี่ยวข้องกับข้อความนั้นจะถูก execute และการ traverse ancestor chain จะหยุด

class Collection < Array
end

Collection.ancestors # => [Collection, Array, Enumerable, Object, Kernel, BasicObject]

collection = Collection.new([:a, :b, :c])
collection.each_with_index # => :a

ที่นี่ ข้อความ collection.each_with_index ถูก receive โดยโมดูล Enumerable แล้วเมธอด Enumerable#each_with_index จะถูกเรียกสำหรับข้อความนี้

ที่นี่ เมื่อ collection.each_with_index ถูกเรียก Ruby จะตรวจสอบว่า:

  • Collection ตอบสนองต่อข้อความ each_with_index => NO
  • Array ตอบสนองต่อข้อความ each_with_index => NO
  • Enumerable ตอบสนองต่อข้อความ each_with_index => YES

ดังนั้น จากที่นี่ Ruby หยุดการ traverse ancestor chain และเรียกเมธอดที่เกี่ยวข้องกับข้อความ ในกรณีนี้คือเมธอด Enumerable#each_with_index

ใน Ruby กลไกนี้เรียกว่า Method Lookup Path

ตอนนี้จะเกิดอะไรขึ้นถ้าไม่มีคลาสและโมดูลใดๆ ที่ประกอบเป็น ancestor chain ของ receiver ตอบสนองต่อข้อความ?

BasicObject#method_missing

พอเล่นดีๆ แล้ว! มาทำลายสิ่งต่างๆ แบบนักพัฒนา: โดยการโยน exception เราจะ implement คลาส Collection และเรียกเมธอดที่ไม่รู้จักบน instance หนึ่งของมัน

class Collection
end

c = Collection.new
c.search('item1') # => NoMethodError: undefined method `search` for #<Collection:0x123456890>

ที่นี่ คลาส Collection ไม่ได้ implement เมธอด search ดังนั้น NoMethodError จึงถูก raise แต่ error นี้มาจากไหน?

Error ถูก raise ในเมธอด BasicObject#method_missing เมธอดนี้ถูกเรียกเมื่อ Method Lookup Path จบลงโดยไม่พบเมธอดใดๆ ที่ตรงกับข้อความที่กำหนด

โอเค... แต่มETHOD นี้แค่ raise NoMethodError ดังนั้นการ override เมธอดนี้ในบริบทของคลาส Collection ของเราจะดีมาก

การ Override เมธอด BasicObject#method_missing

เดาไง? Override method_missing ได้อย่างสมบูรณ์ เพราะเมธอดนี้ก็ยังอยู่ภายใต้กลไกของ Method Lookup Path ความแตกต่างกับเมธอดทั่วไปคือเราแน่ใจว่าจะพบเมธอดนี้อย่างน้อยหนึ่งครั้งโดย Method Lookup Path

แน่นอน เพราะคลาส BasicObject — ซึ่งเป็น root class ของทุกคลาสใน Ruby — กำหนดเวอร์ชัน minimal ของเมธอดนี้ Ruby Magic แบบคลาสสิคใช่ไหม?

มาลอง override เมธอดนี้ในคลาส Collection ของเรา:

class Collection
  def initialize
    @collection = {}
  end

  def method_missing(method_id, *args)
    if method_id[-1] == '='
      key = method_id[0..-2]
      @collection[key.to_sym] = args.first
    else
      @collection[method_id]
    end
  end
end

collection = Collection.new
collection.obj1 = 'value1'
collection.obj2 = 'value2'
collection.obj1 # => 'value1'
collection.obj2 # => 'value2'

ที่นี่ Collection#method_missing ทำหน้าที่เป็น delegator ไปยัง instance variable @collection จริงๆ แล้วนี่คือวิธีที่ Ruby จัดการ object delegation โดยประมาณ — c.f: library delegate

ถ้าเมธอดที่หายไปเป็น setter method (collection.obj1 = 'value1') ชื่อเมธอด (:obj1) จะถูกใช้เป็น key และ argument ('value1') เป็น value ของ hash entry @collection (@collection[:obj1] = 'value1')

HTML Tag Generator

ตอนนี้เรารู้แล้วว่าเมธอด method_missing ทำงานเบื้องหลังอย่างไร มา implement use case ที่ reproducible กัน

ที่นี่เป้าหมายคือการ define DSL ดังนี้:

HTML.p 'hello world' # => <p>hello world</p>
HTML.div 'hello world' # => <div>hello world</div>
HTML.h1 'hello world' # => <h1>hello world</h1>
HTML.h2 'hello world' # => <h2>hello world</h2>
HTML.span 'hello world' # => <span>hello world</span>
HTML.p "hello #{HTML.b 'world'}" # => <p>hello <b>world</b></p>

เพื่อทำเช่นนั้น เราจะ implement เมธอด HTML.method_missing เพื่อไม่ต้อง define เมธอดสำหรับแต่ละ HTML tag

ก่อนอื่น เรา define โมดูล HTML จากนั้น define class method method_missing ในโมดูลนี้:

module HTML
  def HTML.method_missing(method_id, *args, &block)
    "<#{method_id}>#{args.first}</#{method_id}>"
  end
end

เมธอดของเราจะแค่สร้าง HTML tag โดยใช้ method_id ที่หายไป — :div สำหรับการเรียก HTML.div ตัวอย่างเช่น

โปรดทราบว่า class methods ก็ยังอยู่ภายใต้ Method Lookup Path เช่นกัน

เราสามารถปรับปรุง HTML tag generator ของเราได้โดย:

  • ใช้ block argument เพื่อจัดการ nested tags
  • จัดการ single tags — <br/> ตัวอย่างเช่น

แต่โปรดสังเกตว่า ด้วยเพียงไม่กี่บรรทัดของโค้ด เราสามารถ generate HTML tags จำนวนมากได้

สรุป:

method_missing เป็น entry point ที่ดีสำหรับการสร้าง DSLs ที่คำสั่งส่วนใหญ่จะ share ชุดของ patterns ที่ระบุไว้

Conclusion

เราเดินทางขึ้นไปจนถึงปลาย ancestor chain ใน Ruby และดำน้ำเข้าไปใน BasicObject#method_missing BasicObject#method_missing เป็นส่วนหนึ่งของ Ruby Hook Methods ใช้เพื่อ interact กับออบเจ็กต์ในช่วงเวลาที่แน่นอนใน lifecycle ของพวกมัน เหมือน hook methods อื่นๆ Ruby Hook Methods เมธอด hook นี้ต้องใช้อย่างระมัดระวัง และด้วย "อย่างระมัดระวัง" เราหมายถึง ไม่ควรปรับเปลี่ยนพฤติกรรมของ Ruby Object Model — เว้นแต่คุณจะเล่นกับมันหรือเขียนบล็อกเกี่ยวกับมัน ;-)

Voilà!