Computer >> कंप्यूटर >  >> प्रोग्रामिंग >> Ruby

रूबी में जिम्मेदार मंकीपैचिंग

जब मैंने पहली बार 2011 में पेशेवर रूप से रूबी कोड लिखना शुरू किया, तो भाषा के बारे में मुझे सबसे ज्यादा प्रभावित करने वाली चीजों में से एक इसकी लचीलापन थी। ऐसा लगा जैसे रूबी के साथ सब कुछ संभव है। C# और Java जैसी भाषाओं की कठोरता की तुलना में, रूबी प्रोग्राम लगभग ऐसा लग रहा था जैसे वे जीवित थे ।

गौर करें कि रूबी प्रोग्राम में आप कितनी अविश्वसनीय चीजें कर सकते हैं। आप अपनी इच्छानुसार विधियों को परिभाषित और हटा सकते हैं। आप उन विधियों को कॉल कर सकते हैं जो मौजूद नहीं हैं। आप पूरी अनाम कक्षाओं को पतली हवा से बाहर निकाल सकते हैं। यह बिल्कुल जंगली है।

लेकिन कहानी यहीं खत्म नहीं होती। जब आप इन तकनीकों को अपने कोड के अंदर लागू कर सकते हैं, रूबी आपको उन्हें वर्चुअल मशीन में लोड की गई किसी भी चीज़ पर लागू करने देती है। दूसरे शब्दों में, आप अन्य लोगों के कोड के साथ उतनी ही आसानी से खिलवाड़ कर सकते हैं, जितनी आसानी से आप स्वयं कर सकते हैं।

मंकीपैच क्या हैं?

मंकीपैच दर्ज करें ।

संक्षेप में, मंकीपैच मौजूदा कोड के साथ "बंदर" है। मौजूदा कोड अक्सर वह कोड होता है जिस तक आपकी सीधी पहुंच नहीं होती है, जैसे किसी रत्न से कोड या रूबी मानक पुस्तकालय से। पैच आमतौर पर बग को ठीक करने, प्रदर्शन में सुधार करने आदि के लिए मूल कोड के व्यवहार को बदलने के लिए डिज़ाइन किए जाते हैं।

सबसे अत्याधुनिक बंदर पैच रूबी कक्षाओं को फिर से खोलते हैं और तरीकों को जोड़कर या ओवरराइड करके व्यवहार को संशोधित करते हैं।

यह फिर से खोलने का विचार रूबी के ऑब्जेक्ट मॉडल का मूल है। जबकि जावा में, कक्षाओं को केवल एक बार परिभाषित किया जा सकता है, रूबी कक्षाओं (और उस मामले के लिए मॉड्यूल) को कई बार परिभाषित किया जा सकता है। जब हम कक्षा को दूसरी, तीसरी, चौथी बार आदि परिभाषित करते हैं, तो हम कहते हैं कि हम फिर से खोल रहे हैं यह। हमारे द्वारा परिभाषित कोई भी नई विधि मौजूदा वर्ग परिभाषा में जोड़ दी जाती है और उस वर्ग के उदाहरणों पर कॉल की जा सकती है।

यह संक्षिप्त उदाहरण कक्षा को फिर से खोलने की अवधारणा को दिखाता है:

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 . पर उपलब्ध हैं फिर से खोलने के जादू के माध्यम से कक्षा।

अनिवार्य रूप से, मंकीपैचिंग तृतीय-पक्ष कोड में कक्षाओं को फिर से खोलने का कार्य है।

क्या मंकीपैचिंग खतरनाक है?

अगर पिछला वाक्य आपको डराता है, तो शायद यह अच्छी बात है। मंकीपैचिंग, खासकर जब लापरवाही से की जाती है, वास्तविक अराजकता पैदा कर सकती है।

एक पल के लिए विचार करें कि अगर हम Array#<< . को फिर से परिभाषित करें तो क्या होगा? :

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

