- 1. Visitor Pattern คืออะไร
- 2. ปัญหาที่ต้องการแก้
- 3. บทบาทและโครงสร้าง
- 4. ตัวอย่าง Java ที่รันได้
- 5. ทำไมต้องมี accept(): เข้าใจ Double Dispatch
- 6. เพิ่มงานใหม่โดยไม่แก้คลาสสินค้า
- 7. ถ้าเพิ่มสินค้า Food จะเกิดอะไรขึ้น
- 8. เลือกใช้เมื่อใด
- 9. Visitor ไม่ได้กำหนดวิธีเดินต้นไม้ให้เอง
- 10. ข้อผิดพลาดที่พบบ่อยและแนวทางตรวจสอบ
- 11. แบบฝึกหัด Workshop
- 12. เอกสารอ้างอิง
แยกการทำงานที่เปลี่ยนบ่อยออกจากโครงสร้างออบเจ็กต์ที่ค่อนข้างคงที่ โดยให้ออบเจ็กต์รับผู้มาเยือนผ่าน
accept()แล้วส่งต่อไปยังvisit()ที่ตรงกับชนิดของตน

หมวดหมู่: 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 จัดการอย่างจำเพาะ ต้องดำเนินการดังนี้
- สร้าง
Food implements Productและเขียนaccept()ให้เรียกvisitor.visit(this) - เพิ่ม
R visit(Food food)ในProductVisitor<R> - เพิ่ม implementation ให้
ShippingVisitor,LabelVisitor,PackingVisitorและ Visitor อื่นที่ได้รับผลกระทบ - ตรวจสอบกติกาธุรกิจและผลทดสอบสำหรับสินค้าใหม่
ในรูปแบบ 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
#คำถามอภิปราย
- ทำไม
accept()ในBookและElectronicsจึงหน้าตาคล้ายกัน แต่ยังมีความสำคัญต่อการเลือก overload? - ทำไมตัวอย่างให้ Client รวมค่าจัดส่ง แทนการเก็บยอดสะสมไว้ใน ShippingVisitor?
- หากร้านค้าเพิ่มชนิดสินค้าทุกสัปดาห์ แต่แทบไม่เพิ่มงานใหม่ Visitor ยังเหมาะหรือไม่?
- งานใดควรอยู่ใน 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 ที่เกี่ยวข้อง