किसी वर्ग को खुद की सूची के उपवर्ग के रूप में परिभाषित करने से क्या नुकसान होगा?


48

मेरी एक हालिया परियोजना में, मैंने निम्नलिखित हेडर के साथ एक वर्ग को परिभाषित किया:

public class Node extends ArrayList<Node> {
    ...
}

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

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

दूसरी ओर, मैं देखता हूं कि यह कैसे बुरा व्यवहार हो सकता है। किसी चीज़ को स्वयं की सूची के रूप में परिभाषित करने का विचार सरल या शारीरिक रूप से लागू करने योग्य नहीं है।

क्या इसका कोई मान्य कारण है कि मुझे इसके लिए अपने उपयोग पर विचार करते हुए अपने कोड में इसका उपयोग नहीं करना चाहिए ?


To यदि मुझे इसे और समझाने की आवश्यकता है, तो मुझे खुशी होगी; मैं इस प्रश्न को संक्षिप्त रखने का प्रयास कर रहा हूं।



1
@ वागट के पास सूचियों को शामिल करने के बजाय किसी वर्ग को स्वयं की सूची के रूप में कड़ाई से परिभाषित करने के साथ अधिक है, जिसमें सूचियां हैं। मुझे लगता है कि यह एक समान चीज का एक प्रकार है, लेकिन पूरी तरह से समान नहीं है। यह प्रश्न रॉबर्ट हार्वे के उत्तर की तर्ज पर किसी चीज़ को संदर्भित करता है ।
Addison Crump

16
कुछ चीज़ों से इनहेरिट किया गया, जो असामान्य नहीं है, जैसा कि C ++ में आमतौर पर इस्तेमाल किया जाने वाला CRTP मुहावरा है। कंटेनर के मामले में, महत्वपूर्ण प्रश्न यह बताना है कि आपको रचना के बजाय वंशानुक्रम का उपयोग क्यों करना है।
क्रिस्टोफ़

1
मुझे यह विचार पसंद आया। सब के बाद, एक पेड़ में प्रत्येक नोड एक (उप) पेड़ है। आमतौर पर एक नोड का ट्री-नेस एक प्रकार के दृश्य से छुपाया जाता है (शास्त्रीय नोड एक पेड़ नहीं है, लेकिन एक एकल वस्तु है) और इसके बजाय डेटा दृश्य में व्यक्त किया गया है (एकल नोड शाखाओं तक पहुंच की अनुमति देता है)। यहाँ यह दूसरा तरीका है; हम शाखा डेटा नहीं देखेंगे, यह प्रकार में है। विडंबना यह है कि, C ++ में वास्तविक ऑब्जेक्ट लेआउट संभवतः समान होगा (विरासत में डेटा युक्त करने के लिए समान रूप से लागू किया गया है), जो मुझे लगता है कि हम एक ही चीज़ को व्यक्त करने के दो तरीकों से निपट रहे हैं।
पीटर -

1
शायद मैं थोड़ा याद कर रहा हूं, लेकिन क्या आपको वास्तव में लागू करने की तुलना में विस्तार करने की आवश्यकता है ? ArrayList<Node> List<Node>
मथायस

जवाबों:


107

सच कहूँ तो मुझे यहाँ विरासत की आवश्यकता नहीं दिखती। इसका कोई मतलब नहीं है; Node एक है ArrayList का Node?

यदि यह केवल एक पुनरावर्ती डेटा संरचना है, तो आप बस कुछ ऐसा लिखेंगे:

public class Node {
    public String item;
    public List<Node> children;
}

कौन करता है मतलब; नोड में बच्चों या वंशज नोड्स की एक सूची है।


34
यह एक ही बात नहीं है। जब तक आप सूची में एक नई सूची के अलावा कुछ संग्रहीत नहीं कर रहे हैं तब तक एक सूची विज्ञापन-शिशु- सूची की एक सूची निरर्थक है । यह वही है जो सूचियों के लिए स्वतंत्र रूप से संचालित होता है। ItemItem
रॉबर्ट हार्वे

