यह कैसे परिभाषित किया जाता है कि एक विधि को परिभाषित करने की तुलना में एक मजबूत प्रतिबद्धता को ओवरराइड किया जा सकता है जिसे एक विधि कहा जा सकता है?


36

से: http://www.artima.com/lejava/articles/designprinciples4.html

एरिक गामा: मुझे अभी भी लगता है कि यह दस साल बाद भी सच है। व्यवहार को बदलने के लिए वंशानुक्रम एक अच्छा तरीका है। लेकिन हम जानते हैं कि यह भंगुर है, क्योंकि उपवर्ग आसानी से उस संदर्भ के बारे में धारणा बना सकता है जिसमें एक विधि जिसे वह ओवरराइड करता है उसे कहा जा रहा है। बेस क्लास और उपवर्ग के बीच एक तंग युग्मन है, क्योंकि इसमें निहित संदर्भ है जिसमें उपवर्ग कोड मैं प्लग इन किया जाएगा। रचना में एक अच्छा संपत्ति है। युग्मन कम हो जाता है बस कुछ छोटी चीजों को आप कुछ बड़े में प्लग करते हैं, और बड़ी वस्तु छोटी वस्तु को वापस बुलाती है। एपीआई के दृष्टिकोण से यह परिभाषित करना कि एक विधि को ओवरराइड किया जा सकता है, यह परिभाषित करने की तुलना में एक मजबूत प्रतिबद्धता है कि एक विधि को बुलाया जा सकता है।

मुझे समझ नहीं आ रहा है कि उसका क्या मतलब है। क्या कोई इसे समझा सकता है?

जवाबों:


63

प्रतिबद्धता एक ऐसी चीज है जो आपके भविष्य के विकल्पों को कम करती है। एक विधि प्रकाशित करना तात्पर्य है कि उपयोगकर्ता इसे कॉल करेंगे, इसलिए आप संगतता को तोड़े बिना इस पद्धति को नहीं निकाल सकते। यदि आप इसे रखते हैं private, तो वे (सीधे) इसे कॉल नहीं कर सकते हैं, और आप किसी दिन समस्याओं के बिना इसे दूर कर सकते हैं। इसलिए, एक विधि प्रकाशित करना प्रकाशन नहीं करने की तुलना में एक मजबूत प्रतिबद्धता है। एक overridable विधि का प्रकाशन एक और भी मजबूत प्रतिबद्धता है। आपके उपयोगकर्ता इसे कॉल कर सकते हैं , और वे नई कक्षाएं बना सकते हैं जहां विधि वह नहीं करती है जो आपको लगता है कि यह करता है!

उदाहरण के लिए, यदि आप एक क्लीन-अप विधि प्रकाशित करते हैं, तो आप यह सुनिश्चित कर सकते हैं कि जब तक उपयोगकर्ता इस विधि को अंतिम कार्य के रूप में कॉल करना याद रखते हैं, तब तक संसाधनों का उचित रूप से निपटान किया जाता है। लेकिन अगर यह तरीका अधिक उपयोगी है, तो कोई इसे उपवर्ग में ओवरराइड कर सकता है और कॉल नहीं कर सकताsuper । नतीजतन, एक तीसरा उपयोगकर्ता उस वर्ग का उपयोग कर सकता है और संसाधन रिसाव का कारण बन सकता है, भले ही उन्हें कर्तव्यपूर्वक cleanup()अंत में बुलाया जाए ! इसका मतलब है कि अब आप अपने कोड के शब्दार्थों की गारंटी नहीं दे सकते, जो कि बहुत बुरी बात है।

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


11
यह संभवतः विरासत के खिलाफ सबसे अच्छा तर्क है जो मैंने कभी पढ़ा है। इसके खिलाफ सभी कारणों में से जो मैंने सामना किया है, मैं इन दो तर्कों के सामने कभी नहीं आया (युग्मन और ओवरराइडिंग के माध्यम से कार्यक्षमता को तोड़ना), फिर भी दोनों विरासत के खिलाफ बहुत शक्तिशाली तर्क हैं।
डेविड अरनो

5
@DavidArno मुझे नहीं लगता कि यह विरासत के खिलाफ एक तर्क है। मुझे लगता है कि यह "डिफ़ॉल्ट रूप से सब कुछ अधिक करने योग्य" के खिलाफ एक तर्क है। विरासत अपने आप में खतरनाक नहीं है, बिना सोचे समझे इसका उपयोग करना है।
svick

