แยกการทำงานที่เปลี่ยนบ่อยออกจากโครงสร้างออบเจ็กต์ที่ค่อนข้างคงที่ โดยให้ออบเจ็กต์รับผู้มาเยือนผ่าน accept() แล้วส่งต่อไปยัง visit() ที่ตรงกับชนิดของตน

info-visitor-pattern-with-java

หมวดหมู่: Behavioral Design Pattern
สิ่งที่ได้ฝึก: Interface, Polymorphism, Method Overloading, Generics และ Double Dispatch

#1. Visitor Pattern คืออะไร

Visitor Pattern เป็นรูปแบบการออกแบบที่แยก “งานที่ทำกับออบเจ็กต์” ออกมาไว้ในคลาส Visitor แทนการเพิ่มทุกงานลงในคลาสข้อมูล เช่น แยกงานคำนวณค่าจัดส่ง งานสร้างป้ายสินค้า และงานเลือกบรรจุภัณฑ์ออกจากคลาสสินค้า

องค์ประกอบสำคัญมีสองฝั่ง คือ Element ซึ่งเป็นออบเจ็กต์ที่ถูกประมวลผล และ Visitor ซึ่งเป็นออบเจ็กต์ที่บรรจุการทำงานสำหรับ Element แต่ละชนิด

เมื่อเตรียมโครงสร้างให้รองรับ Visitor แล้ว การเพิ่มงานใหม่ทำได้ด้วยการสร้าง Visitor ตัวใหม่ โดยไม่ต้องเพิ่มเมธอดงานนั้นลงใน Element เดิมทุกคลาส อย่างไรก็ตาม การนำแพตเทิร์นนี้มาใช้ครั้งแรกยังต้องเพิ่มสัญญา accept() ให้กับ Element

ตัวอย่างใน Java API คือ ElementVisitor<R, P> ซึ่งใช้ทำงานกับองค์ประกอบของโปรแกรม เมื่อส่ง Visitor เข้า Element.accept() จะเรียกเมธอด visitXyz() ที่เหมาะกับองค์ประกอบนั้น [1]

#2. ปัญหาที่ต้องการแก้

สมมติระบบร้านค้ามีสินค้าสองชนิด

สินค้า ข้อมูลเฉพาะ
Book ชื่อ จำนวนหน้า น้ำหนัก
Electronics ชื่อ น้ำหนัก สถานะแตกหักง่าย

ต่อมาระบบต้องรองรับหลายงาน ได้แก่ คำนวณค่าจัดส่ง สร้างป้ายสินค้า และเลือกบรรจุภัณฑ์

หากเพิ่มทุกงานลงในคลาสสินค้า แต่ละคลาสจะมีทั้งข้อมูลและรายละเอียดของหลายงาน เช่น calculateShipping(), createLabel() และ choosePackaging() เมื่อเพิ่มงานอีกก็ต้องกลับมาแก้คลาสสินค้าเหล่านี้

อีกทางหนึ่งคือรวมทุกอย่างไว้ใน Service แล้วตรวจชนิดด้วย instanceof แต่เมื่อมีหลายงาน ก็อาจเกิดเงื่อนไขตรวจชนิดซ้ำอยู่หลายแห่ง

Visitor จัดกลุ่มโค้ดตาม งาน โดยแต่ละ Visitor มีเมธอดรองรับสินค้าทุกชนิดในสัญญาเดียวกัน

งาน คลาสที่รับผิดชอบ
คำนวณค่าจัดส่ง ShippingVisitor
สร้างป้ายสินค้า LabelVisitor
เลือกบรรจุภัณฑ์ PackingVisitor

แพตเทิร์นนี้ไม่ได้ทำให้เงื่อนไขธุรกิจทั้งหมดหายไป เช่น การตรวจว่าสินค้าแตกหักง่ายหรือไม่ยังเป็นเงื่อนไขที่เหมาะสมภายในงานคำนวณค่าจัดส่ง

#3. บทบาทและโครงสร้าง

