ข้ามไปยังเนื้อหา

Visitor

Visitor ให้คุณนิยาม operation ใหม่บนโครงสร้างของ object โดยไม่ต้องแก้ไข class ที่ประกอบกันเป็นโครงสร้างนั้น คุณห่อ operation ไว้ใน visitor แล้วปล่อยให้แต่ละ element รับ (accept) visitor เข้ามา

สมมติว่าคุณมีลำดับชั้นของรูปทรง — วงกลม, สี่เหลี่ยมผืนผ้า, และอื่น ๆ — และคุณต้องการ operation ใหม่ ๆ ทั่วทั้งหมดอยู่เรื่อย ๆ ได้แก่ คำนวณพื้นที่, เรนเดอร์เป็น SVG, serialize เป็น JSON, ประมาณค่าสี หากแต่ละ operation อยู่ในฐานะเมท็อดบนทุก class รูปทรง การเพิ่ม operation หนึ่งก็หมายถึงการแก้ไขทุก class และ class รูปทรงก็ค่อย ๆ สะสมความกังวลที่ไม่เกี่ยวข้องกันเข้ามา

Visitor พลิกกลับเรื่องนี้ operation ย้ายเข้าไปอยู่ใน object ของตัวเอง — visitor — โดยมีหนึ่งเมท็อดต่อ element หนึ่งชนิด แต่ละ element เปิดเผยเมท็อด accept ที่เรียกกลับไปยังเมท็อดของ visitor ที่ตรงกัน พร้อมส่งตัวเองไปด้วย การ dispatch สองขั้นนี้ (element เลือกเมท็อดของ visitor ส่วน runtime เลือกชนิดของ element) คือ double dispatch นั่นคือ code ที่ถูกรันขึ้นอยู่กับทั้ง visitor และ element การเพิ่ม operation ใหม่ตอนนี้จึงเป็นเพียงการเขียน visitor ใหม่ โดยไม่ต้องเปลี่ยนรูปทรงเลย จุดที่ต้องระวังคือภาพสะท้อนกลับด้าน — การเพิ่มรูปทรงใหม่บังคับให้ต้องเปลี่ยนทุก visitor

classDiagram
  class Shape {
    <<interface>>
    +accept(v Visitor) double
  }
  class Circle {
    +radius: double
    +accept(v Visitor) double
  }
  class Rectangle {
    +width: double
    +height: double
    +accept(v Visitor) double
  }
  class Visitor {
    <<interface>>
    +visitCircle(c Circle) double
    +visitRectangle(r Rectangle) double
  }
  class AreaVisitor {
    +visitCircle(c Circle) double
    +visitRectangle(r Rectangle) double
  }
  Shape <|.. Circle
  Shape <|.. Rectangle
  Visitor <|.. AreaVisitor
  Circle ..> Visitor : accept
  Rectangle ..> Visitor : accept
แต่ละ element รับ visitor และ dispatch ไปยังเมท็อดสำหรับชนิดของตนเอง
  • Shape (Element) — interface สำหรับสมาชิกของโครงสร้าง ประกาศเมท็อด accept ที่รับ visitor เข้ามา
  • Circle, Rectangle — concrete element แต่ละตัวเมท็อด accept จะเรียกกลับไปยังเมท็อดของ visitor ที่เจาะจงตามชนิดของตน พร้อมส่งตัวเองไปด้วย
  • Visitor — interface ที่ประกาศเมท็อด visit หนึ่งตัวต่อ element หนึ่งชนิด
  • AreaVisitor — concrete operation เมท็อด visit แต่ละตัว implement operation สำหรับ element หนึ่งชนิด

ลำดับชั้นของรูปทรงพร้อม operation คำนวณพื้นที่ที่ห่อไว้ในรูปของ visitor operation ใหม่กลายเป็น visitor ใหม่ ส่วนรูปทรงไม่เปลี่ยนแปลงเลย

interface ShapeVisitor {
visitCircle(c: Circle): number;
visitRectangle(r: Rectangle): number;
}
interface Shape {
accept(v: ShapeVisitor): number;
}
class Circle implements Shape {
constructor(readonly radius: number) {}
accept(v: ShapeVisitor): number {
return v.visitCircle(this);
}
}
class Rectangle implements Shape {
constructor(readonly width: number, readonly height: number) {}
accept(v: ShapeVisitor): number {
return v.visitRectangle(this);
}
}
class AreaVisitor implements ShapeVisitor {
visitCircle(c: Circle): number {
return Math.PI * c.radius * c.radius;
}
visitRectangle(r: Rectangle): number {
return r.width * r.height;
}
}
const shapes: Shape[] = [new Circle(1), new Rectangle(2, 3)];
const area = new AreaVisitor();
for (const s of shapes) {
console.log(s.accept(area).toFixed(2)); // 3.14, then 6.00
}
  • ข้อดี: เพิ่ม operation ใหม่บนโครงสร้างได้ด้วยการเขียน visitor ใหม่ โดยไม่ต้องเปลี่ยน class ของ element
  • ข้อดี: พฤติกรรมที่เกี่ยวข้องกันของ operation หนึ่งอยู่รวมกันใน visitor ตัวเดียว แทนที่จะถูกป้ายกระจายไปทั่วทุก element
  • ข้อเสีย: การเพิ่ม element ชนิดใหม่บังคับให้ต้องเปลี่ยนทุก visitor — ข้อแลกเปลี่ยนตรงกันข้ามพอดี ดังนั้นจงใช้ pattern นี้เฉพาะเมื่อชุดของ element นิ่งแล้วเท่านั้น
  • ข้อเสีย: visitor มักต้องเข้าถึงส่วนภายในของ element ซึ่งอาจทำให้ encapsulation อ่อนแอลง
  • ข้อเสีย: พิธีรีตองของ double dispatch (เมท็อด accept บวกกับเมท็อด visit) เยิ่นเย้อเกินไปสำหรับลำดับชั้นเล็ก ๆ
  • ต้นไม้ของ Composite คือโครงสร้างต้นแบบที่ visitor เดินผ่าน visitor เดินผ่าน composite และนำ operation ไปใช้ที่แต่ละโหนด
  • Iterator สามารถป้อน element ให้แก่ visitor แยก วิธีที่คุณเดินผ่าน ออกจาก สิ่งที่คุณทำ ที่แต่ละ element
VisitorIteratorStrategy
จุดประสงค์เพิ่ม operation ใหม่บน structure เดิมวนซ้ำ collection ด้วย interface เดียวสลับ algorithm ให้ context
เปลี่ยน structureไม่ได้ (open/closed)ไม่ควร (อาจ corrupt)ไม่เปลี่ยน
เพิ่ม node type ใหม่ต้องแก้ visitor ทุกตัวง่ายไม่เกี่ยวข้อง
ตัวอย่างAST transform, lint rule, report genfor..of, generatorpricing, compression
Visitor pattern ให้คุณเพิ่มอะไรได้โดยไม่ต้องเปลี่ยน class ของ element?
double dispatch ใน Visitor pattern คืออะไร?
ข้อเสียหลักของ Visitor คืออะไร?