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

Monkeypatching รับผิดชอบใน Ruby

เมื่อฉันเริ่มเขียนโค้ด Ruby อย่างมืออาชีพในปี 2011 สิ่งหนึ่งที่ทำให้ฉันประทับใจมากที่สุดเกี่ยวกับภาษาคือความยืดหยุ่น รู้สึกเหมือนกับ Ruby ทุกอย่างเป็นไปได้ เมื่อเทียบกับความแข็งแกร่งของภาษาอย่าง C# และ Java โปรแกรม Ruby เกือบจะดูเหมือน มีชีวิต .

พิจารณาว่าคุณสามารถทำอะไรได้มากมายในโปรแกรม Ruby คุณสามารถกำหนดและลบวิธีการได้ตามต้องการ คุณสามารถเรียกวิธีการที่ไม่มีอยู่ คุณสามารถเสกชั้นเรียนนิรนามทั้งหมดออกจากอากาศ มันดุร้ายมาก

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

Monkeypatches คืออะไร

ป้อน monkeypatch .

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

Monkeypatches ที่ไม่ซับซ้อนที่สุดเปิดคลาส ruby ​​อีกครั้งและแก้ไขพฤติกรรมโดยการเพิ่มหรือแทนที่เมธอด

แนวคิดในการเปิดใหม่นี้เป็นแกนหลักของโมเดลวัตถุของ Ruby ในขณะที่ใน Java สามารถกำหนดคลาสได้เพียงครั้งเดียว คลาส Ruby (และโมดูลสำหรับเรื่องนั้น) สามารถกำหนดได้หลายครั้ง เมื่อเรากำหนดคลาสเป็นครั้งที่สอง สาม สี่ ฯลฯ เราบอกว่าเรากำลัง เปิดใหม่ มัน. วิธีการใหม่ที่เรากำหนดจะถูกเพิ่มเข้าไปในคำจำกัดความของคลาสที่มีอยู่ และสามารถเรียกใช้บนอินสแตนซ์ของคลาสนั้นได้

ตัวอย่างสั้นๆ นี้แสดงให้เห็นถึงแนวคิดในการเปิดชั้นเรียนใหม่:

class Sounds
  def honk
    "Honk!"
  end
end
 
class Sounds
  def squeak
    "Squeak!"
  end
end
 
sounds = Sounds.new
sounds.honk    # => "Honk!"
sounds.squeak  # => "Squeak!"

สังเกตว่าทั้ง #honk และ #squeak วิธีการที่มีอยู่ใน Sounds เรียนผ่านเวทมนตร์แห่งการเปิดใหม่

โดยพื้นฐานแล้ว Monkeypatching คือการเปิดชั้นเรียนอีกครั้งในรหัสบุคคลที่สาม

Monkeypatching เป็นอันตรายหรือไม่

ถ้าประโยคก่อนหน้าทำให้คุณกลัว นั่นอาจเป็นสิ่งที่ดี Monkeypatching โดยเฉพาะอย่างยิ่งเมื่อทำโดยประมาทอาจทำให้เกิดความโกลาหลอย่างแท้จริง

พิจารณาสักครู่ว่าจะเกิดอะไรขึ้นหากเรากำหนด Array#<< . ใหม่ :

class Array
  def <<(*args)
    # do nothing 😈
  end
end

ด้วยโค้ดสี่บรรทัดนี้ ทุกอินสแตนซ์อาร์เรย์เดียวในโปรแกรมทั้งหมดจะใช้งานไม่ได้

ยิ่งไปกว่านั้น การใช้งานดั้งเดิมของ #<< หายไป นอกจากการรีสตาร์ทกระบวนการ Ruby แล้ว ไม่มีทางที่จะกู้คืนได้อีก

เมื่อ Monkeypatching ผิดพลาดอย่างมหันต์

ย้อนกลับไปในปี 2011 ฉันทำงานให้กับบริษัทโซเชียลเน็ตเวิร์กที่มีชื่อเสียง ในขณะนั้น codebase เป็นเสาหิน Rails ขนาดใหญ่ที่ทำงานบน Ruby 1.8.7 วิศวกรหลายร้อยคนมีส่วนร่วมใน Codebase ทุกวัน และการพัฒนาก็รวดเร็วมาก

