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

रूबी में सुरक्षित मंकीपैचिंग मास्टर करें: रखरखाव योग्य कोड के लिए सर्वोत्तम अभ्यास

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

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

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

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

आइए मंकीपैच से मिलते हैं।

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

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

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

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

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) # कुछ न करें 😈 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

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

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

अपने लिए एस्केप हैच दें

जब संभव हो, मैं अपने मंकीपैच की कार्यक्षमता को ऑप्ट-इन बनाना पसंद करता हूं। यह वास्तव में केवल एक विकल्प है यदि आपके पास इस बात पर नियंत्रण है कि पैच्ड कोड कहां लागू किया जाता है। हमारे रेल्स पैच के मामले में, यह DateTimeSelector में @options हैश के माध्यम से संभव है:

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. . 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

निष्कर्ष

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

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

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

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

  1. Ruby में Lambdas में महारत हासिल करें: बुनियादी से उन्नत तकनीकें तक Ruby में Lambdas में महारत हासिल करें: बुनियादी से उन्नत तकनीकें तक

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

  1. रूबी के साथ इंसर्शन सॉर्ट में महारत: एक चरण-दर-चरण गाइड रूबी के साथ इंसर्शन सॉर्ट में महारत: एक चरण-दर-चरण गाइड

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

  1. Ruby 2.6 में 9 प्रमुख संवर्द्धन का अन्वेषण Ruby 2.6 में 9 प्रमुख संवर्द्धन का अन्वेषण

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