कोड की इन चार पंक्तियों के साथ, पूरे कार्यक्रम में हर एक सरणी उदाहरण अब टूट गया है।

और भी, #<< . का मूल कार्यान्वयन चला गया है। रूबी प्रक्रिया को फिर से शुरू करने के अलावा, इसे वापस पाने का कोई तरीका नहीं है।

जब मंकीपैचिंग बहुत गलत हो जाती है

2011 में वापस, मैंने एक प्रमुख सोशल नेटवर्किंग कंपनी के लिए काम किया। उस समय, कोडबेस रूबी 1.8.7 पर चलने वाला एक विशाल रेल मोनोलिथ था। कई सौ इंजीनियरों ने दैनिक आधार पर कोडबेस में योगदान दिया, और विकास की गति बहुत तेज थी।

एक समय पर, मेरी टीम ने 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

हमने पैच लिखा और अंततः इसे उत्पादन में लगाया... केवल यह पता लगाने के लिए कि यह काम नहीं कर रहा था। हमारे उपयोगकर्ता शाब्दिक %{...} . के साथ तार देख रहे थे अच्छी तरह से बहुवचन पाठ के बजाय वर्ण। इसका कोई मतलब नहीं था। पैच ने मेरे लैपटॉप पर विकास के माहौल में पूरी तरह से अच्छा काम किया था। यह उत्पादन में काम क्यों नहीं कर रहा था?

प्रारंभ में, हमने सोचा था कि हमें रूबी में ही एक बग मिलेगा, केवल बाद में, यह पता लगाने के लिए कि एक उत्पादन रेल कंसोल ने विकास में रेल कंसोल की तुलना में एक अलग परिणाम उत्पन्न किया। चूंकि दोनों कंसोल एक ही रूबी संस्करण पर चलते हैं, इसलिए हम रूबी मानक पुस्तकालय में एक बग से इंकार कर सकते हैं। कुछ और चल रहा था।

कई दिनों तक कड़ी मशक्कत के बाद, एक सहकर्मी एक रेल इनिशियलाइज़र को ट्रैक करने में सक्षम था जिसने एक और जोड़ा String#% . का कार्यान्वयन जो हममें से किसी ने पहले नहीं देखा था। चीजों को और अधिक जटिल बनाने के लिए, इस पहले के कार्यान्वयन में एक बग भी था, इसलिए उत्पादन कंसोल में हमने जो परिणाम देखे, वे रूबी के आधिकारिक दस्तावेज़ीकरण से भिन्न थे।

हालांकि यह कहानी का अंत नहीं है। पहले के मंकीपैच को ट्रैक करने में, हमने कम से कम तीन अन्य लोगों को भी पाया, सभी एक ही तरीके को पैच कर रहे हैं। हमने एक दूसरे को दहशत से देखा। यह कभी कैसे काम करता है??

हमने अंततः असंगत व्यवहार को रेल की उत्सुक लोडिंग तक चाक-चौबंद कर दिया। विकास में, रेल आलसी रूबी फाइलों को लोड करता है, यानी, उन्हें केवल तभी लोड करता है जब वे require होते हैं डी। उत्पादन में, हालांकि, रेल शुरू होने पर ऐप की सभी रूबी फाइलों को लोड करता है। यह एक बड़े मंकी रिंच को मंकीपैचिंग में डाल सकता है।

कक्षा को फिर से खोलने के परिणाम

इस मामले में, प्रत्येक मंकीपैच ने String . को फिर से खोल दिया वर्ग और प्रभावी ढंग से #% . के मौजूदा संस्करण को बदल दिया दूसरे के साथ विधि। इस दृष्टिकोण के कई प्रमुख नुकसान हैं:

  • आखिरी पैच लागू किया गया "जीतता है", जिसका अर्थ है, व्यवहार लोड ऑर्डर पर निर्भर है
  • मूल कार्यान्वयन तक पहुंचने का कोई तरीका नहीं है
  • पैच लगभग कोई ऑडिट ट्रेल नहीं छोड़ते हैं, जिससे उन्हें बाद में ढूंढना बहुत मुश्किल हो जाता है