มีอยู่ช่วงหนึ่ง ทีมของฉันตัดสินใจ Monkeypatch String#% เพื่อให้การเขียนพหูพจน์ง่ายขึ้นสำหรับวัตถุประสงค์ในการทำให้เป็นสากล นี่คือตัวอย่างสิ่งที่แพทช์ของเราสามารถทำได้:

replacements = {
  horse_count: 3,
  horses: {
    one: "is 1 horse",
    other: "are %{horse_count} horses"
  }
}
 
# "there are 3 horses in the barn"
"there %{horse_count:horses} in the barn" % replacements

เราเขียนแพตช์นี้และในที่สุดก็นำไปปรับใช้ในการผลิต... เพียงแต่พบว่ามันไม่ได้ผล ผู้ใช้ของเราเห็นสตริงที่มีตัวอักษร %{...} อักขระแทนข้อความพหูพจน์อย่างสวยงาม มันไม่สมเหตุสมผล แพตช์ทำงานได้ดีในสภาพแวดล้อมการพัฒนาบนแล็ปท็อปของฉัน เหตุใดจึงไม่ทำงานในการผลิต

ในตอนแรก เราคิดว่าเราพบจุดบกพร่องในตัว Ruby เอง แต่ต่อมาพบว่าคอนโซล Rails ที่ใช้งานจริงสร้างผลลัพธ์ที่แตกต่างจากคอนโซล Rails ในการพัฒนา เนื่องจากคอนโซลทั้งสองทำงานบน Ruby เวอร์ชันเดียวกัน เราจึงสามารถแยกแยะจุดบกพร่องในไลบรารีมาตรฐาน Ruby ได้ มีอย่างอื่นเกิดขึ้น

หลังจากใช้เวลาหลายวันในการเกาหัว เพื่อนร่วมงานสามารถติดตามตัวเริ่มต้น Rails ที่เพิ่ม อื่น การใช้งาน String#% ที่เราไม่เคยเห็นมาก่อน เพื่อทำให้สิ่งต่าง ๆ ซับซ้อนยิ่งขึ้น การใช้งานก่อนหน้านี้ยังมีข้อบกพร่อง ดังนั้นผลลัพธ์ที่เราเห็นในคอนโซลการใช้งานจริงจึงแตกต่างจากเอกสารทางการของ Ruby

นั่นไม่ใช่จุดสิ้นสุดของเรื่องแม้ว่า ในการติดตาม Monkeypatch ก่อนหน้านี้ เรายังพบอีกไม่น้อยกว่าสามตัว ทั้งหมดแก้ไขด้วยวิธีเดียวกัน เรามองหน้ากันอย่างสยอง มันเคยทำงานอย่างไร?

ในที่สุดเราก็ชอล์กพฤติกรรมที่ไม่สอดคล้องกันจนถึงการโหลดอย่างกระตือรือร้นของ Rails ในการพัฒนา Rails lazy โหลดไฟล์ Ruby นั่นคือโหลดเฉพาะเมื่อ require ง. อย่างไรก็ตาม ในการผลิต Rails จะโหลดไฟล์ Ruby ของแอปทั้งหมดเมื่อเริ่มต้น สิ่งนี้สามารถโยนประแจลิงตัวใหญ่ไปเย็บลิงได้

ผลที่ตามมาของการเปิดชั้นเรียนใหม่อีกครั้ง

ในกรณีนี้ Monkeypatches แต่ละตัวได้เปิด String . อีกครั้ง คลาสและแทนที่เวอร์ชันที่มีอยู่ของ #% . อย่างมีประสิทธิภาพ ด้วยวิธีอื่น มีข้อผิดพลาดที่สำคัญหลายประการสำหรับแนวทางนี้:

  • แพตช์สุดท้ายที่ใช้ "ชนะ" ความหมาย พฤติกรรมขึ้นอยู่กับลำดับการโหลด
  • ไม่มีทางเข้าถึงการใช้งานเดิมได้
  • แพทช์แทบไม่มีร่องรอยการตรวจสอบเลย ซึ่งทำให้ยากต่อการค้นหาในภายหลัง

ไม่น่าแปลกใจเลยที่เราเจอสิ่งเหล่านี้

ตอนแรกเราไม่รู้ด้วยซ้ำว่ามีลิงตัวอื่นเล่นอยู่ เนื่องจากข้อผิดพลาดในวิธีการชนะ ดูเหมือนว่าการใช้งานดั้งเดิมจะใช้งานไม่ได้ เมื่อเราค้นพบแพตช์การแข่งขันอื่นๆ เราไม่สามารถบอกได้ว่าอันไหนชนะโดยไม่เพิ่ม puts มากมาย แถลงการณ์