บทบาท ในตัวอย่าง หน้าที่
Element Product ประกาศ accept(visitor)
Concrete Element Book, Electronics ส่งตัวเองให้ Visitor ผ่าน visitor.visit(this)
Visitor ProductVisitor<R> ประกาศ visit() แยกตามชนิด Element
Concrete Visitor ShippingVisitor, LabelVisitor เขียนการทำงานจริง
Object Structure List<Product> เก็บออบเจ็กต์หลายชนิด
Client VisitorDemo เลือก Visitor และเริ่มประมวลผล
classDiagram
    direction TB
    class Product {
        <<interface>>
        +accept(visitor) R
    }
    class Book {
        +name
        +pages
        +weightKg
        +accept(visitor) R
    }
    class Electronics {
        +name
        +weightKg
        +fragile
        +accept(visitor) R
    }
    class ProductVisitor {
        <<interface>>
        +visit(Book) R
        +visit(Electronics) R
    }
    class ShippingVisitor {
        +visit(Book) BigDecimal
        +visit(Electronics) BigDecimal
    }
    class LabelVisitor {
        +visit(Book) String
        +visit(Electronics) String
    }
    Product <|.. Book
    Product <|.. Electronics
    ProductVisitor <|.. ShippingVisitor
    ProductVisitor <|.. LabelVisitor
    Product ..> ProductVisitor : accepts

แผนภาพย่อรูปแบบ Generic เพื่อให้อ่านง่าย ในโค้ดจริงใช้ ProductVisitor<R> โดย R คือชนิดผลลัพธ์ของงาน เช่น BigDecimal สำหรับค่าจัดส่ง และ String สำหรับข้อความป้ายสินค้า

#4. ตัวอย่าง Java ที่รันได้

กติกาค่าจัดส่งต่อไปนี้เป็น ข้อมูลสมมติสำหรับการเรียนรู้ ไม่ใช่อัตราของบริษัทขนส่ง

สินค้า สูตรค่าจัดส่ง
หนังสือ 20 บาท + น้ำหนักกิโลกรัม × 25 บาท
อุปกรณ์อิเล็กทรอนิกส์ 50 บาท + น้ำหนักกิโลกรัม × 40 บาท
อุปกรณ์ที่แตกหักง่าย เพิ่มจากสูตรอุปกรณ์อีก 30 บาท

สร้างไฟล์ชื่อ VisitorDemo.java แล้วคัดลอกโค้ดทั้งหมดต่อไปนี้

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;

// Element
interface Product {
    <R> R accept(ProductVisitor<R> visitor);
}

// Visitor: one overload for each concrete element type
interface ProductVisitor<R> {
    R visit(Book book);
    R visit(Electronics electronics);
}

// Concrete elements
record Book(String name, int pages, BigDecimal weightKg)
        implements Product {
    public Book {
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(weightKg, "weightKg");
        if (pages <= 0 || weightKg.signum() <= 0) {
            throw new IllegalArgumentException(
                    "pages and weightKg must be positive");
        }
    }

    @Override
    public <R> R accept(ProductVisitor<R> visitor) {
        return visitor.visit(this);
    }
}

record Electronics(String name, BigDecimal weightKg, boolean fragile)
        implements Product {
    public Electronics {
        Objects.requireNonNull(name, "name");
        Objects.requireNonNull(weightKg, "weightKg");
        if (weightKg.signum() <= 0) {
            throw new IllegalArgumentException("weightKg must be positive");
        }
    }

    @Override
    public <R> R accept(ProductVisitor<R> visitor) {
        return visitor.visit(this);
    }
}

// Concrete visitor 1: calculate shipping in THB
final class ShippingVisitor implements ProductVisitor<BigDecimal> {
    @Override
    public BigDecimal visit(Book book) {
        return book.weightKg()
                .multiply(new BigDecimal("25.00"))
                .add(new BigDecimal("20.00"))
                .setScale(2, RoundingMode.HALF_UP);
    }

