सी-लाइक लैंग्वेज '(C #, Java, आदि) शब्दार्थ के कारण विज़िटर पैटर्न visit
/ accept
कंस्ट्रक्शन एक आवश्यक बुराई है। विज़िटर पैटर्न का लक्ष्य आपकी कॉल को रूट करने के लिए दोहरे-प्रेषण का उपयोग करना है जैसा कि आप कोड को पढ़ने से उम्मीद करेंगे।
आम तौर पर जब विज़िटर पैटर्न का उपयोग किया जाता है, तो एक ऑब्जेक्ट पदानुक्रम शामिल होता है, जहां सभी नोड्स आधार Node
प्रकार से प्राप्त होते हैं , जैसा कि इसके बाद संदर्भित किया जाता है Node
। सहज रूप से, हम इसे इस तरह लिखेंगे:
Node root = GetTreeRoot();
new MyVisitor().visit(root);
यहाँ समस्या है। यदि हमारी MyVisitor
कक्षा को निम्नलिखित की तरह परिभाषित किया गया था:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
यदि, रनटाइम पर, वास्तविक प्रकार की परवाह किए बिना root
, हमारी कॉल ओवरलोड में जाएगीvisit(Node node)
। यह सभी प्रकार के घोषित चर के लिए सही होगा Node
। ऐसा क्यों है? क्योंकि जावा और अन्य सी-जैसी भाषाएं केवल स्थैतिक प्रकार , या उस प्रकार को मानती हैं , जिसे चर के रूप में घोषित किया जाता है, जो तय करते समय कॉल करने के लिए अधिभार होता है। जावा, प्रत्येक विधि कॉल के लिए, रनटाइम पर, पूछने के लिए अतिरिक्त कदम नहीं उठाता है, "ठीक है, गतिशील प्रकार क्या है root
, मैं देख रहा हूं। यह एक है TrainNode
। आइए देखें कि क्या कोई तरीका है MyVisitor
जिसमें प्रकार के एक पैरामीटर को स्वीकार करता है।TrainNode
... "। कंपाइलर, संकलन-समय पर, निर्धारित करता है कि कौन सी विधि है जिसे कहा जाएगा। (यदि जावा ने वास्तव में तर्कों के गतिशील प्रकारों का निरीक्षण किया, तो प्रदर्शन बहुत भयानक होगा।"
जावा हमें एक टूल देता है जो रनटाइम (यानी डायनामिक) प्रकार के ऑब्जेक्ट को ध्यान में रखता है जब एक विधि को बुलाया जाता है - वर्चुअल मेथड प्रेषण । जब हम एक आभासी विधि कहते हैं, तो कॉल वास्तव में मेमोरी में एक टेबल पर जाती है जिसमें फ़ंक्शन पॉइंटर्स होते हैं। प्रत्येक प्रकार की एक तालिका है। यदि किसी वर्ग द्वारा किसी विशेष विधि को ओवरराइड किया जाता है, तो उस क्लास की फंक्शन टेबल प्रविष्टि में ओवरराइड फ़ंक्शन का पता होगा। यदि कक्षा एक विधि को ओवरराइड नहीं करती है, तो इसमें बेस क्लास के कार्यान्वयन के लिए एक पॉइंटर होगा। यह अभी भी एक प्रदर्शन ओवरहेड को सम्मिलित करता है (प्रत्येक विधि कॉल मूल रूप से दो बिंदुओं को डीफ़र करना होगा: एक प्रकार की फ़ंक्शन तालिका और स्वयं फ़ंक्शन का एक और इंगित करता है), लेकिन यह अभी भी पैरामीटर प्रकारों का निरीक्षण करने की तुलना में तेज़ है।
विज़िटर पैटर्न का लक्ष्य डबल-प्रेषण को पूरा करना है - न केवल कॉल लक्ष्य का प्रकार माना जाता है ( MyVisitor
, आभासी विधियों के माध्यम से), लेकिन यह भी पैरामीटर का प्रकार ( Node
हम किस प्रकार देख रहे हैं)? आगंतुक पैटर्न हमें यह करने की अनुमति देता हैvisit
/ accept
संयोजन ।
इसे हमारी लाइन बदलकर:
root.accept(new MyVisitor());
हम जो चाहते हैं वह प्राप्त कर सकते हैं: आभासी विधि प्रेषण के माध्यम से, हम सही स्वीकार () कॉल को उपवर्ग द्वारा लागू किए गए तरीके से दर्ज करते हैं - हमारे उदाहरण में TrainElement
, हम इसका TrainElement
कार्यान्वयन करेंगे accept()
:
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
क्या के दायरे के अंदर इस बिंदु पर संकलक पता है, TrainNode
की accept
? यह जानता है कि स्थिर प्रकार this
एक हैTrainNode
। यह जानकारी का एक महत्वपूर्ण अतिरिक्त टुकड़ा है कि संकलक को हमारे कॉलर के दायरे के बारे में पता नहीं था: वहां, इसके बारे root
में सभी जानते थे कि यह एक था Node
। अब संकलक जानता है कि this
( root
) सिर्फ एक नहीं है Node
, लेकिन यह वास्तव में एक है TrainNode
। परिणाम में, एक पंक्ति के अंदर पाया accept()
:v.visit(this)
अर्थ पूरी तरह से कुछ और है। संकलक अब एक अधिभार के लिए देखो की होगी visit()
कि एक लेता है TrainNode
। यदि यह एक नहीं मिल रहा है, तो यह कॉल को एक अधिभार के लिए संकलित करेगा जो एक लेता हैNode
। यदि न तो मौजूद है, तो आपको एक संकलन त्रुटि मिलेगी (जब तक कि आपके पास एक अधिभार नहीं होता है object
)। निष्पादन इस प्रकार दर्ज करेगा जो हमने सभी के साथ किया था: MyVisitor
का कार्यान्वयन visit(TrainNode e)
। किसी भी जाति की आवश्यकता नहीं थी, और, सबसे महत्वपूर्ण बात, कोई प्रतिबिंब की आवश्यकता नहीं थी। इस प्रकार, इस तंत्र का ओवरहेड कम है: इसमें केवल सूचक संदर्भ शामिल हैं और कुछ नहीं।
आप अपने प्रश्न में सही हैं - हम कलाकारों का उपयोग कर सकते हैं और सही व्यवहार प्राप्त कर सकते हैं। हालाँकि, अक्सर, हम यह भी नहीं जानते कि नोड किस प्रकार का है। निम्नलिखित पदानुक्रम का मामला लें:
abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }
और हम एक सरल संकलक लिख रहे थे जो एक स्रोत फ़ाइल को पार्स करता है और एक वस्तु पदानुक्रम का उत्पादन करता है जो ऊपर विनिर्देश के अनुरूप है। यदि हम एक आगंतुक के रूप में लागू पदानुक्रम के लिए दुभाषिया लिख रहे थे:
class Interpreter implements IVisitor<int> {
int visit(AdditionNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left + right;
}
int visit(MultiplicationNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left * right;
}
int visit(LiteralNode n) {
return n.value;
}
}
कास्टिंग हमें बहुत दूर नहीं होगा, क्योंकि हम के प्रकार पता नहीं है left
या right
में visit()
तरीकों। हमारे पार्सर सबसे अधिक संभावना सिर्फ एक प्रकार की वस्तु लौटाएंगेNode
जो पदानुक्रम की जड़ के रूप में अच्छी तरह से इंगित करता है, इसलिए हम उस सुरक्षित रूप से भी नहीं डाल सकते हैं। तो हमारे सरल दुभाषिया जैसे दिख सकते हैं:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
विज़िटर पैटर्न हमें कुछ बहुत शक्तिशाली करने की अनुमति देता है: एक वस्तु पदानुक्रम दिया जाता है, यह हमें मॉड्यूलर संचालन बनाने की अनुमति देता है जो पदानुक्रम के वर्ग में ही कोड डालने की आवश्यकता के बिना पदानुक्रम पर काम करता है। विज़िटर पैटर्न व्यापक रूप से उपयोग किया जाता है, उदाहरण के लिए, संकलक निर्माण में। एक विशेष कार्यक्रम के वाक्यविन्यास वृक्ष को देखते हुए, कई आगंतुक लिखे जाते हैं जो उस पेड़ पर काम करते हैं: प्रकार की जाँच, अनुकूलन, मशीन कोड उत्सर्जन सभी को आमतौर पर विभिन्न आगंतुकों के रूप में लागू किया जाता है। ऑप्टिमाइज़ेशन विज़िटर के मामले में, यह इनपुट ट्री को देखते हुए एक नए सिंटैक्स ट्री को भी आउटपुट कर सकता है।
इसकी कमियां हैं, बेशक: यदि हम पदानुक्रम में एक नया प्रकार जोड़ते हैं, तो हमें visit()
उस नए प्रकार के लिए IVisitor
इंटरफ़ेस में एक विधि जोड़ने की भी आवश्यकता है , और हमारे सभी आगंतुकों में स्टब (या पूर्ण) कार्यान्वयन बनाएं। accept()
ऊपर बताए गए कारणों के लिए हमें भी विधि जोड़ने की आवश्यकता है । यदि प्रदर्शन का यह मतलब नहीं है कि आपके लिए बहुत कुछ है, तो आगंतुकों की आवश्यकता के बिना लिखने के लिए समाधान हैं accept()
, लेकिन वे सामान्य रूप से प्रतिबिंब को शामिल करते हैं और इस प्रकार काफी बड़े ओवरहेड को उकसा सकते हैं।