รูปแบบผู้เยี่ยมชมvisit/acceptโครงสร้างเป็นสิ่งชั่วร้ายที่จำเป็นเนื่องจากความหมายของภาษาซี (C #, Java, ฯลฯ ) เป้าหมายของรูปแบบผู้เยี่ยมชมคือการใช้การจัดส่งสองครั้งเพื่อกำหนดเส้นทางการโทรของคุณตามที่คุณคาดหวังจากการอ่านรหัส
โดยปกติเมื่อรูปแบบที่ผู้เข้าชมจะใช้ลำดับชั้นของวัตถุที่มีส่วนเกี่ยวข้องที่โหนดทั้งหมดจะได้มาจากฐานชนิดเรียกว่าต่อจากนี้ไปเป็น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นี้จะเป็นจริงสำหรับตัวแปรทั้งหมดประกาศประเภท ทำไมถึงเป็นแบบนี้? เนื่องจาก Java และภาษาอื่น ๆ ที่คล้าย C จะพิจารณาเฉพาะประเภทคงที่หรือประเภทที่ตัวแปรถูกประกาศเป็นของพารามิเตอร์เมื่อตัดสินใจว่าจะเรียกโอเวอร์โหลดใด Java ไม่ได้ใช้ขั้นตอนพิเศษในการถามสำหรับการเรียกทุกเมธอดในรันไทม์ว่า "โอเคประเภทไดนามิกrootคืออะไรโอ้ฉันเห็นมันเป็นTrainNodeลองดูว่ามีวิธีใดบ้างMyVisitorที่ยอมรับพารามิเตอร์ประเภทTrainNode... ". ในเวลาคอมไพล์กำหนดว่าจะเรียกใช้เมธอดใด (ถ้า Java ตรวจสอบชนิดไดนามิกของอาร์กิวเมนต์จริงๆประสิทธิภาพจะแย่มาก)
Java ให้เครื่องมือหนึ่งแก่เราในการพิจารณาประเภทรันไทม์ (เช่นไดนามิก) ของอ็อบเจ็กต์เมื่อมีการเรียกเมธอด - virtual methodวิธีการจัดส่งเสมือนเมื่อเราเรียกวิธีการเสมือนจริงการเรียกจะไปที่ตารางในหน่วยความจำที่ประกอบด้วยตัวชี้ฟังก์ชัน แต่ละประเภทมีตาราง ถ้าเมธอดเฉพาะถูกแทนที่โดยคลาสรายการตารางฟังก์ชันของคลาสนั้นจะมีแอดเดรสของฟังก์ชันที่ถูกแทนที่ หากคลาสไม่ได้แทนที่เมธอดคลาสนั้นจะมีตัวชี้ไปที่การใช้งานคลาสพื้นฐาน สิ่งนี้ยังคงมีค่าใช้จ่ายด้านประสิทธิภาพ (โดยทั่วไปการเรียกแต่ละวิธีจะเป็นการยกเลิกการอ้างอิงสองพอยน์เตอร์: อันหนึ่งชี้ไปที่ตารางฟังก์ชันของประเภทและอีกฟังก์ชันหนึ่งเอง) แต่ก็ยังเร็วกว่าการตรวจสอบประเภทพารามิเตอร์
เป้าหมายของรูปแบบผู้เยี่ยมชมคือการบรรลุการจัดส่งแบบสองครั้ง - ไม่เพียง แต่พิจารณาประเภทของเป้าหมายการโทร ( MyVisitorผ่านวิธีการเสมือน) แต่ยังรวมถึงประเภทของพารามิเตอร์ด้วย ( Nodeเรากำลังดูประเภทใด) รูปแบบผู้เยี่ยมชมช่วยให้เราสามารถทำได้โดยใช้visit / acceptcombination
โดยเปลี่ยนบรรทัดของเราเป็นสิ่งนี้:
root.accept(new MyVisitor());
เราจะได้รับสิ่งที่ต้องการ: ผ่านวิธีการจัดส่งเสมือนเราป้อนการเรียกยอมรับ () ที่ถูกต้องตามที่คลาสย่อยดำเนินการ - ในตัวอย่างของTrainElementเราเราจะเข้าสู่TrainElementการใช้งานaccept():
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
อะไรคอมไพเลอร์รู้ที่จุดนี้อยู่ภายในขอบเขตของTrainNode's accept? มันรู้ว่าประเภทคงที่ของthisคือTrainNode . นี่เป็นข้อมูลเพิ่มเติมที่สำคัญที่คอมไพเลอร์ไม่ทราบในขอบเขตของผู้โทรของเรา: ที่นั่นทั้งหมดที่รู้rootก็คือมันเป็นไฟล์Node. ตอนนี้คอมไพเลอร์รู้แล้วว่าthis( root) ไม่ใช่แค่ a Nodeแต่จริงๆแล้วมันคือไฟล์TrainNode. ดังนั้นบรรทัดเดียวที่พบภายในaccept(): v.visit(this)หมายถึงอย่างอื่นทั้งหมด ตอนนี้คอมไพเลอร์จะมองหาการโอเวอร์โหลดvisit()ที่ใช้เวลา a TrainNode. หากไม่พบมันจะรวบรวมการโทรไปยังโอเวอร์โหลดที่ใช้เวลา aNode :หากไม่มีอยู่คุณจะได้รับข้อผิดพลาดในการคอมไพล์ (เว้นแต่คุณจะมีโอเวอร์โหลดobject) การดำเนินการจะเข้าสู่สิ่งที่เราตั้งใจไว้ตลอดมา: MyVisitorการนำไปใช้visit(TrainNode e). ไม่จำเป็นต้องมีการร่ายและที่สำคัญที่สุดคือไม่จำเป็นต้องมีการสะท้อนกลับ ดังนั้นค่าใช้จ่ายของกลไกนี้จึงค่อนข้างต่ำ: ประกอบด้วยการอ้างอิงตัวชี้เท่านั้นและไม่มีอะไรอื่น
ตอบคำถามของคุณถูกต้อง - เราสามารถใช้นักแสดงและปรับพฤติกรรมที่ถูกต้องได้ อย่างไรก็ตามบ่อยครั้งเราไม่รู้ด้วยซ้ำว่า Node คืออะไร ใช้กรณีของลำดับชั้นต่อไปนี้:
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()แต่โดยปกติจะเกี่ยวข้องกับการไตร่ตรองจึงอาจมีค่าใช้จ่ายค่อนข้างมาก