    @Override
    public BigDecimal visit(Electronics electronics) {
        BigDecimal fee = electronics.weightKg()
                .multiply(new BigDecimal("40.00"))
                .add(new BigDecimal("50.00"));
        if (electronics.fragile()) {
            fee = fee.add(new BigDecimal("30.00"));
        }
        return fee.setScale(2, RoundingMode.HALF_UP);
    }
}

// Concrete visitor 2: generate a display label
final class LabelVisitor implements ProductVisitor<String> {
    @Override
    public String visit(Book book) {
        return "BOOK | " + book.name() + " | " + book.pages() + " pages";
    }

    @Override
    public String visit(Electronics electronics) {
        return "ELECTRONICS | " + electronics.name() + " | "
                + (electronics.fragile() ? "FRAGILE" : "STANDARD");
    }
}

// Client; the list is the object structure in this example
public class VisitorDemo {
    public static void main(String[] args) {
        List<Product> cart = List.of(
                new Book("Java Design Patterns", 320, new BigDecimal("0.80")),
                new Electronics(
                        "Mechanical Keyboard", new BigDecimal("1.20"), true)
        );

        ProductVisitor<BigDecimal> shipping = new ShippingVisitor();
        ProductVisitor<String> labels = new LabelVisitor();
        BigDecimal total = new BigDecimal("0.00");

        for (Product product : cart) {
            System.out.println(product.accept(labels));
            BigDecimal fee = product.accept(shipping);
            System.out.println("Shipping: " + fee.toPlainString() + " THB");
            total = total.add(fee);
        }

        System.out.println(
                "Total shipping: " + total.toPlainString() + " THB");
    }
}

ตัวอย่างใช้ record เพื่อประกาศข้อมูลที่แก้ค่าฟิลด์โดยตรงไม่ได้ พร้อมเมธอดอ่านค่าที่ Java สร้างให้ เช่น book.pages() และ electronics.fragile() หากใช้คลาสปกติก็สามารถประยุกต์แนวคิดเดียวกันได้

ใช้ BigDecimal ที่สร้างจากข้อความเพื่อกำหนดค่าทศนิยมในตัวอย่าง และปัดค่าจัดส่งของแต่ละชิ้นเป็นสองตำแหน่งด้วย HALF_UP ก่อนนำมารวม ตัวอย่างนี้จงใจให้ Visitor ไม่มีตัวแปรสะสมยอด จึงเรียกซ้ำกับสินค้าเดิมได้โดยไม่เกิดยอดค้างจากครั้งก่อน

#คอมไพล์และรัน

เปิด Terminal ในโฟลเดอร์ที่เก็บไฟล์ แล้วใช้คำสั่ง

javac --release 17 VisitorDemo.java
java VisitorDemo

หรือใช้ Source-file Mode ของ JDK

java VisitorDemo.java

ผลลัพธ์

BOOK | Java Design Patterns | 320 pages
Shipping: 40.00 THB
ELECTRONICS | Mechanical Keyboard | FRAGILE
Shipping: 128.00 THB
Total shipping: 168.00 THB

ตรวจสูตรได้ดังนี้

  • หนังสือ: 20 + (0.80 × 25) = 40.00 บาท
  • คีย์บอร์ดที่แตกหักง่าย: 50 + (1.20 × 40) + 30 = 128.00 บาท
  • ค่าจัดส่งรวม: 40.00 + 128.00 = 168.00 บาท

ค่ารวมนี้เป็นผลบวกค่าจัดส่งรายชิ้น ไม่ใช่การคำนวณน้ำหนักรวมเพื่อส่งกล่องเดียว

#5. ทำไมต้องมี accept(): เข้าใจ Double Dispatch

พิจารณาโค้ด

Product product = new Book(
        "Java Design Patterns", 320, new BigDecimal("0.80"));
ProductVisitor<BigDecimal> shipping = new ShippingVisitor();

BigDecimal fee = product.accept(shipping);

แม้ตัวแปร product จะประกาศเป็น Product แต่ในขณะทำงาน ออบเจ็กต์จริงคือ Book การเรียก accept() จึงเข้าไปทำงานใน Book.accept()