สุดท้าย แม้ว่าเราจะค้นพบวิธีชนะในการพัฒนา วิธีอื่นก็จะชนะในการผลิต นอกจากนี้ยังเป็นการยากที่จะบอกได้ว่าโปรแกรมแก้ไขใดใช้ครั้งสุดท้ายเนื่องจาก Ruby 1.8 ไม่มี Method#source_location ที่ยอดเยี่ยม วิธีที่เรามี

ฉันใช้เวลาอย่างน้อยหนึ่งสัปดาห์ในการค้นหาว่าเกิดอะไรขึ้น เสียเวลาโดยเปล่าประโยชน์ในการไล่ตามปัญหาที่หลีกเลี่ยงได้โดยสิ้นเชิง

ในที่สุด เราก็ตัดสินใจเปิดตัว LocalizedString คลาส wrapper พร้อม #% . ประกอบ กระบวนการ. Stringของเรา Monkeypatch ก็กลายเป็น:

class String
  def localize
    LocalizedString.new(self)
  end
end

เมื่อ Monkeypatching ล้มเหลว

จากประสบการณ์ของผม Monkeypatches มักจะล้มเหลวด้วยเหตุผลสองประการ:

  • ตัวแพทช์เสีย ใน codebase ที่ฉันได้กล่าวไว้ข้างต้น ไม่เพียงแต่มีการใช้งานที่แข่งขันกันหลายรายการของวิธีการเดียวกัน แต่วิธีการที่ "ชนะ" ไม่ได้ผล
  • สมมติฐานไม่ถูกต้อง รหัสโฮสต์ได้รับการอัปเดตแล้วและโปรแกรมแก้ไขจะไม่มีผลตามที่เขียนไว้อีกต่อไป

มาดูหัวข้อย่อยที่สองอย่างละเอียดกัน

แม้แต่แผนการที่ดีที่สุด...

Monkeypatching มักจะล้มเหลวด้วยเหตุผลเดียวกับที่คุณเข้าถึงตั้งแต่แรก - เพราะคุณไม่มีสิทธิ์เข้าถึงรหัสดั้งเดิม ด้วยเหตุผลดังกล่าว รหัสเดิมสามารถเปลี่ยนจากใต้คุณได้

ลองพิจารณาตัวอย่างนี้ในอัญมณีที่แอปของคุณพึ่งพา:

class Sale
  def initialize(amount, discount_pct, tax_rate = nil)
    @amount = amount
    @discount_pct = discount_pct
    @tax_rate = tax_rate
  end
 
  def total
    discounted_amount + sales_tax
  end
 
  private
 
  def discounted_amount
    @amount * (1 - @discount_pct)
  end
 
  def sales_tax
    if @tax_rate
      discounted_amount * @tax_rate
    else
      0
    end
  end
end

รอนั่นไม่ถูกต้อง ภาษีขายควรใช้กับยอดเต็ม ไม่ใช่ยอดลดราคา คุณส่งคำขอดึงไปยังโครงการ ในขณะที่คุณรอให้ผู้ดูแลรวม PR ของคุณ คุณเพิ่ม Monkeypatch นี้ในแอปของคุณ:

class Sale
  private
 
  def sales_tax
    if @tax_rate
      @amount * @tax_rate
    else
      0
    end
  end
end

มันทำงานได้อย่างสมบูรณ์แบบ เช็คอินแล้วลืมไปเลย

ทุกอย่างดีเป็นเวลานาน แล้ววันหนึ่งทีมการเงินจะส่งอีเมลถึงคุณเพื่อถามว่าเหตุใดบริษัทจึงไม่เก็บภาษีขายเป็นเวลาหนึ่งเดือน

สับสน คุณเริ่มขุดคุ้ยปัญหาและในที่สุดก็สังเกตเห็นว่าเพื่อนร่วมงานคนหนึ่งของคุณเพิ่งอัปเดตอัญมณีที่มี Sale ระดับ. นี่คือรหัสที่อัปเดต:

