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 - 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}from __future__ import annotationsimport mathfrom abc import ABC, abstractmethod
class ShapeVisitor(ABC): @abstractmethod def visit_circle(self, c: "Circle") -> float: ...
@abstractmethod def visit_rectangle(self, r: "Rectangle") -> float: ...
class Shape(ABC): @abstractmethod def accept(self, v: ShapeVisitor) -> float: ...
class Circle(Shape): def __init__(self, radius: float) -> None: self.radius = radius
def accept(self, v: ShapeVisitor) -> float: return v.visit_circle(self)
class Rectangle(Shape): def __init__(self, width: float, height: float) -> None: self.width = width self.height = height
def accept(self, v: ShapeVisitor) -> float: return v.visit_rectangle(self)
class AreaVisitor(ShapeVisitor): def visit_circle(self, c: Circle) -> float: return math.pi * c.radius * c.radius
def visit_rectangle(self, r: Rectangle) -> float: return r.width * r.height
shapes: list[Shape] = [Circle(1), Rectangle(2, 3)]area = AreaVisitor()for s in shapes: print(f"{s.accept(area):.2f}") # 3.14, then 6.00package main
import ( "fmt" "math")
// Visitor declares one method per concrete element type.type Visitor interface { VisitCircle(c Circle) float64 VisitRectangle(r Rectangle) float64}
type Shape interface { Accept(v Visitor) float64}
type Circle struct{ Radius float64 }
func (c Circle) Accept(v Visitor) float64 { return v.VisitCircle(c)}
type Rectangle struct{ Width, Height float64 }
func (r Rectangle) Accept(v Visitor) float64 { return v.VisitRectangle(r)}
type AreaVisitor struct{}
func (AreaVisitor) VisitCircle(c Circle) float64 { return math.Pi * c.Radius * c.Radius}
func (AreaVisitor) VisitRectangle(r Rectangle) float64 { return r.Width * r.Height}
func main() { shapes := []Shape{Circle{Radius: 1}, Rectangle{Width: 2, Height: 3}} area := AreaVisitor{} for _, s := range shapes { fmt.Printf("%.2f\n", s.Accept(area)) // 3.14, then 6.00 }}use std::f64::consts::PI;
// Visitor has one method per element variant.trait Visitor { fn visit_circle(&self, c: &Circle) -> f64; fn visit_rectangle(&self, r: &Rectangle) -> f64;}
trait Shape { fn accept(&self, v: &dyn Visitor) -> f64;}
struct Circle { radius: f64,}impl Shape for Circle { fn accept(&self, v: &dyn Visitor) -> f64 { v.visit_circle(self) }}
struct Rectangle { width: f64, height: f64,}impl Shape for Rectangle { fn accept(&self, v: &dyn Visitor) -> f64 { v.visit_rectangle(self) }}
struct AreaVisitor;impl Visitor for AreaVisitor { fn visit_circle(&self, c: &Circle) -> f64 { PI * c.radius * c.radius } fn visit_rectangle(&self, r: &Rectangle) -> f64 { r.width * r.height }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 2.0, height: 3.0 }), ]; let area = AreaVisitor; for s in &shapes { println!("{:.2}", s.accept(&area)); // 3.14, then 6.00 }}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: เพิ่ม operation ใหม่บนโครงสร้างได้ด้วยการเขียน visitor ใหม่ โดยไม่ต้องเปลี่ยน class ของ element
- ข้อดี: พฤติกรรมที่เกี่ยวข้องกันของ operation หนึ่งอยู่รวมกันใน visitor ตัวเดียว แทนที่จะถูกป้ายกระจายไปทั่วทุก element
- ข้อเสีย: การเพิ่ม element ชนิดใหม่บังคับให้ต้องเปลี่ยนทุก visitor — ข้อแลกเปลี่ยนตรงกันข้ามพอดี ดังนั้นจงใช้ pattern นี้เฉพาะเมื่อชุดของ element นิ่งแล้วเท่านั้น
- ข้อเสีย: visitor มักต้องเข้าถึงส่วนภายในของ element ซึ่งอาจทำให้ encapsulation อ่อนแอลง
- ข้อเสีย: พิธีรีตองของ double dispatch (เมท็อด accept บวกกับเมท็อด visit) เยิ่นเย้อเกินไปสำหรับลำดับชั้นเล็ก ๆ
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- ต้นไม้ของ Composite คือโครงสร้างต้นแบบที่ visitor เดินผ่าน visitor เดินผ่าน composite และนำ operation ไปใช้ที่แต่ละโหนด
- Iterator สามารถป้อน element ให้แก่ visitor แยก วิธีที่คุณเดินผ่าน ออกจาก สิ่งที่คุณทำ ที่แต่ละ element
| Visitor | Iterator | Strategy | |
|---|---|---|---|
| จุดประสงค์ | เพิ่ม operation ใหม่บน structure เดิม | วนซ้ำ collection ด้วย interface เดียว | สลับ algorithm ให้ context |
| เปลี่ยน structure | ไม่ได้ (open/closed) | ไม่ควร (อาจ corrupt) | ไม่เปลี่ยน |
| เพิ่ม node type ใหม่ | ต้องแก้ visitor ทุกตัว | ง่าย | ไม่เกี่ยวข้อง |
| ตัวอย่าง | AST transform, lint rule, report gen | for..of, generator | pricing, compression |