6
ओह, मैं अब देखता हूं। मैं से संपर्क किया था Node एक है ArrayList के Nodeरूप में Node 0 कई लोगों के लिए होता है Node वस्तुओं। उनका कार्य ही है, अनिवार्य रूप से, लेकिन इसके बजाय का किया जा रहा है की तरह वस्तुओं की एक सूची है, यह है की तरह वस्तुओं की एक सूची। लिखित कक्षा के लिए मूल डिजाइन यह विश्वास था कि किसी Nodeको उप- Nodeएस के सेट के रूप में व्यक्त किया जा सकता है , जो दोनों कार्यान्वयन करते हैं, लेकिन यह निश्चित रूप से कम भ्रमित नहीं है। +1
Addison Crump

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

12
@ nl-x कि एक वाक्यविन्यास छोटा है इसका मतलब यह नहीं है कि यह बेहतर है। इसके अलावा, संयोग से, इस तरह से एक पेड़ में वस्तुओं का उपयोग करना बहुत दुर्लभ है। आमतौर पर संरचना का संकलन समय पर नहीं किया जाता है, इसलिए आप निरंतर मूल्यों द्वारा ऐसे पेड़ों का पता नहीं लगाते हैं। गहराई भी तय नहीं है, इसलिए आप उस सिंटैक्स का उपयोग करके सभी मूल्यों पर पुनरावृति नहीं कर सकते। जब आप एक पारंपरिक ट्री ट्रैवर्सल समस्या का उपयोग करने के लिए कोड को अनुकूलित करते हैं, तो आप Childrenकेवल एक या दो बार लिखना समाप्त करते हैं , और आमतौर पर कोड स्पष्टता के लिए ऐसे मामलों में इसे लिखना बेहतर होता है।
सर्व


57

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

संक्षिप्त SOLID अच्छी वस्तु उन्मुख डिजाइन के लिए कुछ उत्तराधिकार प्रदान करता है। यहाँ, I nterface Segregation Principle (ISP) और L iskov एवोल्यूशन प्राइसिंग (LSP) कुछ कहना है।

आईएसपी हमें अपने इंटरफेस को यथासंभव छोटा रखने के लिए कहता है। लेकिन विरासत में मिलने से ArrayList, आपको कई, कई तरीके मिलते हैं। यह करने के लिए सार्थक है get(), remove(), set()(की जगह), या add()एक विशेष सूचकांक में (डालने) एक बच्चे नोड? क्या यह ensureCapacity()अंतर्निहित सूची के लिए समझदार है ? यह sort()एक नोड के लिए क्या मतलब है ? क्या आपकी कक्षा के उपयोगकर्ता वास्तव में ए पाने वाले हैं subList()? चूँकि आप उन तरीकों को नहीं छिपा सकते जो आप नहीं चाहते हैं, एकमात्र समाधान ArrayList को एक सदस्य चर के रूप में रखना है, और उन सभी तरीकों को अग्रेषित करना है जो आप वास्तव में चाहते हैं:

private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }

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

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

लेकिन एलएसपी बहुत अधिक गहराई से चलता है: हमें उन सभी चीजों के साथ संगतता बनाए रखना होगा जो सभी मूल वर्गों और इंटरफेस के प्रलेखन द्वारा वादा किया गया है। में उनके जवाब , लिन ऐसे ही एक मामले में जहां पाया गया है Listइंटरफेस (आप के माध्यम से विरासत में मिला है जो ArrayList) की गारंटी देता है कि कैसे equals()और hashCode()तरीके काम करने वाले हैं। के लिए hashCode()आप भी एक विशेष एल्गोरिथ्म है कि वास्तव में लागू किया जाना चाहिए दिया जाता है। मान लेते हैं कि आपने यह लिखा है Node:

public class Node extends ArrayList<Node> {
  public final int value;

  public Node(int value, Node... children) {
    this.value = Value;
    for (Node child : children)
      add(child);
  }

  ...

}

इसके लिए आवश्यक है कि इसमें valueकोई योगदान नहीं हो सकता है hashCode()और यह प्रभावित नहीं कर सकता है equals()Listइंटरफ़ेस - जो आप इसे से इनहेरिट से सम्मानित करने के लिए वादा करता हूँ - की आवश्यकता है new Node(0).equals(new Node(123))सच।


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

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