class Sale
  def initialize(amount, discount_pct, sales_tax_rate = nil)
    @amount = amount
    @discount_pct = discount_pct
    @sales_tax_rate = sales_tax_rate
  end
 
  def total
    discounted_amount + sales_tax
  end
 
  private
 
  def discounted_amount
    @amount * (1 - @discount_pct)
  end
 
  def sales_tax
    if @sales_tax_rate
      discounted_amount * @sales_tax_rate
    else
      0
    end
  end
end

ดูเหมือนว่าหนึ่งในผู้ดูแลโครงการเปลี่ยนชื่อ @tax_rate ตัวแปรอินสแตนซ์เป็น @sales_tax_rate . Monkeypatch ตรวจสอบค่าของเก่า @tax_rate ตัวแปรซึ่งมักจะ nil . ไม่มีใครสังเกตเห็นเพราะไม่มีข้อผิดพลาดเกิดขึ้น แอปใช้งานได้เหมือนไม่มีอะไรเกิดขึ้น

ทำไมต้อง Monkeypatch?

จากตัวอย่างเหล่านี้ ดูเหมือนว่า Monkeypatching จะไม่คุ้มกับอาการปวดหัวที่อาจเกิดขึ้น แล้วเราจะทำไปทำไม? ในความคิดของฉัน มีกรณีการใช้งานหลักสามกรณี:

  • เพื่อแก้ไขโค้ดบุคคลที่สามที่เสียหรือไม่สมบูรณ์
  • เพื่อทดสอบการเปลี่ยนแปลงหรือการเปลี่ยนแปลงหลายอย่างในการพัฒนาอย่างรวดเร็ว
  • เพื่อรวมฟังก์ชันที่มีอยู่ด้วยเครื่องมือวัดหรือโค้ดคำอธิบายประกอบ

ในบางกรณี เท่านั้น วิธีที่มีประสิทธิภาพในการแก้ไขข้อผิดพลาดหรือปัญหาด้านประสิทธิภาพในโค้ดของบุคคลที่สามคือการใช้ Monkeypatch

แต่พลังอันยิ่งใหญ่มาพร้อมกับความรับผิดชอบที่ยิ่งใหญ่

Monkeypatching อย่างมีความรับผิดชอบ

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

นี่คือรายการกฎที่ฉันพยายามปฏิบัติตาม:

  1. ห่อโปรแกรมแก้ไขในโมดูลด้วยชื่อที่ชัดเจนแล้วใช้ Module#prepend เพื่อนำไปใช้
  2. ตรวจสอบให้แน่ใจว่าคุณกำลังแก้ไขสิ่งที่ถูกต้อง
  3. จำกัดพื้นที่ผิวของแผ่นแปะ
  4. หาทางหนีให้ตัวเอง
  5. สื่อสารมากเกินไป

สำหรับส่วนที่เหลือของบทความนี้ เราจะใช้กฎเหล่านี้เพื่อเขียนโปรแกรมแก้ไขลิงสำหรับ DateTimeSelector ของ Rails ดังนั้นจึงเลือกข้ามการแสดงผลฟิลด์ที่ถูกทิ้ง นี่คือการเปลี่ยนแปลงที่ฉันพยายามทำกับ Rails เมื่อไม่กี่ปีก่อน สามารถดูรายละเอียดได้ที่นี่

คุณไม่จำเป็นต้องรู้อะไรมากเกี่ยวกับทุ่งที่ถูกทิ้งเพื่อทำความเข้าใจ Monkeypatch ท้ายที่สุดแล้ว สิ่งที่ทำคือแทนที่เมธอดเดียวที่เรียกว่า build_hidden กับสิ่งที่ไม่มีประสิทธิภาพ

เริ่มกันเลย!

ใช้ Module#prepend

ในฐานโค้ดที่ฉันพบในบทบาทก่อนหน้านี้ การใช้งาน String#% . ทั้งหมด ถูกนำไปใช้โดยเปิด String . อีกครั้ง ระดับ. นี่คือรายการเสริมของข้อเสียที่ฉันกล่าวถึงก่อนหน้านี้:

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

จะดีกว่ามากที่จะห่อแพตช์ของคุณในโมดูลและใช้งานโดยใช้ Module#prepend . การทำเช่นนี้ทำให้คุณมีอิสระในการเรียกการใช้งานแบบเดิม และเรียกใช้ Module#ancestors ได้อย่างรวดเร็ว จะแสดงแพตช์ในลำดับชั้นการสืบทอด ดังนั้นจึงง่ายต่อการค้นหาหากมีสิ่งผิดปกติ