15
हालांकि यह एक अच्छे बिंदु की तरह लगता है, मैं वास्तव में यह नहीं देख सकता कि "एक उपयोगकर्ता अपने स्वयं के छोटी गाड़ी कोड को कैसे जोड़ सकता है" एक तर्क है। इनहेरिटेंस को सक्षम करने से उपयोगकर्ता बिना अपडाउनटैबिलिटी खोए बिना कार्यक्षमता में कमी कर सकते हैं, एक उपाय जो बग्स को रोक सकता है और ठीक कर सकता है। यदि आपके एपीआई का एक उपयोगकर्ता कोड टूट गया है तो यह एपीआई दोष नहीं है।
सेब डेब

4
आप आसानी से उस तर्क को चारों ओर मोड़ सकते हैं: पहला कोडर एक सफाई तर्क देता है, लेकिन गलतियाँ करता है और सब कुछ साफ नहीं करता है। दूसरा कोडर सफाई पद्धति को ओवरराइड करता है और एक अच्छा काम करता है, और कोडर # 3 वर्ग का उपयोग करता है और इसके पास कोई संसाधन लीक नहीं है, भले ही कोडर # 1 ने इसे गड़बड़ कर दिया हो।
Pieter B

6
@ डोभाल वास्तव में। यही कारण है कि यह एक भयावहता है कि वंशानुक्रम केवल प्रत्येक परिचयात्मक OOP पुस्तक और वर्ग के बारे में सबक नंबर एक है।
केविन क्रुमविडे

30

यदि आप एक सामान्य फ़ंक्शन प्रकाशित करते हैं, तो आप एक तरफा अनुबंध देते हैं:
यदि कॉल किया जाता है तो फ़ंक्शन क्या करता है?

यदि आप कॉलबैक प्रकाशित करते हैं, तो आप एक तरफा अनुबंध भी देते हैं:
इसे कब और कैसे कहा जाएगा?

और यदि आप एक उल्लेखनीय कार्य प्रकाशित करते हैं, तो यह दोनों एक ही बार में है, इसलिए आप एक दो-पक्षीय अनुबंध देते हैं:
इसे कब बुलाया जाएगा, और यदि यह कहा जाता है, तो इसे क्या करना चाहिए?

यहां तक ​​कि अगर आपके उपयोगकर्ता आपके एपीआई का दुरुपयोग नहीं कर रहे हैं ( अनुबंध के उनके हिस्से को तोड़कर , जिसका पता लगाने के लिए निषेधात्मक रूप से महंगा हो सकता है), आप आसानी से देख सकते हैं कि उत्तरार्द्ध को कहीं अधिक दस्तावेज़ीकरण की आवश्यकता है, और आपके द्वारा दस्तावेज़ के लिए सब कुछ एक प्रतिबद्धता है, जो सीमित है आपके आगे के विकल्प।

इस तरह के दो-पक्षीय अनुबंध पर पुनर्विचार का एक उदाहरण java.awt.Component से showऔर में hideजाना setVisible(boolean)है ।


+1। मुझे यकीन नहीं है कि अन्य जवाब क्यों स्वीकार किया गया था; यह कुछ दिलचस्प बिंदु बनाता है, लेकिन यह निश्चित रूप से इस प्रश्न का सही उत्तर नहीं है , इसमें यह निश्चित रूप से उद्धृत मार्ग का मतलब नहीं है।
बरबाद

यह सही उत्तर है, लेकिन मैं उदाहरण नहीं समझता। शो को बदलना और सेटवीविज़ (बुलियन) के साथ छिपाना कोड को तोड़ने लगता है जो वंशानुक्रम का भी उपयोग नहीं करता है। क्या मैं कुछ भूल रहा हूँ?
इग्नेशेप

3
@eigensheep: showऔर hideअभी भी मौजूद हैं, वे बस हैं @Deprecated। इसलिए परिवर्तन किसी भी कोड को नहीं तोड़ता है जो केवल उन्हें आमंत्रित करता है। लेकिन अगर आपने उन्हें ओवरराइड किया है, तो आपके ओवरराइड्स को उन ग्राहकों द्वारा नहीं बुलाया जाएगा जो नए 'सेटविजिबल' में माइग्रेट करते हैं। (मैंने कभी भी स्विंग का उपयोग नहीं किया है, इसलिए मुझे नहीं पता कि उन लोगों को ओवरराइड करना कितना आम है; लेकिन चूंकि यह बहुत समय पहले हुआ था, मैं कल्पना करता हूं कि डेडुप्लिकेटर को यह याद है कि यह उसे / उसके दर्द को थोड़ा सा याद करता है।)
रूक

12

किलियन फोथ का जवाब बेहतरीन है। मैं सिर्फ कैनोनिकल उदाहरण जोड़ना चाहूंगा कि यह एक समस्या क्यों है। एक पूर्णांक बिंदु वर्ग की कल्पना करें:

class Point2D {
    public int x;
    public int y;

    // constructor
    public Point2D(int theX, int theY) { x = theX; y = theY; }

