ऑब्जेक्ट ओरिएंटेड प्रोग्रामिंग . में , एक वस्तु अक्सर कार्य करने के लिए दूसरी वस्तु पर निर्भर करती है।
उदाहरण के लिए, अगर मैं वित्त रिपोर्ट चलाने के लिए एक साधारण वर्ग बनाता हूं:
class FinanceReport
def net_income
FinanceApi.gross_income - FinanceApi.total_costs
end
end
हम कह सकते हैं कि FinanceReport
निर्भर करता है FinanceApi
, जिसका उपयोग वह बाहरी भुगतान संसाधक से जानकारी प्राप्त करने के लिए करता है।
लेकिन क्या होगा अगर हम किसी बिंदु पर एक अलग एपीआई हिट करना चाहते हैं? या, अधिक संभावना है, क्या होगा यदि हम बाहरी संसाधनों को प्रभावित किए बिना इस वर्ग का परीक्षण करना चाहते हैं? सबसे आम उत्तर डिपेंडेंसी इंजेक्शन का उपयोग करना है।
निर्भरता इंजेक्शन के साथ, हम स्पष्ट रूप से FinanceApi
. का उल्लेख नहीं करते हैं FinanceReport
. के अंदर . इसके बजाय, हम इसे एक तर्क के रूप में पास करते हैं। हम इंजेक्ट यह।
निर्भरता इंजेक्शन का उपयोग करके, हमारी कक्षा बन जाती है:
class FinanceReport
def net_income(financials)
financials.gross_income - financials.total_costs
end
end
अब हमारी कक्षा को यह जानकारी नहीं है कि FinanceApi
वस्तु भी मौजूद है! हम किसी भी वस्तु को पास कर सकते हैं इसके लिए जब तक यह gross_income
. को लागू करता है और total_costs
।
इसके कई लाभ हैं:
- हमारा कोड अब कम "युग्मित" से
FinanceApi
. हो गया है । - हमें
FinanceApi
. का उपयोग करने के लिए बाध्य किया गया है एक सार्वजनिक इंटरफ़ेस के माध्यम से। - अब हम अपने परीक्षणों में एक नकली या स्टब ऑब्जेक्ट में पास कर सकते हैं ताकि हमें वास्तविक एपीआई को हिट न करना पड़े।
अधिकांश डेवलपर निर्भरता इंजेक्शन . पर विचार करते हैं सामान्य तौर पर एक अच्छी बात बनने के लिए (मैं भी!) हालांकि, जैसा कि सभी तकनीकों के साथ होता है, इसमें समझौता होता है।
हमारा कोड अब थोड़ा अधिक अपारदर्शी है। जब हमने स्पष्ट रूप से FinanceApi
. का उपयोग किया था , यह स्पष्ट था कि हमारे मूल्य कहां से आ रहे थे। डिपेंडेंसी इंजेक्शन को शामिल करने वाले कोड में यह बिल्कुल स्पष्ट नहीं है।
अगर कॉल्स अन्यथा self
. पर चली जातीं , तो हमने कोड को और अधिक क्रियात्मक बना दिया है। ऑब्जेक्ट-ओरिएंटेड "किसी ऑब्जेक्ट को एक संदेश भेजें और इसे कार्य करने दें" प्रतिमान का उपयोग करने के बजाय, हम खुद को एक अधिक कार्यात्मक "इनपुट -> आउटपुट" प्रतिमान की ओर बढ़ते हुए पाते हैं।
यह आखिरी मामला है (कॉल को पुनर्निर्देशित करना जो self
. पर जाएगा ) जिसे मैं आज देखना चाहता हूं। मैं इन स्थितियों के लिए निर्भरता इंजेक्शन के लिए एक संभावित विकल्प प्रस्तुत करना चाहता हूं:आधार वर्ग को गतिशील रूप से बदलें (थोड़े)।
समस्या को हल करना है
आइए एक पल का बैक अप लें और उस समस्या से शुरू करें जिसने मुझे इस पथ से शुरू करने के लिए प्रेरित किया:पीडीएफ रिपोर्ट।
मेरे मुवक्किल ने विभिन्न प्रिंट करने योग्य पीडीएफ रिपोर्ट तैयार करने की क्षमता का अनुरोध किया - एक रिपोर्ट एक खाते के लिए सभी खर्चों को सूचीबद्ध करती है, दूसरी लिस्टिंग राजस्व, दूसरी भविष्य के वर्षों के लिए पूर्वानुमानित लाभ, आदि।
हम आदरणीय prawn
का उपयोग कर रहे हैं मणि इन पीडीएफ़ को बनाने के लिए, प्रत्येक रिपोर्ट की अपनी रूबी ऑब्जेक्ट होने के साथ Prawn::Document
से उप-वर्गीकृत किया गया है ।
कुछ इस तरह:
class CostReport < Prawn::Document
def initialize(...)
...
end
def render
text "Cost Report"
move_down 20
...
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
. के साथ मिलकर एपीआई, इसलिए एक अलग पीडीएफ लाइब्रेरी में बदलने के लिए लगभग निश्चित रूप से कोड के संपूर्ण पुनर्लेखन की आवश्यकता होगी।
परीक्षण भी यहां एक बड़ी चिंता नहीं है, क्योंकि हमारे मामले में स्वचालित परीक्षणों के साथ उत्पन्न पीडीएफ रिपोर्ट का परीक्षण करना सार्थक होने के लिए बहुत बोझिल है।
तो निर्भरता इंजेक्शन हमें वह व्यवहार देता है जो हम चाहते हैं लेकिन हमारे लिए न्यूनतम लाभ के साथ अतिरिक्त ओवरहेड भी पेश करते हैं। आइए एक और विकल्प देखें।
समाधान 2:प्रतिनिधिमंडल
रूबी की मानक लाइब्रेरी हमें 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
, आदि) को अब OverviewReport
. पर अग्रेषित किया जा रहा है SimpleDelegator
. के माध्यम से हमने बनाया।
व्यवहार के संदर्भ में, हमने अनिवार्य रूप से इसे SalesReport
. जैसा बना दिया है अब OverviewReport
. का उपवर्ग है . हमारे मामले में, इसका मतलब है कि prawn
के लिए सभी कॉल्स का एपीआई अब SalesReport -> OverviewReport -> Prawn::Document
पर जाएं ।
SimpleDelegator कैसे काम करता है
जिस तरह से SimpleDelegator
हुड के तहत काम मूल रूप से रूबी के 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!"
SimpleDelegator
. को उपवर्गित करके हमारे अपने ThingDecorator
. के साथ क्लास यहाँ, हम कुछ विधियों को अधिलेखित कर सकते हैं और दूसरों को डिफ़ॉल्ट Thing
. के माध्यम से जाने दे सकते हैं वस्तु।
ऊपर दिया गया तुच्छ उदाहरण वास्तव में SimpleDelegator
नहीं करता है न्याय, यद्यपि। आप इस कोड को देख सकते हैं और मुझसे बहुत अच्छी तरह कह सकते हैं, "Thing
उपवर्ग नहीं करता है" मुझे वही परिणाम दो?”
हाँ, हाँ करता है। लेकिन यहाँ मुख्य अंतर है:SimpleDelegator
वह वस्तु लेता है जिसे वह अपने निर्माता में एक तर्क के रूप में सौंपेगा। इसका मतलब है कि हम रनटाइम पर अलग-अलग ऑब्जेक्ट में पास कर सकते हैं ।
यह वही है जो कॉल को prawn
पर पुनर्निर्देशित करने की अनुमति देता है ऊपर समाधान 2 में वस्तु। अगर हम एक ही रिपोर्ट कहते हैं, prawn
कॉल कंस्ट्रक्टर में बनाए गए एक नए दस्तावेज़ पर जाते हैं। हालांकि, ओवरव्यू रिपोर्ट इसे बदल सकती है ताकि prawn
को कॉल किया जा सके इसकी . को अग्रेषित किए जाते हैं दस्तावेज़।
निष्कर्ष
निर्भरता इंजेक्शन शायद अधिकांश . के लिए सबसे अच्छा समाधान है डिकूपिंग समस्याएं अधिकांश उस समय के।
हालांकि, सभी तकनीकों के साथ, ट्रेड-ऑफ़ हैं। मेरे मामले में, मुझे नहीं लगता था कि DI द्वारा पेश किया गया ओवरहेड इसके द्वारा प्रदान किए गए लाभों के लायक था, इसलिए मैंने दूसरा समाधान खोजा।
रूबी में सभी चीजों की तरह, हमेशा एक और तरीका होता है . मैं इस समाधान के लिए अक्सर नहीं पहुँचता, लेकिन इन स्थितियों के लिए यह निश्चित रूप से आपके रूबी टूलबेल्ट के लिए एक अच्छा अतिरिक्त है।