ऐसा ही एक उदाहरण कर्मचारी-बॉस मॉडलिंग समस्या है। हमारे पास कई Personनाम हैं, प्रत्येक का नाम और पता है। एक Employeeहै एक Personऔर एक है Boss। ए Bossभी है Person। तो क्या मुझे एक Personवर्ग बनाना चाहिए जो विरासत में मिला है Bossऔर Employee? अब मुझे एक समस्या है: बॉस भी एक कर्मचारी है और एक और श्रेष्ठ है। तो ऐसा लगता है जैसे Bossविस्तार करना चाहिए Employee। लेकिन एक CompanyOwnerहै, Bossलेकिन एक नहीं है Employee? किसी भी तरह का वंशानुक्रम ग्राफ किसी तरह यहाँ टूट जाएगा।

OOP पदानुक्रम, वंशानुक्रम और मौजूदा कक्षाओं के फिर से उपयोग के बारे में नहीं है, यह व्यवहार को सामान्य बनाने के बारे में है । यही कारण है कि है - OOP के बारे में "और मुझे परवाह नहीं है कैसे। मैं वस्तुओं का एक समूह है और उन्हें एक विशेष बात यह है कि क्या करना चाहते हैं" है इंटरफेस के लिए कर रहे हैं। यदि आप Iterableइंटरफ़ेस को अपने लिए लागू करते हैं Nodeक्योंकि आप इसे चलना चाहते हैं, तो यह पूरी तरह से ठीक है। यदि आप Collectionइंटरफ़ेस लागू करते हैं क्योंकि आप चाइल्ड नोड्स आदि जोड़ना / हटाना चाहते हैं, तो यह ठीक है। लेकिन एक अन्य वर्ग से विरासत में मिला क्योंकि ऐसा होता है कि आपको वह सब देना होगा, या कम से कम तब तक नहीं जब तक कि आपने इसे ऊपर बताए अनुसार सावधानीपूर्वक न सोचा हो।


10
मैंने आपके पिछले 4 पैराग्राफ को मुद्रित किया है, उन्हें OOP और इनहेरिटेंस के रूप में शीर्षक दिया गया है और उन्हें काम पर दीवार पर खड़ा किया है। मेरे कुछ सहयोगियों को यह देखने की जरूरत है कि जैसा कि मैं जानता हूं कि वे आपके द्वारा डाले गए तरीके की सराहना करेंगे :) धन्यवाद।
YSC

17

खुद के रूप में एक कंटेनर का विस्तार करना आमतौर पर खराब अभ्यास माना जाता है। वहाँ सिर्फ एक होने के बजाय एक कंटेनर का विस्तार करने के लिए वास्तव में बहुत कम कारण है। अपने आप को एक कंटेनर का विस्तार करना इसे अतिरिक्त अजीब बनाता है।


14

जो कुछ कहा गया है उसे जोड़ते हुए, इस तरह की संरचनाओं से बचने के लिए कुछ हद तक जावा-विशिष्ट कारण है।

equalsसूचियों के लिए विधि के अनुबंध के लिए एक सूची को किसी अन्य वस्तु के बराबर माना जाना चाहिए

यदि और केवल यदि निर्दिष्ट वस्तु भी एक सूची है, तो दोनों सूचियों का आकार समान है, और दो सूचियों में सभी संगत तत्व समान हैं

स्रोत: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)

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


1
यह मेरे कोड को बुरी प्रथा से अलग करने का एक उत्कृष्ट कारण है। : पी
Addison Crump

3

स्मृति के बारे में:

मैं कहूंगा कि यह पूर्णतावाद का मामला है। डिफ़ॉल्ट-निर्माता ArrayListइस तरह दिखता है:

public ArrayList(int initialCapacity) {
     super();

     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);

     this.elementData = new Object[initialCapacity];
 }

public ArrayList() {
     this(10);
}

स्रोत । इस कंस्ट्रक्टर का उपयोग Oracle-JDK में भी किया जाता है।

अब अपने कोड के साथ एक एकल-लिंक की गई सूची बनाने पर विचार करें। आपने बस अपनी स्मृति-खपत को कारक 10x (सफलतापूर्वक मामूली अधिक होने के लिए) से फूला है। पेड़ों के लिए यह आसानी से खराब हो सकता है और साथ ही पेड़ की संरचना पर कोई विशेष आवश्यकता के बिना भी। LinkedListवैकल्पिक रूप से या अन्य वर्ग का उपयोग करके इस समस्या को हल करना चाहिए।

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

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