ภายใน Book.accept() มีโค้ด

return visitor.visit(this);

ณ จุดนี้ this มีชนิดที่คอมไพเลอร์รู้ว่าเป็น Book จึงเลือก signature visit(Book) และเมื่อทำงานจริงจะเรียก implementation ของ Visitor ที่ส่งเข้ามา เช่น ShippingVisitor.visit(Book)

จึงมีการ dispatch ผ่านเมธอดอินสแตนซ์สองครั้ง ได้แก่ เลือก accept() ตาม Element จริง แล้วเลือก implementation ของ visit(Book) ตาม Visitor จริง ผลของงานจึงขึ้นกับทั้งชนิด Element และชนิด Visitor

ประเด็นที่ต้องแยกให้ชัด: การเลือก overload เช่น visit(Book) หรือ visit(Electronics) เกิดในขั้นคอมไพล์ ส่วนการเลือก implementation ของเมธอดที่ override เกิดขณะทำงานตามกติกาของ Java ไม่ใช่การที่ Java เลือก overload ใหม่จากชนิดอาร์กิวเมนต์จริงทุกครั้งที่รัน [2]

#ทำไมเรียก visitor.visit(product) ตรง ๆ ไม่ได้

โค้ดต่อไปนี้ คอมไพล์ไม่ผ่านกับ interface ในตัวอย่าง เพราะไม่มี overload ที่รับ Product

Product product = new Book(
        "Java Design Patterns", 320, new BigDecimal("0.80"));
ProductVisitor<BigDecimal> shipping = new ShippingVisitor();

// ไม่มี visit(Product) ใน ProductVisitor
// BigDecimal fee = shipping.visit(product);

แม้เพิ่ม visit(Product) เข้าไป การเรียกด้วยตัวแปรที่ประกาศชนิด Product ก็จะเลือก overload นั้น ไม่ได้เปลี่ยนไปเลือก visit(Book) อัตโนมัติเพียงเพราะออบเจ็กต์จริงเป็นหนังสือ

เมธอด accept() ใน Concrete Element จึงเป็นสะพานที่ทำให้การเลือกเมธอดเหมาะกับชนิดของ Element โดยไม่ต้องให้ Client ตรวจชนิดเอง

#6. เพิ่มงานใหม่โดยไม่แก้คลาสสินค้า

ต้องการเพิ่มงานเลือกบรรจุภัณฑ์ ให้เพิ่มคลาสต่อไปนี้ในไฟล์เดิม โดยวางไว้นอกคลาสอื่น

final class PackingVisitor implements ProductVisitor<String> {
    @Override
    public String visit(Book book) {
        return "Padded paper mailer";
    }

    @Override
    public String visit(Electronics electronics) {
        return electronics.fragile()
                ? "Box with bubble wrap"
                : "Standard box";
    }
}

เพิ่มโค้ดต่อไปนี้ใน main() หลังสร้าง cart

ProductVisitor<String> packing = new PackingVisitor();
for (Product product : cart) {
    System.out.println(product.accept(packing));
}

ผลลัพธ์เพิ่มเติม

Padded paper mailer
Box with bubble wrap

การเพิ่มงานนี้ไม่ต้องแก้ Book, Electronics หรือ ProductVisitor แต่ต้องเพิ่มคลาสใหม่และเชื่อมการเรียกใช้งานใน Client

นี่คือขอบเขตที่ Visitor ช่วยเรื่อง Open/Closed Principle: เปิดให้เพิ่ม operation ใหม่ได้สะดวก ภายใต้ชุดชนิด Element ที่รองรับอยู่แล้ว

#7. ถ้าเพิ่มสินค้า Food จะเกิดอะไรขึ้น

การเพิ่ม ชนิด Element ต่างจากการเพิ่ม operation