आश्चर्य की बात नहीं, शायद, हम इन सभी में भाग गए।

सबसे पहले, हमें यह भी नहीं पता था कि खेल में अन्य बंदर भी थे। जीतने के तरीके में बग के कारण, ऐसा प्रतीत होता है कि मूल कार्यान्वयन टूट गया था। जब हमने अन्य प्रतिस्पर्धी पैच की खोज की, तो यह बताना असंभव था कि प्रचुर मात्रा में puts को जोड़े बिना कौन जीता बयान।

अंत में, जब हमें पता चला कि विकास में कौन सी विधि जीती है, तो उत्पादन में एक अलग जीत होगी। यह बताना भी प्रोग्रामेटिक रूप से कठिन था कि रूबी 1.8 में अद्भुत Method#source_location नहीं होने के कारण आखिरी बार कौन सा पैच लागू किया गया था। विधि अब हमारे पास है।

मैंने कम से कम एक सप्ताह यह पता लगाने की कोशिश में बिताया कि क्या हो रहा था, समय मैंने अनिवार्य रूप से एक पूरी तरह से परिहार्य समस्या का पीछा करते हुए बर्बाद किया।

आखिरकार, हमने LocalizedString . को पेश करने का फैसला किया साथ में #% . के साथ रैपर क्लास तरीका। हमारा String मंकीपैच तो बस बन गया:

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

जब मंकीपैचिंग विफल हो जाए

मेरे अनुभव में, मंकीपैच अक्सर दो कारणों में से एक के कारण विफल हो जाते हैं:

  • पैच ही टूट गया है। कोडबेस में मैंने ऊपर उल्लेख किया है, न केवल एक ही विधि के कई प्रतिस्पर्धी कार्यान्वयन थे, बल्कि वह तरीका जो "जीता" काम नहीं करता था।
  • मान्यताएं अमान्य हैं। होस्ट कोड अपडेट कर दिया गया है और पैच अब लिखित रूप में लागू नहीं होता है।

आइए दूसरे बुलेट पॉइंट को अधिक विस्तार से देखें।

यहां तक ​​कि सबसे अच्छी योजनाएं भी...

मंकीपैचिंग अक्सर उसी कारण से विफल हो जाती है जिस कारण से आप इसके लिए पहली बार पहुंचे - क्योंकि आपके पास मूल कोड तक पहुंच नहीं है। ठीक इसी कारण से, मूल कोड आपके नीचे से बदल सकता है।

इस उदाहरण को एक ऐसे रत्न में देखें जिस पर आपका ऐप निर्भर करता है:

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

रुको, यह सही नहीं है। बिक्री कर पूरी राशि पर लागू किया जाना चाहिए, छूट वाली राशि पर नहीं। आप प्रोजेक्ट के लिए पुल अनुरोध सबमिट करें। जब आप अपने पीआर को मर्ज करने के लिए अनुरक्षक की प्रतीक्षा कर रहे हों, तो आप इस मंकीपैच को अपने ऐप में जोड़ें:

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 . मंकीपैच पुराने @tax_rate . के मान की जांच करता है चर, जो हमेशा nil . होता है . किसी ने ध्यान नहीं दिया क्योंकि कोई त्रुटि कभी नहीं उठाई गई थी। ऐप ऐसे चिपक गया जैसे कुछ हुआ ही न हो।

मंकीपैच क्यों?

