ऑब्जेक्ट-ओरिएंटेड प्रोग्रामिंग में, एक ऑब्जेक्ट अक्सर कार्य करने के लिए दूसरे ऑब्जेक्ट पर निर्भर करेगा।
उदाहरण के लिए, यदि मैं फाइनेंस रिपोर्ट चलाने के लिए एक सिंपल क्लास बनाता हूँ:
class FinanceReport
def net_income
FinanceApi.gross_income - FinanceApi.total_costs
end
end
हम कह सकते हैं कि FinanceReport निर्भर करता है FinanceApi पर, जिसका उपयोग वह बाहरी पेमेंट प्रोसेसर से जानकारी खींचने के लिए करता है।
लेकिन क्या होगा यदि हम किसी बिंदु पर एक अलग API हिट करना चाहते हैं? या, अधिक संभावना है, क्या होगा यदि हम बाहरी संसाधनों को हिट किए बिना इस क्लास को टेस्ट करना चाहते हैं? सबसे आम उत्तर डिपेंडेंसी इंजेक्शन का उपयोग करना है।
डिपेंडेंसी इंजेक्शन के साथ, हम FinanceReport के अंदर FinanceApi को स्पष्ट रूप से संदर्भित नहीं करते। इसके बजाय, हम इसे एक आर्गुमेंट के रूप में पास करते हैं। हम इसे इंजेक्ट करते हैं।
डिपेंडेंसी इंजेक्शन का उपयोग करके, हमारी क्लास बन जाती है:
class FinanceReport
def net_income(financials)
financials.gross_income - financials.total_costs
end
end
अब हमारी क्लास को पता भी नहीं है कि FinanceApi ऑब्जेक्ट मौजूद है! हम कोई भी ऑब्जेक्ट इसे पास कर सकते हैं जब तक कि यह gross_income और total_costs को लागू करता है।
इसके कई लाभ हैं:
- हमारा कोड अब
FinanceApiसे कम "कपल्ड" है। - हम
FinanceApiका उपयोग पब्लिक इंटरफ़ेस के माध्यम से करने के लिए मजबूर हैं। - हम अब अपने टेस्ट्स में एक मॉक या स्टब ऑब्जेक्ट पास कर सकते हैं ताकि हमें वास्तविक API को हिट न करना पड़े।
अधिकांश डेवलपर्स डिपेंडेंसी इंजेक्शन को सामान्य रूप से एक अच्छी चीज़ मानते हैं (मैं भी!)। हालांकि, सभी तकनीकों की तरह, ट्रेड-ऑफ्स हैं।
हमारा कोड अब थोड़ा अधिक अपारदर्शी है। जब हमने स्पष्ट रूप से FinanceApi का उपयोग किया, तो यह स्पष्ट था कि हमारे वैल्यूज़ कहाँ से आ रहे हैं। डिपेंडेंसी इंजेक्शन को शामिल करने वाले कोड में यह उतना स्पष्ट नहीं है।
यदि कॉल्स अन्यथा self पर गए होते, तो हमने कोड को अधिक वर्बोज़ बना दिया है। ऑब्जेक्ट-ओरिएंटेड "एक ऑब्जेक्ट को मैसेज भेजें और उसे कार्य करने दें" पैराडाइम का उपयोग करने के बजाय, हम खुद को एक अधिक फंक्शनल "इनपुट्स -> आउटपुट्स" पैराडाइम की ओर बढ़ते हुए पाते हैं।
यह अंतिम मामला (कॉल्स को रीडायरेक्ट करना जो self पर गए होते) वह है जिसे मैं आज देखना चाहता हूं। मैं इन स्थितियों के लिए डिपेंडेंसी इंजेक्शन के एक संभावित विकल्प को प्रस्तुत करना चाहता हूं: बेस क्लास को डायनामिकली बदलें (किंडा)।
हल की जाने वाली समस्या
आइए एक पल के लिए पीछे हटें और उस समस्या से शुरू करें जिसने मुझे इस रास्ते पर ले जाना शुरू किया: PDF रिपोर्ट्स।
मेरे क्लाइंट ने विभिन्न प्रिंट करने योग्य PDF रिपोर्ट जनरेट करने की क्षमता का अनुरोध किया — एक रिपोर्ट जो एक अकाउंट के सभी खर्चों को सूचीबद्ध करती है, दूसरी राजस्व को सूचीबद्ध करती है, दूसरी भविष्य के वर्षों के लिए अनुमानित मुनाफे को सूचीबद्ध करती है, आदि।
हम इन PDFs को बनाने के लिए प्रतिष्ठित prawn जेम का उपयोग कर रहे हैं, प्रत्येक रिपोर्ट अपने स्वयं के Ruby ऑब्जेक्ट के रूप में Prawn::Document से सबक्लास्ड है।
कुछ ऐसा:
class CostReport < Prawn::Document
def initialize(...)
...
end
def render
text "Cost Report"
move_down 20
...
end
end
अब तक, सब ठीक है। लेकिन यहाँ रगड़ है: क्लाइंट एक "ओवरव्यू" रिपोर्ट चाहता है जिसमें इन अन्य सभी रिपोर्ट्स के भाग शामिल हों।
समाधान 1: डिपेंडेंसी इंजेक्शन
जैसा कि पहले उल्लेख किया गया है, इस तरह की समस्या का एक सामान्य समाधान कोड को डिपेंडेंसी इंजेक्शन का उपयोग करने के लिए रीफैक्टर करना है। यानी, इन सभी रिपोर्ट्स को self पर मेथड्स कॉल करने के बजाय, हम इसके बजाय अपने PDF डॉक्यूमेंट को एक आर्गुमेंट के रूप में पास करेंगे।
यह हमें कुछ ऐसा देगा:
class CostReport < Prawn::Document
...
def title(pdf = self)
pdf.text "Cost Report"
pdf.move_down 20
...
end
end
यह काम करता है, लेकिन यहाँ कुछ ओवरहेड है। एक बात के लिए, हर एक ड्राइंग मेथड को अब pdf आर्गुमेंट लेना होगा, और prawn पर हर एक कॉल को अब इस pdf आर्गुमेंट के माध्यम से जाना होगा।
डिपेंडेंसी इंजेक्शन के कुछ लाभ हैं: यह हमें हमारे सिस्टम में डिकॉपल्ड कंपोनेंट्स की ओर धकेलता है और यूनिट टेस्टिंग को आसान बनाने के लिए मॉक या स्टब पास करने की अनुमति देता है।
हालांकि, हम अपने परिदृश्य में इन लाभों को प्राप्त नहीं कर रहे हैं। हम पहले से ही prawn API से दृढ़ता से कपल्ड हैं, इसलिए एक अलग PDF लाइब्रेरी में बदलाव के लिए लगभग निश्चित रूप से कोड के पूर्ण रीराइट की आवश्यकता होगी।
टेस्टिंग भी यहां बड़ी चिंता नहीं है, क्योंकि हमारे मामले में ऑटोमेटेड टेस्ट्स के साथ जनरेटेड PDF रिपोर्ट्स का टेस्ट करना बहुत बोझिल है।
तो डिपेंडेंसी इंजेक्शन हमें वह व्यवहार देता है जो हम चाहते हैं लेकिन न्यूनतम लाभ के साथ अतिरिकtoverhead भी पेश करता है। आइए एक अन्य विकल्प देखें।
समाधान 2: डेलीगेशन
Ruby की स्टैंडर्ड लाइब्रेरी हमें डेकोरेटर पैटर्न को लागू करने के लिए एक आसान तरीके के रूप में SimpleDelegator प्रदान करती है। आप अपने ऑब्जेक्ट को कंस्ट्रक्टर में पास करते हैं, और फिर डेलीगेटर पर किसी भी मेथड कॉल को आपके ऑब्जेक्ट पर फ़ॉरवर्ड किया जाता है।
SimpleDelegator का उपयोग करके, हम एक बेस रिपोर्ट क्लास बना सकते हैं जो prawn के चारों ओर लपेटती है।
class PrawnWrapper < SimpleDelegator
def initialize(document: nil)
document ||= Prawn::Document.new(...)
super(document)
end
end
हम तब अपनी रिपोर्ट्स को इस क्लास से इनहेरिट करने के लिए अपडेट कर सकते हैं, और वे पहले की तरह ही कार्य करेंगी, हमारे इनिशियलाइज़र में बनाए गए डिफ़ॉल्ट डॉक्यूमेंट का उपयोग करके। जादू तब होता है जब हम इसे अपनी ओवरव्यू रिपोर्ट में उपयोग करते हैं:
class OverviewReport < PrawnWrapper
...
def render
sales = SaleReport.new(..., document: self)
sales.sales_table
costs = CostReport.new(..., document: self)
costs.costs_pie_chart
...
end
end
यहां SaleReport#sales_table और CostReport#costs_pie_chart अपरिवर्तित रहते हैं, लेकिन prawn पर उनके कॉल्स (उदाहरण के लिए, text(...), move_down 20, आदि) अब हमारे द्वारा बनाए गए SimpleDelegator के माध्यम से OverviewReport पर फ़ॉरवर्ड किए जा रहे हैं।
व्यवहार के संदर्भ में, हमने अनिवार्य रूप से ऐसा बना दिया है जैसे SalesReport अब OverviewReport का सबक्लास है। हमारे मामले में, इसका मतलब है कि prawn के API पर सभी कॉल्स अब SalesReport -> OverviewReport -> Prawn::Document पर जाते हैं।
SimpleDelegator कैसे काम करता है
SimpleDelegator जिस तरह से हुड के नीचे काम करता है वह मूल रूप से Ruby के method_missing कार्यक्षमता का उपयोग करके मेथड कॉल्स को दूसरे ऑब्जेक्ट पर फ़ॉरवर्ड करना है।
तो SimpleDelegator (या इसका सबक्लास) एक मेथड कॉल प्राप्त करता है। यदि यह उस मेथड को लागू करता है, तो बढ़िया; यह इसे किसी अन्य ऑब्जेक्ट की तरह ही निष्पादित करेगा। हालांकि, यदि उसके पास वह मेथड परिभाषित नहीं है, तो यह method_missing पर पहुंच जाएगा। method_missing तब अपने कंस्ट्रक्टर को दिए गए ऑब्जेक्ट पर उस मेथड को call करने का प्रयास करेगा।
एक सरल उदाहरण:
require 'simple_delegator'
class Thing
def one
'one'
end
def two
'two'
end
end
class ThingDecorator < SimpleDelegator
def two
'three!'
end
end
ThingDecorator.new(Thing.new).one #=> "one"
ThingDecorator.new(Thing.new).two #=> "three!"
यहां हमारे अपने ThingDecorator क्लास के साथ SimpleDelegator को सबक्लास करके, हम कुछ मेथड्स को ओवरराइट कर सकते हैं और दूसरों को डिफ़ॉल्ट Thing ऑब्जेक्ट पर गिरने दे सकते हैं।
ऊपर दिया गया तुच्छ उदाहरण SimpleDelegator के साथ न्याय नहीं करता, हालांकि। आप इस कोड को देख सकते हैं और बहुत अच्छी तरह से मुझसे कह सकते हैं, "क्या Thing को सबक्लास करने से मुझे वही परिणाम नहीं मिलेगा?"
हां, हां मिलेगा। लेकिन यहाँ मुख्य अंतर है: SimpleDelegator अपने कंस्ट्रक्टर में एक आर्गुमेंट के रूप में वह ऑब्जेक्ट लेता है जिसे यह डेलीगेट करेगा। इसका मतलब है हम रनटाइम पर अलग-अलग ऑब्जेक्ट्स पास कर सकते हैं।
यही वह चीज़ है जो हमें उपरोक्त समाधान 2 में prawn ऑब्जेक्ट पर कॉल्स को रीडायरेक्ट करने की अनुमति देती है। यदि हम एक सिंगल रिपोर्ट को कॉल करते हैं, तो prawn कॉल्स कंस्ट्रक्टर में बनाए गए एक नए डॉक्यूमेंट पर जाते हैं। हालांकि, ओवरव्यू रिपोर्ट इसे बदल सकती है ताकि prawn पर कॉल्स इसके डॉक्यूमेंट पर फ़ॉरवर्ड किए जाएं।
निष्कर्ष
डिपेंडेंसी इंजेक्शन शायद अधिकांश डिकॉपलिंग समस्याओं का अधिकांश समय सबसे अच्छा समाधान है।
हालांकि, सभी तकनीकों की तरह, ट्रेड-ऑफ्स हैं। मेरे मामले में, मुझे नहीं लगा कि DI द्वारा पेश किया गया ओवरहेड उन लाभों के लायक था जो इसने प्रदान किए, इसलिए मैंने एक अन्य समाधान की तलाश की।
Ruby में सभी चीजों की तरह, हमेशा एक और तरीका होता है। मैं इस समाधान के लिए अक्सर नहीं पहुंचूंगा, लेकिन यह निश्चित रूप से इन स्थितियों के लिए आपके Ruby टूलबेल्ट में एक अच्छा जोड़ है।