"बुरी प्रथा" के बारे में:

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


1
क्या तुलना में 10 गुना अधिक है ? अगर मेरे पास एक उदाहरण क्षेत्र के रूप में एक डिफ़ॉल्ट-निर्मित एरेलेलिस्ट है (इसके बजाय इनहेरिट करने के बजाय), तो मैं वास्तव में थोड़ी अधिक मेमोरी का उपयोग कर रहा हूं क्योंकि मुझे अपनी ऑब्जेक्ट में एक अतिरिक्त पॉइंटर-टू-एरियर लीलस्ट स्टोर करने की आवश्यकता है। यहां तक ​​कि अगर हम सेब की तुलना संतरे से कर रहे हैं और किसी भी ArrayList का उपयोग न करने के लिए ArrayList से विरासत की तुलना कर रहे हैं, तो ध्यान दें कि जावा में हम हमेशा पॉइंटर्स से ऑब्जेक्ट्स के साथ व्यवहार करते हैं, कभी भी C ++ के रूप में मूल्य द्वारा ऑब्जेक्ट नहीं करेंगे। new Object[0]कुल 4 शब्दों का उपयोग करते हुए , new Object[10]केवल 14 शब्दों का उपयोग करेगा।
आमोन

@amon मैंने इसे स्पष्ट रूप से नहीं कहा है, इसके लिए sry करें। हालांकि संदर्भ यह देखने के लिए पर्याप्त होना चाहिए कि मैं एक लिंक्डलिस्ट से तुलना कर रहा हूं। और इसे संदर्भ कहा जाता है, सूचक btw नहीं। और स्मृति के साथ बिंदु के बारे में नहीं था कि वर्ग का उपयोग वंशानुक्रम के माध्यम से किया जाता है या नहीं, लेकिन बस तथ्य यह है कि (लगभग) अप्रयुक्त ArrayListएस काफी स्मृति का इंतजार कर रहे हैं।
पॉल

-2

यह संयुक्त पैटर्न के शब्दार्थ जैसा दिखता है । इसका उपयोग पुनरावर्ती डेटास्ट्रक्चर को मॉडल करने के लिए किया जाता है।


13
मैं इसे कम नहीं करूंगा, लेकिन यही कारण है कि मुझे वास्तव में GoF किताब से नफरत है। यह एक ब्लीडिन का पेड़ है! पेड़ का वर्णन करने के लिए हमें "मिश्रित पैटर्न" शब्द का आविष्कार करने की आवश्यकता क्यों थी?
डेविड अरनो

3
@DavidArno मेरा मानना ​​है कि यह संपूर्ण बहुरूपी-नोड पहलू है जो इसे "समग्र पैटर्न" के रूप में परिभाषित करता है, एक पेड़ डेटा संरचना का उपयोग इसका एक हिस्सा है।
JAB

2
@RobertHarvey, मैं असहमत हूं (आपकी टिप्पणियों के साथ आपके उत्तर और यहां दोनों)। public class Node extends ArrayList<Node> { public string Item; }आपके उत्तर का वंशानुक्रम संस्करण है। तुम्हारा होना सरल है, लेकिन दोनों कुछ हासिल करते हैं: वे गैर-बाइनरी पेड़ों को परिभाषित करते हैं।
डेविड अरनो

2
@ दाविदनो: मैंने कभी नहीं कहा कि यह काम नहीं करेगा, मैंने सिर्फ इतना कहा कि यह आदर्श नहीं है।
रॉबर्ट हार्वे

1
@DavidArno यहां भावनाओं और स्वाद के बारे में नहीं है, लेकिन यह तथ्यों के बारे में है। एक पेड़ नोड्स से बना है और प्रत्येक नोड में संभावित बच्चे हैं। एक दिया गया नोड केवल एक निश्चित समय में एक पत्ती का नोड होता है। एक समग्र में, एक पत्ती नोड निर्माण द्वारा पत्ती है, और इसकी प्रकृति नहीं बदलेगी।
क्रिस्टोफ
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.