इन उदाहरणों को देखते हुए, ऐसा लग सकता है कि मंकीपैचिंग संभावित सिरदर्द के लायक नहीं है। तो हम यह क्यों करते है? मेरी राय में, तीन प्रमुख उपयोग-मामले हैं:

  • टूटे या अपूर्ण तृतीय-पक्ष कोड को ठीक करने के लिए
  • विकास में किसी परिवर्तन या अनेक परिवर्तनों का शीघ्रता से परीक्षण करने के लिए
  • मौजूदा कार्यक्षमता को इंस्ट्रूमेंटेशन या एनोटेशन कोड के साथ लपेटने के लिए

कुछ मामलों में, केवल तृतीय-पक्ष कोड में बग या प्रदर्शन समस्या को हल करने का व्यवहार्य तरीका एक मंकीपैच लागू करना है।

लेकिन बड़ी ताकत के साथ बड़ी जिम्मेदारी आती है।

जिम्मेदारी से मंकीपैचिंग

मैं मंकीपैचिंग बातचीत को जिम्मेदारी के इर्द-गिर्द रखना पसंद करता हूं, बजाय इसके कि यह अच्छा है या बुरा। निश्चित रूप से, बंदरपैचिंग खराब तरीके से किए जाने पर अराजकता पैदा कर सकता है। हालांकि, अगर कुछ सावधानी और परिश्रम के साथ किया जाता है, तो स्थिति के लिए आवश्यक होने पर उस तक पहुंचने से बचने का कोई कारण नहीं है।

यहां उन नियमों की सूची दी गई है जिनका मैं पालन करने का प्रयास करता हूं:

  1. एक स्पष्ट नाम वाले मॉड्यूल में पैच लपेटें और Module#prepend का उपयोग करें इसे लागू करने के लिए
  2. सुनिश्चित करें कि आप सही चीज़ पैच कर रहे हैं
  3. पैच के सतह क्षेत्र को सीमित करें
  4. खुद को बच निकलने दें
  5. अति-संवाद

इस लेख के शेष भाग के लिए, हम इन नियमों का उपयोग रेल के लिए एक मंकीपैच लिखने के लिए करेंगे DateTimeSelector इसलिए यह वैकल्पिक रूप से छोड़े गए फ़ील्ड को प्रस्तुत करना छोड़ देता है। यह एक बदलाव है जिसे मैंने वास्तव में कुछ साल पहले रेल में करने की कोशिश की थी। आप विवरण यहां पा सकते हैं।

हालांकि, मंकीपैच को समझने के लिए आपको छोड़े गए खेतों के बारे में ज्यादा जानने की जरूरत नहीं है। दिन के अंत में, यह केवल build_hidden . नामक एक विधि को प्रतिस्थापित करता है एक के साथ जो प्रभावी रूप से कुछ नहीं करता है।

आइए शुरू करें!

Module#prepend का उपयोग करें

कोडबेस में मैंने अपनी पिछली भूमिका में, String#% . के सभी कार्यान्वयनों का सामना किया String . को फिर से खोलकर लागू किया गया था कक्षा। यहां उन कमियों की एक संवर्धित सूची दी गई है जिनका मैंने पहले उल्लेख किया था:

  • ऐसा प्रतीत होता है कि त्रुटियाँ पैच कोड के बजाय होस्ट वर्ग या मॉड्यूल से उत्पन्न हुई हैं
  • पैच में परिभाषित कोई भी विधि मौजूदा विधियों को उसी नाम से बदल देती है, जिसका अर्थ है, मूल कार्यान्वयन को लागू करने का कोई तरीका नहीं है।
  • यह जानने का कोई तरीका नहीं है कि कौन से पैच लागू किए गए थे और इसलिए, कौन से तरीके "जीते" थे
  • पैच लगभग कोई ऑडिट ट्रेल नहीं छोड़ते हैं, जिससे उन्हें बाद में ढूंढना बहुत मुश्किल हो जाता है

इसके बजाय, अपने पैच को मॉड्यूल में लपेटना और Module#prepend का उपयोग करके इसे लागू करना बेहतर है . ऐसा करने से आप मूल कार्यान्वयन को कॉल करने के लिए स्वतंत्र हैं, और Module#ancestors को एक त्वरित कॉल कर सकते हैं विरासत पदानुक्रम में पैच दिखाएगा ताकि यह पता लगाना आसान हो कि क्या चीजें गलत हैं।

