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

Rails 5, Module#prepend และจุดสิ้นสุดของ `Alias_method_chain`

การประกาศ Rails 4.2 มีข่าวที่น่าสนใจเกี่ยวกับ Rails 5:ที่อาจต้องใช้ Ruby 2.2 ซึ่งจะทำให้เป็น Rails เวอร์ชันแรกที่สามารถใช้ประโยชน์จากสิ่งดี ๆ จาก Ruby 2 ได้ทั้งหมด

โพสต์กล่าวถึงสัญลักษณ์ที่รวบรวมขยะและอาร์กิวเมนต์คำหลัก แต่สำหรับฉัน คุณลักษณะ Ruby 2 ที่น่าสนใจที่สุดอย่างหนึ่งคือ Module#prepend

ข้อดี (และข้อเสีย) ของ alias_method_chain

เมื่อฉันเรียนรู้ Rails เป็นครั้งแรก alias_method_chain พัดใจของฉันโดยสิ้นเชิง มันแสดงให้เห็นอย่างชัดเจนว่า Ruby สามารถยืดหยุ่นได้เพียงใด

ด้วยโค้ดเพียงบรรทัดเดียว คุณสามารถเปลี่ยนวิธีการทำงานโดยสิ้นเชิง คุณไม่จำเป็นต้องแฮ็คห้องสมุดเพื่อเพิ่มโค้ดที่คุณต้องการอีกต่อไป คุณสามารถเพิ่มได้ทันที alias_method_chain นำไปสู่แพทช์แรกของฉันสู่อัญมณี ซึ่งนำไปสู่คำขอดึงครั้งแรกของฉัน ซึ่งนำไปสู่การสนับสนุนโอเพนซอร์สครั้งแรกของฉัน

แต่เหมือนกับการปะลิง alias_method_chain ใช้มากเกินไป และปัญหาเริ่มชัดเจน:

  • ชื่อเมธอดที่สร้างขึ้นนั้นสร้างความสับสน ซึ่งทำให้ค้นหาข้อผิดพลาดและแก้ไขข้อบกพร่องได้ยาก ตัวอย่างเช่น:
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)

หากคุณมีข้อผิดพลาดใน Person#greeting การติดตามย้อนกลับจะบอกคุณว่ามีข้อผิดพลาดเกิดขึ้นจริงใน Person#greeting_without_excitement . แต่วิธีการนั้นถูกกำหนดไว้ที่ไหน? ฉันไม่เห็นมันทุกที่ คุณรู้ได้อย่างไรว่าgreeting วิธีการคือวิธีที่มีจุดบกพร่องในนั้นหรือไม่ และชื่อเมธอดยิ่งสับสนมากขึ้นเรื่อยๆ

  • ถ้าคุณเรียก alias_method_chain สองครั้งด้วยพารามิเตอร์เดียวกันในคลาสเดียวกัน อาจทำให้สแต็กโอเวอร์โฟลว์ได้ (คุณเห็นไหมว่าทำไม) สิ่งนี้จะไม่เกิดขึ้น ตราบใดที่require .ของคุณ งบมีความสอดคล้องกันเกี่ยวกับเส้นทางที่พวกเขาใช้ แต่มันน่ารำคาญมากถ้าคุณวางโค้ดลงในคอนโซล Rails บ่อยๆ

  • และเรื่องอื่นๆ ที่โพสต์ในบล็อกของ Yehuda Katz อธิบายไว้ โพสต์นี้โน้มน้าวให้นักพัฒนา Rails จำนวนมากเริ่มละทิ้ง alias_method_chain เพื่อประโยชน์ในการสืบทอดโมดูล

แล้วทำไมยังใช้อยู่?

คุณสามารถแทนที่ alias_method_chain . ส่วนใหญ่ได้ โดยการแทนที่เมธอดเหล่านั้นในโมดูล และรวมโมดูลเหล่านั้นไว้ในคลาสย่อยของคุณ แต่จะได้ผลก็ต่อเมื่อคุณต้องการแทนที่ซูเปอร์คลาสของคุณ ไม่ใช่คลาสของคุณเอง นั่นคือ:

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

หากคุณพยายามใช้โมดูลแทน alias_method_chain คุณจะได้ผลลัพธ์เป็น:

In child
In child, with extra message
In parent

แต่คุณ ไม่สามารถ จับคู่ผลลัพธ์เดิมโดยไม่ต้องเปลี่ยน log เมธอดใน ChildClass . การสืบทอดทับทิมไม่ได้ผลแบบนั้น ก็ไม่ได้

มีอะไรเปลี่ยนแปลงใน Ruby 2.0

จนถึง Ruby 2.0 ไม่มีวิธีเพิ่มโค้ด ด้านล่าง คลาส เหนือมันเท่านั้น แต่มี prepend คุณสามารถแทนที่เมธอดในคลาสด้วยเมธอดจากโมดูล และยังเข้าถึงการใช้งานคลาสด้วย super . จากตัวอย่างสุดท้าย เราจะได้ผลลัพธ์เดิมด้วย:

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

สมบูรณ์แบบ

ถ้า prepend ยังคงยากที่จะคาดคิด ให้คิดว่ามันเป็นการทำอะไรแบบนี้:

class NewChildClass < ChildClass
  include ExtraMessageLogging
end
  
ChildClass = NewChildClass

เว้นแต่จะไม่ยุ่งกับชื่อคลาสของคุณ และจะมีผลกับวัตถุที่มีอยู่แล้ว

(ใช่ คุณสามารถกำหนดชื่อชั้นเรียนใหม่ใน Ruby ได้ ไม่ มันอาจจะไม่ใช่ความคิดที่ดีนัก)

สิ่งนี้หมายความว่าอย่างไรสำหรับ Rails

ข้อแก้ตัวสุดท้ายสำหรับการใช้ alias_method_chain หายไปใน Ruby 2.0 เราสามารถนำหนึ่งในตัวอย่างที่เหลือของ alias_method_chain ใน Rails:

rails/activesupport/lib/active_support/core_ext/range/each.rb
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

และแทนที่ด้วยโมดูลแทน:

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 ไม่ได้รับการเปลี่ยนชื่อ และ ensure_iteration_allowed ไม่ปะติดปะต่อลิง

ใช้การสืบทอด ไม่ใช่แพตช์

Ruby มอบความยืดหยุ่นให้คุณมากมาย และนั่นเป็นเหตุผลหนึ่งที่ฉันชอบ แต่ก็มีโมเดลวัตถุที่ทรงพลังเช่นกัน ดังนั้น เมื่อคุณต้องการใส่โค้ดของคุณเอง ให้ลองพึ่งพาโมดูลและการสืบทอดก่อนที่จะแฮ็คเข้าไป รหัสของคุณจะเข้าใจและแก้ปัญหาได้ง่ายขึ้นมาก และคุณจะหลีกเลี่ยงผลข้างเคียงบางอย่างที่ยากต่อการตรวจจับ เช่น alias_method_chain .

alias_method_chain เป็นหนึ่งในวิธีที่ยอดเยี่ยมที่สุดที่ฉันรู้จักใน Rails แต่วันของมันก็นับ เราโตแล้ว ของหมดแล้วจะไม่พลาดค่ะ