หากเพิ่ม Food และต้องการให้ Visitor จัดการอย่างจำเพาะ ต้องดำเนินการดังนี้

  1. สร้าง Food implements Product และเขียน accept() ให้เรียก visitor.visit(this)
  2. เพิ่ม R visit(Food food) ใน ProductVisitor<R>
  3. เพิ่ม implementation ให้ ShippingVisitor, LabelVisitor, PackingVisitor และ Visitor อื่นที่ได้รับผลกระทบ
  4. ตรวจสอบกติกาธุรกิจและผลทดสอบสำหรับสินค้าใหม่

ในรูปแบบ interface ที่ใช้ abstract method ดังบทความ คอมไพเลอร์ช่วยแจ้งว่า Visitor ใดยังรองรับชนิดใหม่ไม่ครบ ข้อดีคือมองเห็นงานที่ขาดได้ชัด แต่ต้นทุนคือกระทบหลายคลาส

หากเปลี่ยนเป็น default method เพื่อให้ชนิดใหม่ถูกข้ามหรือใช้ผลลัพธ์ทั่วไป จะลดภาระด้านความเข้ากันได้บางส่วน แต่ต้องยอมรับว่าอาจไม่พบการรองรับที่ขาดด้วยข้อผิดพลาดตอนคอมไพล์

Java API ของ ElementVisitor ก็ระบุข้อควรพิจารณาเรื่องการเพิ่มโครงสร้างภาษาใหม่และความเข้ากันได้ของ Visitor จึงมีคลาสฐานและกลไกจัดการองค์ประกอบที่ไม่รู้จัก [1]

#8. เลือกใช้เมื่อใด

สถานการณ์ แนวทางที่เหมาะสม
ชนิดข้อมูลค่อนข้างคงที่ แต่มีงานใหม่เพิ่มบ่อย พิจารณา Visitor
มีโครงสร้างต้นไม้และหลายงาน เช่น ตรวจสอบ วิเคราะห์ ส่งออก พิจารณา Visitor ร่วมกับวิธีเดินโครงสร้าง
เพิ่มชนิดข้อมูลบ่อย แต่มีงานหลักเพียงไม่กี่งาน พิจารณาวางพฤติกรรมในคลาสผ่าน Polymorphism
ต้องสลับอัลกอริทึมของงานเดียวผ่านสัญญาเดียวกัน พิจารณา Strategy
มีเงื่อนไขไม่กี่กรณีในงานเล็ก ๆ เงื่อนไขที่อ่านง่ายอาจเพียงพอ
แก้ Element ให้มี accept() ไม่ได้ Classic Visitor อาจไม่สะดวก ควรพิจารณา adapter หรือวิธีอื่น

Visitor ช่วยรวมโค้ดของงานหนึ่งไว้ในที่เดียว และเขียนการทำงานจำเพาะชนิดได้โดยไม่ cast แต่เพิ่มจำนวนคลาส ทำให้ Visitor รู้จัก Concrete Element หลายชนิด และอาจต้องเปิดเผยข้อมูลผ่าน accessor มากขึ้น

จึงควรถามว่า ระบบเพิ่มงานบ่อยกว่าเพิ่มชนิดข้อมูลหรือไม่ ก่อนนำแพตเทิร์นนี้มาใช้

#Visitor, Strategy และ Iterator ต่างกันอย่างไร

Pattern คำถามหลัก บทบาทของมัน
Visitor จะทำงานอะไรกับ Element แต่ละชนิด แยก operation ออกจาก Element
Strategy จะเลือกอัลกอริทึมใดสำหรับงานนี้ สลับวิธีทำงานภายใต้สัญญาเดียวกัน
Iterator จะเข้าถึงสมาชิกทีละตัวอย่างไร แยกกลไกการวนผ่านสมาชิก

ในตัวอย่าง for ทำหน้าที่วนผ่านรายการ ส่วน Visitor ทำหน้าที่ประมวลผลสินค้าแต่ละชนิด ทั้งสองส่วนใช้งานร่วมกันได้

#9. Visitor ไม่ได้กำหนดวิธีเดินต้นไม้ให้เอง