अंत में, एक सरल prepend यदि आपको किसी कारण से पैच को अक्षम करने की आवश्यकता है, तो कथन पर टिप्पणी करना आसान है।

हमारे रेल्स मंकीपैच के लिए एक मॉड्यूल की शुरुआत यहां दी गई है:

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

सही चीज़ को पैच करें

यदि आप इस लेख से एक चीज दूर ले जाते हैं, तो इसे रहने दें:जब तक आप यह नहीं जानते कि आप सही कोड पैच कर रहे हैं, तब तक एक मंकीपैच लागू न करें। ज्यादातर मामलों में, प्रोग्रामेटिक रूप से सत्यापित करना संभव होना चाहिए कि आपकी धारणाएं अभी भी हैं (यह रूबी है)। ये रही एक चेकलिस्ट:

  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

रत्न संस्करण जांचें

अंत में, देखते हैं कि हम रेल के सही संस्करण का उपयोग कर रहे हैं। यदि रेल अपग्रेड हो जाती है, तो हमें पैच को भी अपडेट करने की आवश्यकता हो सकती है (या इसे पूरी तरह से हटा दें)।

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

मददपूर्वक जमानत लें

यदि आपका सत्यापन कोड अपेक्षाओं और वास्तविकता के बीच विसंगति को उजागर करता है, तो एक त्रुटि उत्पन्न करना या कम से कम एक उपयोगी चेतावनी संदेश प्रिंट करना एक अच्छा विचार है। जब कुछ गलत लगता है तो आपको और आपके सहकर्मियों को सतर्क करने का विचार यहां है।

यहां बताया गया है कि हम अपने रेल पैच को कैसे संशोधित कर सकते हैं:

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

सतह क्षेत्र सीमित करें

हालांकि मंकीपैच में सहायक विधियों को परिभाषित करना पूरी तरह से अहानिकर लग सकता है, याद रखें कि Module#prepend के माध्यम से परिभाषित कोई भी विधियाँ विरासत के जादू के माध्यम से मौजूदा लोगों को ओवरराइड करेगा। हालांकि ऐसा लग सकता है कि एक मेजबान वर्ग या मॉड्यूल किसी विशेष विधि को परिभाषित नहीं करता है, यह निश्चित रूप से जानना मुश्किल है। इस कारण से, मैं केवल उन तरीकों को परिभाषित करने का प्रयास करता हूं जिन्हें मैं पैच करना चाहता हूं।

ध्यान दें कि यह ऑब्जेक्ट के सिंगलटन वर्ग में परिभाषित विधियों पर भी लागू होता है, यानी class << self के अंदर परिभाषित विधियां ।

हमारे रेल पैच को केवल एक #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

खुद को बच निकलने दें

जब भी संभव हो, मैं अपने मंकीपैच की कार्यक्षमता को ऑप्ट-इन करना पसंद करता हूं। यह वास्तव में केवल एक विकल्प है यदि आपके पास नियंत्रण है जहां पैच कोड लागू किया गया है। हमारे रेल पैच के मामले में, यह @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
})

अति-संवाद

मेरे पास आपके लिए आखिरी सलाह शायद सबसे महत्वपूर्ण है - यह बताना कि आपका पैच क्या करता है और कब इसकी फिर से जांच करने का समय है। मंकीपैच के साथ आपका लक्ष्य हमेशा अंततः पैच को पूरी तरह से हटाना होना चाहिए। इसके लिए, एक जिम्मेदार मंकीपैच में ऐसी टिप्पणियां शामिल होती हैं जो:

  • बताएं कि पैच क्या करता है
  • स्पष्ट करें कि पैच क्यों आवश्यक है
  • उन धारणाओं को रेखांकित करें जो पैच बनाता है
  • भविष्य में एक तिथि निर्दिष्ट करें जब आपकी टीम को वैकल्पिक समाधानों पर पुनर्विचार करना चाहिए, जैसे कि एक अद्यतन रत्न में खींचना
  • प्रासंगिक पुल अनुरोध, ब्लॉग पोस्ट, स्टैक ओवरफ्लो उत्तर आदि के लिंक शामिल करें।

