เมื่อฉันเริ่มเขียนโค้ด 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 อาจทำให้เกิดความโกลาหลเมื่อทำไม่ดี อย่างไรก็ตาม หากทำด้วยความระมัดระวังและความขยันหมั่นเพียร ไม่มีเหตุผลใดที่จะหลีกเลี่ยงการเข้าถึงเมื่อสถานการณ์สมควรได้รับ
นี่คือรายการกฎที่ฉันพยายามปฏิบัติตาม:
- ห่อโปรแกรมแก้ไขในโมดูลด้วยชื่อที่ชัดเจนแล้วใช้
Module#prepend
เพื่อนำไปใช้ - ตรวจสอบให้แน่ใจว่าคุณกำลังแก้ไขสิ่งที่ถูกต้อง
- จำกัดพื้นที่ผิวของแผ่นแปะ
- หาทางหนีให้ตัวเอง
- สื่อสารมากเกินไป
สำหรับส่วนที่เหลือของบทความนี้ เราจะใช้กฎเหล่านี้เพื่อเขียนโปรแกรมแก้ไขลิงสำหรับ 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 หลังจากทั้งหมด) นี่คือรายการตรวจสอบ:
- ตรวจสอบให้แน่ใจว่ามีคลาสหรือโมดูลที่คุณกำลังพยายามแก้ไขอยู่
- ตรวจสอบให้แน่ใจว่ามีวิธีการอยู่และมีความเหมาะสม
- หากโค้ดที่คุณกำลังแก้ไขอยู่ในอัญมณี ให้ตรวจสอบเวอร์ชันของอัญมณี
- ประกันตัวด้วยข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์หากไม่เป็นไปตามสมมติฐาน
ทันทีที่รหัสแพทช์ของเราได้ตั้งสมมติฐานที่สำคัญมาก จะถือว่าค่าคงที่ที่เรียกว่า 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 ของเราและไม่พลาดแม้แต่โพสต์เดียว!