แม้ Visitor มักพบในงาน Abstract Syntax Tree หรือโครงสร้างเอกสาร แต่แพตเทิร์นนี้ไม่ได้บังคับว่าต้องเป็นต้นไม้ ตัวอย่าง List<Product> ก็ใช้ได้

ถ้าโครงสร้างมีลูกหลายระดับ เช่น หมวดสินค้า ภายในมีหมวดย่อยและสินค้า ต้องเลือกให้ชัดว่าใครเดินผ่านสมาชิก

  • ให้โครงสร้างหรือ Client เดิน แล้วเรียก accept() ของแต่ละโหนด
  • ให้ Visitor เดินต่อไปยังลูก เพื่อควบคุมลำดับหรือข้ามบางโหนด

หากทั้งสองฝ่ายเดินซ้ำโดยไม่ตั้งใจ อาจประมวลผลโหนดเดียวกันหลายครั้ง หากโครงสร้างเป็นกราฟที่มีวงจร ต้องมีวิธีตรวจโหนดที่เคยเยี่ยมชมด้วย

อีก API ที่เกี่ยวข้องคือ FileVisitor ซึ่งส่งเข้า Files.walkFileTree() เพื่อกำหนดงานก่อนเข้าไดเรกทอรี หลังออกจากไดเรกทอรี และเมื่อพบไฟล์ [3] API นี้แสดงการแยกงานประมวลผลออกจากกลไกเดินไฟล์ แต่รูปแบบ callback ของมันไม่เหมือน accept()/visit() ในตัวอย่างทุกประการ

#10. ข้อผิดพลาดที่พบบ่อยและแนวทางตรวจสอบ

เขียน accept() ไว้เฉพาะคลาสฐาน แล้วคาดหวังให้ overload รองรับทุกคลาสลูกโดยอัตโนมัติ
ชนิดของ this ที่ใช้เลือก overload ขึ้นกับตำแหน่งที่เขียนเมธอด หากต้องการ visit() จำเพาะคลาสลูก ต้องจัดการ accept() ให้สอดคล้อง ตัวอย่างใช้ record ซึ่งไม่เปิดให้สืบทอดต่อ จึงเห็นการจับคู่ชนิดได้ชัดเจน

เก็บยอดรวมไว้ใน Visitor แล้วใช้ซ้ำโดยไม่รีเซ็ต
อาจเกิดยอดสะสมข้ามคำขอ ตัวอย่างนี้คืนค่ารายชิ้นแล้วให้ Client รวมเอง หากต้องใช้ Visitor ที่มี state ควรกำหนดอายุใช้งานและการใช้งานพร้อมกันให้ชัดเจน

เปิดเผยข้อมูลภายในมากเกินไปเพื่อให้ Visitor เข้าถึงทุกอย่าง
ควรให้ accessor เท่าที่จำเป็น และคงกติกาความถูกต้องของข้อมูลไว้ใน Element เช่น น้ำหนักต้องเป็นบวก

สร้าง Visitor แยกทุกเมธอดโดยไม่พิจารณาความคุ้มค่า
หากงานเป็นพฤติกรรมหลักของออบเจ็กต์และมีจำนวนน้อย การใช้เมธอดปกติอาจอ่านง่ายกว่า

กรณีตรวจสอบสำหรับตัวอย่าง

กรณี ผลที่คาดหวัง
หนังสือหนัก 0.80 กก. ค่าจัดส่ง 40.00 บาท
อุปกรณ์หนัก 1.20 กก. แตกหักง่าย ค่าจัดส่ง 128.00 บาท
อุปกรณ์หนัก 1.20 กก. ไม่แตกหักง่าย ค่าจัดส่ง 98.00 บาท
สินค้าสองชิ้นตามตัวอย่าง ยอดรวม 168.00 บาท
เรียก ShippingVisitor ซ้ำกับหนังสือเดิม ได้ 40.00 บาททุกครั้ง
น้ำหนักเป็นศูนย์หรือติดลบ constructor ปฏิเสธด้วย IllegalArgumentException
จำนวนหน้าหนังสือไม่เป็นบวก constructor ปฏิเสธด้วย IllegalArgumentException
เพิ่ม PackingVisitor สินค้าเดิมทำงานได้โดยไม่แก้คลาสสินค้า