สุดท้าย prepend . อย่างง่าย คำสั่งนั้นง่ายต่อการแสดงความคิดเห็นหากคุณต้องการปิดการใช้งานโปรแกรมแก้ไขด้วยเหตุผลบางประการ

นี่คือจุดเริ่มต้นของโมดูลสำหรับ Monkeypatch ของ Rails:

module RenderDiscardedMonkeypatch
end
 
ActionView::Helpers::DateTimeSelector.prepend(
  RenderDiscardedMonkeypatch
)

แก้ไขสิ่งที่ใช่

หากคุณนำสิ่งหนึ่งออกไปจากบทความนี้ ปล่อยให้เป็นดังนี้:อย่าใช้โปรแกรมแก้ไขลิงเว้นแต่คุณจะรู้ว่าคุณกำลังแก้ไขโค้ดที่ถูกต้อง ในกรณีส่วนใหญ่ คุณควรตรวจสอบโดยทางโปรแกรมว่าสมมติฐานของคุณยังคงมีอยู่ (นี่คือ Ruby หลังจากทั้งหมด) นี่คือรายการตรวจสอบ:

  1. ตรวจสอบให้แน่ใจว่ามีคลาสหรือโมดูลที่คุณกำลังพยายามแก้ไขอยู่
  2. ตรวจสอบให้แน่ใจว่ามีวิธีการอยู่และมีความเหมาะสม
  3. หากโค้ดที่คุณกำลังแก้ไขอยู่ในอัญมณี ให้ตรวจสอบเวอร์ชันของอัญมณี
  4. ประกันตัวด้วยข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์หากไม่เป็นไปตามสมมติฐาน

ทันทีที่รหัสแพทช์ของเราได้ตั้งสมมติฐานที่สำคัญมาก จะถือว่าค่าคงที่ที่เรียกว่า ActionView::Helpers::DateTimeSelector มีอยู่และเป็นคลาสหรือโมดูล

ตรวจสอบคลาส/โมดูล

ตรวจสอบให้แน่ใจว่ามีค่าคงที่ก่อนที่จะพยายามแก้ไข:

module RenderDiscardedMonkeypatch
end
 
const = begin
  Kernel.const_get('ActionView::Helpers::DateTimeSelector')
rescue NameError
end
 
if const
  const.prepend(RenderDiscardedMonkeypatch)
end

ดีมาก แต่ตอนนี้เราได้ปล่อยตัวแปรในเครื่องออกมาแล้ว (const ) สู่ขอบเขตสากล มาแก้ไขกันเถอะ:

module RenderDiscardedMonkeypatch
  def self.apply_patch
    const = begin
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    if const
      const.prepend(self)
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

ตรวจสอบวิธีการ

ต่อไป มาแนะนำตัวแก้ไข build_hidden กระบวนการ. มาเพิ่มการตรวจสอบเพื่อให้แน่ใจว่ามีอยู่และยอมรับจำนวนอาร์กิวเมนต์ที่ถูกต้อง (เช่น มีจำนวนอาร์กิวเมนต์ที่ถูกต้อง) หากสมมติฐานเหล่านั้นไม่คงอยู่ แสดงว่ามีบางอย่างผิดปกติ:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      if const && mtd && mtd.arity == 2
        const.prepend(self)
      end
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
  end
 
  def build_hidden(type, value)
    ''
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

ตรวจสอบเวอร์ชั่นของอัญมณี

สุดท้าย ให้ตรวจสอบว่าเราใช้ Rails เวอร์ชันที่ถูกต้องหรือไม่ หาก Rails ได้รับการอัปเกรด เราอาจจำเป็นต้องอัปเดตแพตช์ด้วย (หรือกำจัดทิ้งทั้งหมด)

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      if const && mtd && mtd.arity == 2 && rails_version_ok?
        const.prepend(self)
      end
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  def build_hidden(type, value)
    ''
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

ประกันตัวช่วย

หากรหัสยืนยันของคุณเผยให้เห็นความแตกต่างระหว่างความคาดหวังกับความเป็นจริง คุณควรแจ้งข้อผิดพลาดหรือพิมพ์ข้อความเตือนที่เป็นประโยชน์เป็นอย่างน้อย แนวคิดนี้คือการเตือนคุณและเพื่อนร่วมงานของคุณเมื่อมีบางอย่างผิดปกติ