    public int hashCode() { return x + y; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point2D) ) { return false; }

        Point2D that = (Point2D) o;

        return (x == that.x) &&
               (y == that.y);
    }
}

अब इसे 3 डी पॉइंट होने के लिए सब-क्लास करें।

class Point3D extends Point2D {
    public int z;

    // constructor
    public Point3D(int theX, int theY, int theZ) {
        super(x, y); z = theZ;
    }

    public int hashCode() { return super.hashCode() + z; }

    public boolean equals(Object o) {
        if (this == o) { return true; }
        if ( !(o instanceof Point3D) ) { return false; }

        Point3D that = (Point3D) o;

        return super.equals(that) &&
               (z == that.z);
    }
}

सुपर सरल! चलो हमारे अंक का उपयोग करें:

Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);

p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false

Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);

p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false

आप शायद सोच रहे हैं कि मैं इतना आसान उदाहरण क्यों पोस्ट कर रहा हूं। यहाँ पकड़ है:

p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!

जब हम 2D बिंदु की तुलना बराबर 3D बिंदु से करते हैं, तो हम सही हो जाते हैं, लेकिन जब हम तुलना को उल्टा करते हैं, तो हम झूठे हो जाते हैं (क्योंकि पी 2 ए विफल हो जाता है instanceof Point3D)।

निष्कर्ष

  1. यह आमतौर पर एक उपवर्ग में इस तरह से लागू करने के लिए संभव है कि यह अब-के साथ संगत नहीं है कि सुपर-क्लास इसे कैसे काम करने की उम्मीद करता है।

  2. समान रूप से एक भिन्न उपवर्ग पर समान (समान) को लागू करना असंभव है, जो कि अभिभावक वर्ग के अनुकूल है।

जब आप एक वर्ग लिखते हैं जिसे आप लोगों को उपवर्ग के लिए अनुमति देने का इरादा रखते हैं, तो प्रत्येक विधि का व्यवहार कैसे होना चाहिए, इसके लिए एक अनुबंध लिखना बहुत अच्छा विचार है । इससे भी बेहतर होगा कि यूनिट परीक्षणों का एक सेट जो लोग ओवरराइड विधियों के अपने कार्यान्वयन के खिलाफ चला सकते हैं ताकि यह साबित हो सके कि वे अनुबंध का उल्लंघन नहीं करते हैं। लगभग कोई भी ऐसा नहीं करता है क्योंकि यह बहुत अधिक काम है। लेकिन अगर आप परवाह करते हैं, तो यह करने की बात है।

एक अच्छी तरह से वर्तनी अनुबंध का एक बढ़िया उदाहरण है तुलना.equals()ऊपर बताए गए कारणों के बारे में जो कहता है, उसे अनदेखा करें । यहाँ एक उदाहरण है कि कैसे तुलनित्र चीजों .equals()को नहीं कर सकता है

टिप्पणियाँ

  1. जोश बलोच का "प्रभावी जावा" आइटम 8 इस उदाहरण का स्रोत था, लेकिन बलोच एक ColorPoint का उपयोग करता है जो तीसरे अक्ष के बजाय एक रंग जोड़ता है और चींटियों के बजाय युगल का उपयोग करता है। बलोच का जावा उदाहरण मूल रूप से ओडस्की / स्पून / वेनर्स द्वारा दोहराया गया है जिन्होंने अपना उदाहरण ऑनलाइन उपलब्ध कराया है।

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

बोनस

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


1
SortedMap और SortedSet वास्तव में equalsकैसे नक्शा और सेट इसे से परिभाषित करते हैं, हालांकि से परिभाषाओं को नहीं बदलते हैं । समानता पूरी तरह से आदेश की अनदेखी करती है, इस आशय के साथ, उदाहरण के लिए, एक ही तत्वों के साथ दो सॉर्टेडसेट्स लेकिन विभिन्न प्रकार के आदेश अभी भी बराबर की तुलना करते हैं।
user2357112

1
@ user2357112 आप सही हैं और मैंने उस उदाहरण को हटा दिया है। SortedMap.equals () मानचित्र के साथ संगत होना एक अलग मुद्दा है जिसके बारे में मैं शिकायत करने के लिए आगे बढ़ूंगा। SortedMap आमतौर पर O (log2 n) और HashMap (मानचित्र का विहित अनुमान) O (1) है। इसलिए, यदि आप वास्तव में ऑर्डर देने की परवाह करते हैं तो आप केवल एक सॉर्टेड मैप का उपयोग करेंगे । इस कारण से मेरा मानना ​​है कि क्रमबद्धता कार्यान्वयनों में समतुल्य () परीक्षण का एक महत्वपूर्ण घटक होने के लिए आदेश पर्याप्त महत्वपूर्ण है। उन्हें मानचित्र के साथ एक समान () कार्यान्वयन साझा नहीं करना चाहिए (वे जावा में AbstractMap के माध्यम से करते हैं)।
GlenPeterson