आप टीम को पैच की मान्यताओं की पुन:पुष्टि करने के लिए आग्रह करने के लिए एक चेतावनी प्रिंट कर सकते हैं या एक परीक्षण में असफल हो सकते हैं और विचार कर सकते हैं कि यह अभी भी आवश्यक है या नहीं।

यहां हमारे रेल का अंतिम संस्करण है 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

निष्कर्ष

मुझे पूरी तरह से लगता है कि ऊपर उल्लिखित कुछ सुझाव ओवरकिल की तरह लग सकते हैं। हमारे रेल पैच में वास्तविक पैच कोड की तुलना में कहीं अधिक रक्षात्मक सत्यापन कोड है!

उस अतिरिक्त कोड को अपने ब्रॉडस्वॉर्ड के लिए एक म्यान के रूप में सोचें। अगर इसे सुरक्षा की परत में लपेटा गया है तो कटने से बचना बहुत आसान है।

हालांकि, वास्तव में जो मायने रखता है, वह यह है कि मैं उत्पादन में जिम्मेदार बंदरों को तैनात करने में आत्मविश्वास महसूस करता हूं। गैर-जिम्मेदार लोग बस टाइम बम हैं जो आपको या आपकी कंपनी के समय, धन और डेवलपर स्वास्थ्य की कीमत चुकाने की प्रतीक्षा कर रहे हैं।

पी.एस. यदि आप रूबी मैजिक की पोस्ट प्रेस से बाहर होते ही पढ़ना चाहते हैं, तो हमारे रूबी मैजिक न्यूजलेटर की सदस्यता लें और एक भी पोस्ट मिस न करें!


  1. रूबी में लैम्ब्डा का उपयोग करना

    ब्लॉक रूबी का इतना महत्वपूर्ण हिस्सा हैं, उनके बिना भाषा की कल्पना करना मुश्किल है। लेकिन लैम्ब्डा? लैम्ब्डा को कौन प्यार करता है? आप एक का उपयोग किए बिना वर्षों तक जा सकते हैं। वे लगभग पुराने जमाने के अवशेष की तरह लगते हैं। ...लेकिन यह बिल्कुल सच नहीं है। एक बार जब आप उनकी थोड़ी जांच कर लेते हैं त

  1. रूबी में इंसर्शन सॉर्ट को समझना

    नोट:रूबी के साथ विभिन्न सॉर्टिंग एल्गोरिदम को लागू करने पर विचार करने वाली श्रृंखला में यह भाग 4 है। भाग 1 ने बबल सॉर्ट की खोज की, भाग 2 ने चयन प्रकार की खोज की, और भाग 3 ने मर्ज सॉर्ट की खोज की। जैसा कि हम डेटा सॉर्ट करने के लिए विभिन्न तरीकों का पता लगाना जारी रखते हैं, हम इंसर्शन सॉर्ट की ओर रु

  1. रूबी में 9 नई सुविधाएँ 2.6

    रूबी का एक नया संस्करण नई सुविधाओं और प्रदर्शन में सुधार के साथ आ रहा है। क्या आप परिवर्तनों के साथ बने रहना चाहेंगे? आइए एक नज़र डालते हैं! अंतहीन रेंज रूबी 2.5 और पुराने संस्करण पहले से ही अंतहीन श्रेणी के एक रूप का समर्थन करते हैं (Float::INFINITY के साथ) ), लेकिन रूबी 2.6 इसे अगले स्तर पर ले