ต่อไปนี้คือวิธีที่เราอาจแก้ไขแพตช์ Rails ของเรา:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching "\
          "ActionView's date_select helper. Please investigate."
      end
 
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since "\
          "ActionView's date_select helper was monkeypatched in "\
          "#{__FILE__}. Please reevaluate the patch."
      end
 
      const.prepend(self)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  def build_hidden(type, value)
    ''
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

จำกัดพื้นที่ผิว

แม้ว่าการกำหนดวิธีตัวช่วยใน Monkeypatch อาจดูไม่อันตรายนัก แต่อย่าลืมว่าวิธีการใดๆ ที่กำหนดผ่าน Module#prepend จะแทนที่สิ่งที่มีอยู่ด้วยเวทมนตร์แห่งมรดก แม้ว่าอาจดูเหมือนโฮสต์คลาสหรือโมดูลไม่ได้กำหนดวิธีการเฉพาะ แต่ก็ยากที่จะทราบได้อย่างแน่นอน ด้วยเหตุนี้ ฉันจึงพยายามกำหนดเฉพาะวิธีที่ฉันตั้งใจจะแก้ไข

โปรดทราบว่าสิ่งนี้ยังใช้กับเมธอดที่กำหนดไว้ในคลาสซิงเกิลตันของอ็อบเจ็กต์ด้วย เช่น เมธอดที่กำหนดไว้ภายใน class << self .

ต่อไปนี้คือวิธีแก้ไขโปรแกรมแก้ไข Rails ของเราเพื่อแทนที่เพียงตัวเดียว #build_hidden วิธีการ:

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching"\
          "ActionView's date_select helper. Please investigate."
      end
 
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since"\
          "ActionView's date_selet helper was monkeypatched in "\
          "#{__FILE__}. Please reevaluate the patch."
      end
 
      const.prepend(InstanceMethods)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  module InstanceMethods
    def build_hidden(type, value)
      ''
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

หาที่หลบภัยให้ตัวเอง

เมื่อเป็นไปได้ ฉันชอบที่จะเลือกใช้ฟังก์ชันการทำงานของ Monkeypatch นั่นเป็นเพียงตัวเลือกจริงๆ หากคุณสามารถควบคุมตำแหน่งที่จะเรียกใช้โค้ดแพตช์ได้ ในกรณีของโปรแกรมแก้ไข Rails ของเรา สามารถทำได้ผ่าน @options แฮชใน DateTimeSelector :

module RenderDiscardedMonkeypatch
  class << self
    def apply_patch
      const = find_const
      mtd = find_method(const)
 
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching"\
          "ActionView's date_select helper. Please investigate."
      end
 
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since"\
          "ActionView's date_selet helper was monkeypatched in "\
          "#{__FILE__}. Please reevaluate the patch."
      end
 
      const.prepend(InstanceMethods)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  module InstanceMethods
    def build_hidden(type, value)
      if @options.fetch(:render_discarded, true)
        super
      else
        ''
      end
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

ดี! ตอนนี้ผู้โทรสามารถเลือกรับได้โดยโทรไปที่ date_select ตัวช่วยด้วยตัวเลือกใหม่ ไม่มีโค้ดพาธอื่นได้รับผลกระทบ:

date_select(@user, :date_of_birth, {
  order: [:month, :day],
  render_discarded: false
})

สื่อสารมากเกินไป

คำแนะนำสุดท้ายที่ฉันมีสำหรับคุณอาจเป็นสิ่งที่สำคัญที่สุด — การสื่อสารสิ่งที่แพทช์ของคุณทำและเมื่อถึงเวลาตรวจสอบอีกครั้ง เป้าหมายของคุณกับ Monkeypatches ควรจะลบแพทช์ออกทั้งหมดในที่สุด ด้วยเหตุนี้ Monkeypatch ที่รับผิดชอบจึงมีความคิดเห็นว่า:

  • อธิบายว่าแพตช์ทำอะไร
  • อธิบายว่าทำไมต้องมีโปรแกรมแก้ไข
  • ร่างสมมติฐานที่แพตช์สร้างขึ้น
  • ระบุวันที่ในอนาคตที่ทีมของคุณควรพิจารณาทางเลือกอื่น เช่น การดึงอัญมณีที่อัปเดต
  • รวมลิงก์ไปยังคำขอดึงที่เกี่ยวข้อง โพสต์บล็อก คำตอบ StackOverflow ฯลฯ