เมื่อตรวจค่าตัวเลข BigDecimal ให้พิจารณาใช้ compareTo() เพราะ equals() เปรียบเทียบ scale ด้วย เช่น 40.0 กับ 40.00 มีค่าเชิงตัวเลขเท่ากันแต่ไม่เท่ากันตาม equals()

#11. แบบฝึกหัด Workshop

#กิจกรรมที่ 1: เพิ่มสินค้าในตะกร้า

เพิ่มหนังสือหนัก 0.40 กก. จำนวนหน้าเป็นบวก

ผลที่คาดหวัง: ค่าจัดส่งหนังสือชิ้นใหม่เท่ากับ 30.00 บาท และยอดรวมของตะกร้าเดิมบวกชิ้นนี้เป็น 198.00 บาท

#กิจกรรมที่ 2: เปลี่ยนสถานะแตกหักง่าย

เปลี่ยน fragile ของคีย์บอร์ดเป็น false

ผลที่คาดหวัง: ค่าจัดส่งคีย์บอร์ดลดเหลือ 98.00 บาท ยอดรวมสินค้าสองชิ้นเดิมเป็น 138.00 บาท และป้ายคีย์บอร์ดลงท้ายด้วย STANDARD

#กิจกรรมที่ 3: เพิ่ม PackingVisitor

เพิ่ม Visitor ตามหัวข้อ 6 แล้วให้ Client พิมพ์บรรจุภัณฑ์ของสินค้าทุกชิ้น

เป้าหมาย: เพิ่มงานใหม่โดยไม่แก้ Book, Electronics และ ProductVisitor

#กิจกรรมที่ 4: เพิ่ม Food

สร้างสินค้าชนิด Food แล้วกำหนดอัตราจัดส่งและรูปแบบป้ายด้วยตนเอง

เป้าหมาย: บันทึกว่าต้องแก้ไฟล์หรือคลาสใดบ้าง แล้วเปรียบเทียบต้นทุนกับการเพิ่ม PackingVisitor

#คำถามอภิปราย

  1. ทำไม accept() ใน Book และ Electronics จึงหน้าตาคล้ายกัน แต่ยังมีความสำคัญต่อการเลือก overload?
  2. ทำไมตัวอย่างให้ Client รวมค่าจัดส่ง แทนการเก็บยอดสะสมไว้ใน ShippingVisitor?
  3. หากร้านค้าเพิ่มชนิดสินค้าทุกสัปดาห์ แต่แทบไม่เพิ่มงานใหม่ Visitor ยังเหมาะหรือไม่?
  4. งานใดควรอยู่ใน Element เพื่อรักษาความถูกต้องของข้อมูล และงานใดเหมาะกับ Visitor?

#12. เอกสารอ้างอิง

[1] Oracle, ElementVisitor — Java SE 17
https://docs.oracle.com/en/java/javase/17/docs/api/java.compiler/javax/lang/model/element/ElementVisitor.html

[2] Oracle, Java Language Specification, Java SE 17, §15.12 Method Invocation Expressions
ดู §15.12.2 สำหรับการเลือก signature ตอนคอมไพล์ และ §15.12.4.4 สำหรับการค้นหาเมธอดขณะทำงาน
https://docs.oracle.com/javase/specs/jls/se17/html/jls-15.html#jls-15.12

[3] Oracle, FileVisitor — Java SE 17
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/FileVisitor.html


แนวคิดที่ควรจำ: เมื่อชุดชนิดออบเจ็กต์ค่อนข้างคงที่ การเพิ่มงานด้วย Visitor ช่วยจัดโค้ดตาม operation ได้ชัดเจน ส่วนการเพิ่มชนิดออบเจ็กต์ใหม่จะมีต้นทุนกับสัญญาและ Visitor ที่เกี่ยวข้อง