3
"वंशानुक्रम बुरा या गलत नहीं है, यह सिर्फ मुश्किल है।" मैं समझता हूं कि आप क्या कह रहे हैं, लेकिन मुश्किल चीजें आम तौर पर त्रुटियों, बगों और समस्याओं को जन्म देती हैं। आप एक अधिक विश्वसनीय तरीके से एक ही बातें (या लगभग सभी एक ही बातें) पूरा कर सकते हैं, तो अधिक मुश्किल रास्ता है बुरा।
jpmc26

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

3
Liskov के सबस्टीट्यूशन सिद्धांत का ELI5 कहता है: क्या एक वर्ग Bको एक कक्षा का बच्चा Aहोना चाहिए और क्या आपको किसी कक्षा की किसी वस्तु का Bतुरंत उपयोग करना चाहिए, आपको कक्षा की Bवस्तु को माता-पिता के लिए सक्षम करना चाहिए और बिना किसी कार्यान्वयन के विवरण के बिना डाले गए चर के एपीआई का उपयोग करना चाहिए । बच्चा। आपने तीसरी संपत्ति प्रदान करके नियम तोड़ा। zजब आप Point3Dचर को Point2Dआधार बनाते हैं , तो आपको पता ही नहीं चलता है कि जब आप चर वर्ग का पता नहीं लगाते हैं, तो आपको समन्वय करने की योजना कैसे बनानी चाहिए? अगर किसी चाइल्ड क्लास को उसके बेस में डालने से आप सार्वजनिक एपीआई तोड़ते हैं, तो आपका एब्सट्रैक्शन गलत है।
एंडी

4

इनहेरिटेंस वीकेंस एनकैप्सुलेशन

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

नीचे पीटर संग्रह ( http://norvig.com/java-iaq.html ) के सौजन्य से जावा संग्रह ढांचे से एक वास्तविक (सरलीकृत) उदाहरण दिया गया है ।

Public Class HashTable{
    ...
    Public Object put(K key, V value){
        try{
            //add object to table;
        }catch(TableFullException e){
            increaseTableSize();
            put(key,value);
        }
    }
}

तो क्या होगा अगर हम इस उपवर्ग?

/** A version of Hashtable that lets you do
 * table.put("dog", "canine");, and then have
 * table.get("dogs") return "canine". **/

public class HashtableWithPlurals extends Hashtable {

    /** Make the table map both key and key + "s" to value. **/
    public Object put(Object key, Object value) {
        super.put(key + "s", value);
        return super.put(key, value);
    }
}

हमारे पास एक बग है: कभी-कभी हम "डॉग" जोड़ते हैं और हैशटेबल को "डॉग्स" के लिए प्रवेश मिलता है। इसका कारण यह था कि हैशटेबल क्लास को डिजाइन करने वाले व्यक्ति ने उम्मीद नहीं की थी।

वंशानुक्रम का विस्तार

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

जब आप इंटरफ़ेस में नए तरीके जोड़ते हैं, तो जो भी आपकी कक्षा से विरासत में मिला है, उसे उन तरीकों को लागू करना होगा।


3

यदि किसी विधि को कहा जाता है, तो आपको केवल यह सुनिश्चित करना होगा कि यह सही ढंग से काम करे। बस। किया हुआ।

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

इसलिए मूल विधि के निर्माता को इस बारे में धारणा बनाने की आवश्यकता है कि भविष्य में कक्षा और उसके तरीकों को कैसे ओवरराइड किया जा सकता है।

हालाँकि, लेखक उद्धृत पाठ में एक अलग मुद्दे के बारे में बात कर रहा है:

लेकिन हम जानते हैं कि यह भंगुर है, क्योंकि उपवर्ग आसानी से उस संदर्भ के बारे में धारणा बना सकता है जिसमें एक विधि जिसे ओवरराइड किया जाता है उसे कहा जाता है।

विधि पर विचार करें aजिसे सामान्य रूप से विधि से कहा जा रहा है b, लेकिन विधि से कुछ दुर्लभ और गैर-स्पष्ट मामलों में c। यदि ओवरराइडिंग विधि का लेखक cविधि और उसकी अपेक्षाओं को अनदेखा करता है a, तो यह स्पष्ट है कि चीजें कैसे गलत हो सकती हैं।

इसलिए यह अधिक महत्वपूर्ण है कि aस्पष्ट रूप से और स्पष्ट रूप से परिभाषित किया गया है, अच्छी तरह से प्रलेखित, "एक काम करता है और इसे अच्छी तरह से करता है" - इससे भी अधिक अगर यह केवल एक विधि है जिसे डिजाइन किया जाना है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.