คุณอาจพิมพ์คำเตือนหรือทำการทดสอบไม่ผ่านในวันที่กำหนดไว้เพื่อกระตุ้นให้ทีมยืนยันสมมติฐานของแพตช์อีกครั้งและพิจารณาว่ายังจำเป็นอยู่หรือไม่

นี่คือเวอร์ชันสุดท้ายของ Rails date_select แพตช์พร้อมความคิดเห็นและการตรวจสอบวันที่:

# ActionView's date_select helper provides the option to "discard" certain
# fields. Discarded fields are (confusingly) still rendered to the page
# using hidden inputs, i.e. <input type="hidden" />. This patch adds an
# additional option to the date_select helper that allows the caller to
# skip rendering the chosen fields altogether. For example, to render all
# but the year field, you might have this in one of your views:
#
# date_select(:date_of_birth, order: [:month, :day])
#
# or, equivalently:
#
# date_select(:date_of_birth, discard_year: true)
#
# To avoid rendering the year field altogether, set :render_discarded to
# false:
#
# date_select(:date_of_birth, discard_year: true, render_discarded: false)
#
# This patch assumes the #build_hidden method exists on
# ActionView::Helpers::DateTimeSelector and accepts two arguments.
#
module RenderDiscardedMonkeypatch
  class << self
    EXPIRATION_DATE = Date.new(2021, 8, 15)
 
    def apply_patch
      if Date.today > EXPIRATION_DATE
        puts "WARNING: Please re-evaluate whether or not the ActionView "\
          "date_select patch present in #{__FILE__} is still necessary."
      end
 
      const = find_const
      mtd = find_method(const)
 
      # make sure the class we want to patch exists;
      # make sure the #build_hidden method exists and accepts exactly
      # two arguments
      unless const && mtd && mtd.arity == 2
        raise "Could not find class or method when patching "\
          "ActionView's date_select helper. Please investigate."
      end
 
      # if rails has been upgraded, make sure this patch is still
      # necessary
      unless rails_version_ok?
        puts "WARNING: It looks like Rails has been upgraded since "\
          "ActionView's date_select helper was monkeypatched in "\
          "#{__FILE__}. Please re-evaluate the patch."
      end
 
      # actually apply the patch
      const.prepend(InstanceMethods)
    end
 
    private
 
    def find_const
      Kernel.const_get('ActionView::Helpers::DateTimeSelector')
    rescue NameError
      # return nil if the constant doesn't exist
    end
 
    def find_method(const)
      return unless const
      const.instance_method(:build_hidden)
    rescue NameError
      # return nil if the method doesn't exist
    end
 
    def rails_version_ok?
      Rails::VERSION::MAJOR == 6 && Rails::VERSION::MINOR == 1
    end
  end
 
  module InstanceMethods
    # :render_discarded is an additional option you can pass to the
    # date_select helper in your views. Use it to avoid rendering
    # "discarded" fields, i.e. fields marked as discarded or simply
    # not included in date_select's :order array. For example,
    # specifying order: [:day, :month] will cause the helper to
    # "discard" the :year field. Discarding a field renders it as a
    # hidden input. Set :render_discarded to false to avoid rendering
    # it altogether.
    def build_hidden(type, value)
      if @options.fetch(:render_discarded, true)
        super
      else
        ''
      end
    end
  end
end
 
RenderDiscardedMonkeypatch.apply_patch

บทสรุป

ฉันเข้าใจดีว่าคำแนะนำบางข้อที่ฉันได้กล่าวไว้ข้างต้นอาจดูเหมือนใช้ยากเกินไป โปรแกรมแก้ไข Rails ของเรามีรหัสยืนยันการป้องกันมากกว่ารหัสโปรแกรมแก้ไขจริง!

คิดว่ารหัสพิเศษทั้งหมดนั้นเป็นปลอกดาบของคุณ ง่ายกว่ามากที่จะหลีกเลี่ยงการถูกตัดหากหุ้มด้วยชั้นการป้องกัน

สิ่งที่สำคัญจริงๆ คือ ฉันรู้สึกมั่นใจในการปรับใช้ Monkeypatches ที่รับผิดชอบในการผลิต สิ่งที่ขาดความรับผิดชอบเป็นเพียงระเบิดเวลาที่รอให้คุณหรือบริษัทเสียเวลา เงิน และสุขภาพของนักพัฒนา

ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!