ออกแบบการส่งต่อคำขอให้ผู้รับผิดชอบแต่ละระดับ โดยให้ผู้ส่งคำขอรู้จักเพียงจุดเริ่มต้นของสายงาน

info-chain-of-responsibility-pattern-with-java หมวดหมู่: Behavioral Design Pattern

#1. Chain of Responsibility คืออะไร

Chain of Responsibility เป็นรูปแบบการออกแบบที่เชื่อมออบเจ็กต์ผู้รับคำขอ หรือ Handler เป็นสาย แต่ละ Handler ตัดสินใจว่าจะจัดการคำขอนั้นเอง ส่งต่อให้ Handler ถัดไป หรือยุติการทำงานตามกติกาที่ออกแบบไว้

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

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

หลักสำคัญ: กำหนดให้ชัดว่า Handler จะหยุดเมื่อใด ส่งต่อเมื่อใด และเมื่อคำขอไปถึงปลายสายโดยไม่มีผู้รับผิดชอบจะเกิดอะไรขึ้น

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

สมมติระบบอนุมัติค่าใช้จ่ายมีกติกาดังนี้

ผู้อนุมัติ วงเงินสูงสุดต่อรายการ
หัวหน้าทีม — TeamLead 5,000 บาท
ผู้จัดการ — Manager 20,000 บาท
ผู้อำนวยการ — Director 100,000 บาท

คำขอที่มียอดไม่เกินวงเงินของ Handler คนแรกที่รับได้ จะได้รับอนุมัติและจบการทำงาน หากยอดเกิน 100,000 บาท จะได้ผลลัพธ์ว่าไม่มีผู้มีอำนาจอนุมัติในสายนี้

วิธีเริ่มต้นอาจเขียนเงื่อนไขรวมไว้ในเมธอดเดียว

if (amount.compareTo(new BigDecimal("5000")) <= 0) {
    // TeamLead approves
} else if (amount.compareTo(new BigDecimal("20000")) <= 0) {
    // Manager approves
} else if (amount.compareTo(new BigDecimal("100000")) <= 0) {
    // Director approves
} else {
    // No eligible approver
}

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

Chain of Responsibility ช่วยแยกผู้รับผิดชอบออกเป็นออบเจ็กต์ และย้ายการกำหนดลำดับไปไว้ในจุดประกอบระบบ

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

#3. ส่วนประกอบของ Pattern

บทบาท หน้าที่ ชื่อในตัวอย่าง
Request ข้อมูลคำขอที่ถูกส่งเข้าไป ExpenseRequest
Handler สัญญากลางสำหรับรับคำขอ ApprovalHandler
Base Handler เก็บตัวถัดไปและรวมตรรกะการส่งต่อ Approver
Concrete Handler ผู้รับผิดชอบแต่ละประเภท TeamLead, Manager, Director
Client ส่งคำขอเข้าจุดเริ่มต้น main()
Result ผลการจัดการคำขอ ApprovalResult

Base Handler เป็นตัวช่วยลดโค้ดซ้ำ ไม่ใช่ข้อบังคับของ Pattern สามารถออกแบบด้วย interface และ composition โดยไม่ใช้คลาสฐานก็ได้

#แผนภาพโครงสร้าง

classDiagram
    direction TB
    class ApprovalHandler {
        <<interface>>
        +handle(request) ApprovalResult
    }
    class Approver {
        <<abstract>>
        -ApprovalHandler next
        -BigDecimal limit
        +handle(request) ApprovalResult
    }
    class TeamLead
    class Manager
    class Director
    ApprovalHandler <|.. Approver
    Approver <|-- TeamLead
    Approver <|-- Manager
    Approver <|-- Director
    Approver --> ApprovalHandler : next

#4. กำหนดพฤติกรรมก่อนเขียนโค้ด

บทความนี้ใช้รูปแบบ พบผู้รับผิดชอบแล้วหยุด โดยมีสัญญาการทำงานดังนี้

  1. จำนวนเงินต้องมากกว่าศูนย์ และข้อมูลที่จำเป็นต้องไม่เป็น null
  2. เริ่มตรวจที่หัวหน้าทีม จากนั้นจึงเป็นผู้จัดการและผู้อำนวยการ
  3. ถ้ายอดน้อยกว่าหรือเท่ากับวงเงินของผู้รับคำขอ ให้คืนผลอนุมัติทันที
  4. ถ้าเกินวงเงิน ให้ส่งคำขอเดิมไปยังตัวถัดไป
  5. ถ้าไม่มีตัวถัดไป ให้คืนสถานะ UNHANDLED

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

#ขั้นตอนการตัดสินใจของ Handler หนึ่งตัว

flowchart TD
    A["รับคำขอ"] --> B{"ยอดไม่เกินวงเงิน?"}
    B -->|ใช่| C["อนุมัติและหยุด"]
    B -->|ไม่ใช่| D{"มีตัวถัดไป?"}
    D -->|มี| E["ส่งต่อคำขอเดิม"]
    D -->|ไม่มี| F["คืน UNHANDLED"]

#5. Workshop: สร้างระบบอนุมัติด้วย Java

#ขั้นที่ 1: ตรวจสอบเครื่องมือ

ติดตั้ง JDK 17 ขึ้นไป แล้วตรวจสอบด้วยคำสั่ง

java --version
javac --version

ตัวอย่างใช้ record สำหรับข้อมูลคำขอและผลลัพธ์ โดยไม่พึ่ง framework หรือ dependency ภายนอก

#ขั้นที่ 2: สร้างไฟล์ ChainOfResponsibilityDemo.java

โค้ดต่อไปนี้ครบในไฟล์เดียว คัดลอกไปคอมไพล์ได้ทันที

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

public class ChainOfResponsibilityDemo {

    enum Status {
        APPROVED,
        UNHANDLED
    }

    record ExpenseRequest(String id, BigDecimal amount) {
        ExpenseRequest {
            Objects.requireNonNull(id, "id must not be null");
            Objects.requireNonNull(amount, "amount must not be null");
            if (id.isBlank()) {
                throw new IllegalArgumentException("id must not be blank");
            }
            if (amount.signum() <= 0) {
                throw new IllegalArgumentException("amount must be positive");
            }
        }
    }

    record ApprovalResult(Status status, String handledBy) {
    }

    interface ApprovalHandler {
        ApprovalResult handle(ExpenseRequest request);
    }

    abstract static class Approver implements ApprovalHandler {
        private final String name;
        private final BigDecimal limit;
        private final ApprovalHandler next;

        protected Approver(String name, String limit, ApprovalHandler next) {
            this.name = name;
            this.limit = new BigDecimal(limit);
            this.next = next;
        }

        @Override
        public final ApprovalResult handle(ExpenseRequest request) {
            Objects.requireNonNull(request, "request must not be null");

            if (request.amount().compareTo(limit) <= 0) {
                return new ApprovalResult(Status.APPROVED, name);
            }

            if (next != null) {
                return next.handle(request);
            }

            return new ApprovalResult(Status.UNHANDLED, "-");
        }
    }

    static final class TeamLead extends Approver {
        TeamLead(ApprovalHandler next) {
            super("TeamLead", "5000", next);
        }
    }

    static final class Manager extends Approver {
        Manager(ApprovalHandler next) {
            super("Manager", "20000", next);
        }
    }

    static final class Director extends Approver {
        Director(ApprovalHandler next) {
            super("Director", "100000", next);
        }
    }

    public static void main(String[] args) {
        ApprovalHandler chain = new TeamLead(
                new Manager(
                        new Director(null)));

        List<ExpenseRequest> requests = List.of(
                new ExpenseRequest("EXP-001", new BigDecimal("3000")),
                new ExpenseRequest("EXP-002", new BigDecimal("12000")),
                new ExpenseRequest("EXP-003", new BigDecimal("70000")),
                new ExpenseRequest("EXP-004", new BigDecimal("150000"))
        );

        for (ExpenseRequest request : requests) {
            ApprovalResult result = chain.handle(request);
            System.out.printf("%s | %s THB | %s | %s%n",
                    request.id(),
                    request.amount().toPlainString(),
                    result.status(),
                    result.handledBy());
        }
    }
}

#ขั้นที่ 3: คอมไพล์และรัน

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

javac --release 17 ChainOfResponsibilityDemo.java
java ChainOfResponsibilityDemo

หรือใช้โหมดรันไฟล์ต้นฉบับโดยตรงของ JDK

java ChainOfResponsibilityDemo.java

ผลลัพธ์

EXP-001 | 3000 THB | APPROVED | TeamLead
EXP-002 | 12000 THB | APPROVED | Manager
EXP-003 | 70000 THB | APPROVED | Director
EXP-004 | 150000 THB | UNHANDLED | -

#6. อ่านโค้ดให้เห็นแนวคิด

#6.1 Client รู้จักเพียง Handler ตัวแรก

ApprovalResult result = chain.handle(request);

Client ไม่ต้องเลือกผู้อนุมัติด้วย if-else และไม่ต้องเรียก Manager หรือ Director เอง ผู้รับคำขอแต่ละตัวเป็นผู้ตัดสินใจส่งต่อ

#6.2 การเชื่อมสายอยู่ในจุดประกอบระบบ

ApprovalHandler chain = new TeamLead(
        new Manager(
                new Director(null)));

การเขียน constructor ซ้อนกันหมายถึงประกอบจากปลายสายกลับมาหาต้นสาย แต่คำขอจะเริ่มจาก TeamLead และเดินไปตาม next

ใช้ final กับตัวอ้างอิง next เพื่อไม่ให้เปลี่ยนสายระหว่างประมวลผล ตัวอย่างนี้ไม่เก็บข้อมูลคำขอไว้ใน Handler จึงไม่มีสถานะของคำขอค้างข้ามการเรียกใช้งาน

#6.3 จัดการแล้วต้องหยุด

if (request.amount().compareTo(limit) <= 0) {
    return new ApprovalResult(Status.APPROVED, name);
}

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

#6.4 จัดการไม่ได้จึงส่งต่อ

if (next != null) {
    return next.handle(request);
}

ส่งคำขอเดิมไปยังตัวถัดไป พร้อมส่งผลลัพธ์ย้อนกลับให้ Client โดยไม่เปลี่ยนความหมายระหว่างทาง

#6.5 ปลายสายต้องมีผลลัพธ์ชัดเจน

return new ApprovalResult(Status.UNHANDLED, "-");

การคืนสถานะชัดเจนช่วยให้ Client แยกคำขอที่อนุมัติแล้วออกจากคำขอที่ยังต้องดำเนินการต่อ แทนการคืน null แล้วให้ผู้เรียกเดาความหมาย

#6.6 เหตุใดใช้ BigDecimal

ตัวอย่างสร้างยอดเงินจากข้อความ เช่น new BigDecimal("12000.50") เพื่อเก็บค่าทศนิยมตามที่ระบุ และใช้ compareTo() สำหรับเปรียบเทียบค่าตัวเลข

ตามเอกสาร Java ค่า BigDecimal("2.0") และ BigDecimal("2.00") เปรียบเทียบด้วย compareTo() แล้วเท่ากัน แต่ equals() พิจารณา scale ด้วย [1] จึงควรเลือกวิธีเปรียบเทียบให้ตรงกับกติกา

ระบบเงินจริงยังต้องกำหนดสกุลเงิน จำนวนตำแหน่งทศนิยม และนโยบายการปัดเศษให้ชัดเจน โค้ดนี้สาธิตการตัดสินวงเงิน ไม่ได้คำนวณภาษีหรืออัตราแลกเปลี่ยน

#7. ติดตามคำขอ 12,000 บาท

ลำดับ ผู้รับคำขอ การตัดสินใจ
1 TeamLead 12,000 เกิน 5,000 จึงส่งต่อ
2 Manager 12,000 ไม่เกิน 20,000 จึงอนุมัติ
3 Director ไม่ถูกเรียก เพราะ Manager จบคำขอแล้ว

ผลสุดท้ายคือ APPROVED โดย Manager

กรณีที่ลำดับเปลี่ยน: หากวาง Director ไว้ก่อน TeamLead รายการ 3,000 บาทจะถูก Director อนุมัติทันที เพราะอยู่ในวงเงิน ดังนั้นลำดับเป็นส่วนหนึ่งของกติกาธุรกิจ ต้องกำหนดและตรวจสอบอย่างตั้งใจ

#8. เพิ่มผู้อนุมัติระดับใหม่

สมมติเพิ่มรองผู้อำนวยการ ซึ่งอนุมัติได้ไม่เกิน 50,000 บาท ให้เพิ่มคลาสนี้ภายใน ChainOfResponsibilityDemo เช่นเดียวกับคลาสอื่น

static final class DeputyDirector extends Approver {
    DeputyDirector(ApprovalHandler next) {
        super("DeputyDirector", "50000", next);
    }
}

จากนั้นเปลี่ยนจุดประกอบสายเป็น

ApprovalHandler chain = new TeamLead(
        new Manager(
                new DeputyDirector(
                        new Director(null))));

รายการ 35,000 บาทจะได้รับอนุมัติจาก DeputyDirector โดยไม่ต้องเปลี่ยนโค้ดที่ส่งคำขอหรือเมธอด handle() เดิม แต่ยังต้องแก้จุดประกอบสายให้รวมผู้อนุมัติใหม่

หากภายหลังแต่ละระดับมีกฎต่างกันมาก ควรแยกจุดตัดสินใจ เช่น canHandle(request) และจุดดำเนินการ เช่น approve(request) ให้ Concrete Handler กำหนดได้ แทนการขยายคลาสฐานด้วยเงื่อนไขเฉพาะทุกประเภท

#9. สองลักษณะการใช้งานที่ควรแยกให้ชัด

ประเด็น พบผู้รับผิดชอบแล้วหยุด ตรวจเป็นลำดับแล้วส่งต่อ
เป้าหมาย หาผู้รับผิดชอบที่เหมาะสมหนึ่งราย ให้หลายขั้นตรวจหรือประมวลผลตามหน้าที่
เมื่อทำงานของตัวเองสำเร็จ คืนผลและหยุด อาจเรียกขั้นถัดไป
เมื่อไม่ผ่านเงื่อนไข อาจส่งต่อให้ผู้รับผิดชอบอื่น อาจยุติสายทันที
ตัวอย่าง อนุมัติตามวงเงิน, ส่งเรื่องเจ้าหน้าที่ authentication, authorization, request filters

ตัวอย่างอนุมัติในบทความเป็นแบบแรก ส่วน filter chain อาจให้หลาย filter ทำงานกับคำขอเดียว แต่แต่ละ filter ยังมีสิทธิ์หยุดสายตามกติกา

เอกสาร Spring Security อธิบายว่า Servlet Filter สามารถป้องกันไม่ให้ filter ถัดไปหรือ servlet ถูกเรียก หรือปรับ request/response แล้วส่งต่อได้ [2] ส่วน Jakarta FilterChain.doFilter() ใช้เรียก filter ถัดไป หรือทรัพยากรปลายสายเมื่อไม่มี filter ถัดไป [3]

ในเชิงการออกแบบ การส่งต่อและการหยุดเหล่านี้ใช้แนวคิดความรับผิดชอบเป็นสายได้ แต่ไม่ได้หมายความว่า filter chain ทุกระบบต้องมีโครงสร้างคลาสเหมือนตัวอย่างอนุมัตินี้

#10. ข้อดีและข้อแลกเปลี่ยน

ประเด็น ผลต่อการออกแบบ
ลดการผูกติด Client พึ่งพา Handler interface แทนผู้รับผิดชอบแต่ละประเภท
แยกความรับผิดชอบ กฎของแต่ละผู้รับคำขออยู่ในส่วนที่เกี่ยวข้อง
เพิ่มหรือจัดลำดับได้ เปลี่ยนการประกอบสายได้โดยไม่แก้โค้ดผู้ส่งคำขอ
ติดตามเส้นทางยากขึ้น ต้องรู้ว่าคำขอผ่าน Handler ใดและหยุดที่ใด
ไม่มีผู้รับประกันผล คำขออาจถึงปลายสาย จึงต้องมี fallback ชัดเจน
ลำดับเปลี่ยนผลลัพธ์ Handler ที่กฎครอบคลุมกว้างอาจรับคำขอทั้งหมดก่อนตัวอื่น
จำนวนออบเจ็กต์เพิ่ม สำหรับกฎเล็กและคงที่ โค้ดอาจซับซ้อนเกินความจำเป็น

สำหรับสายที่มี Handler จำนวน n ตัว เวลาการส่งต่อกรณีเลวร้ายสุดเป็น O(n) โดยยังไม่รวมต้นทุนงานของแต่ละ Handler ตัวอย่างนี้เรียกซ้อนผ่าน next.handle() จึงอาจใช้ call stack ตามจำนวน Handler ที่ผ่าน และต้องระวังหากสายยาวมาก

#11. เปรียบเทียบกับ Pattern ใกล้เคียง

Pattern คำถามหลักที่ Pattern ตอบ
Chain of Responsibility ใครจะรับผิดชอบคำขอนี้ และจะส่งต่อหรือหยุดเมื่อใด?
Strategy จะเลือกวิธีหรืออัลกอริทึมใดมาทำงาน?
Decorator จะห่อออบเจ็กต์เพื่อเพิ่มพฤติกรรมอย่างไร?
Command จะเก็บการเรียกงานเป็นออบเจ็กต์อย่างไร?
Observer เมื่อมีเหตุการณ์ จะส่งการแจ้งเตือนไปยังผู้ติดตามใดบ้าง?

Chain of Responsibility กับ Decorator อาจมีรูปร่างเป็นออบเจ็กต์ต่อกันคล้ายกัน แต่เจตนาต่างกัน: แบบแรกเน้นการเลือกผู้รับผิดชอบและการส่งต่อ ส่วนแบบหลังเน้นเพิ่มพฤติกรรมด้วยการห่อออบเจ็กต์

#12. กรณีทดสอบที่ควรตรวจ

เริ่มทดสอบที่ขอบเขตวงเงิน เพราะเงื่อนไข < กับ <= ให้ผลต่างกันอย่างมีนัยสำคัญ

จำนวนเงินหรือข้อมูลเข้า ผลที่คาดหวัง
0.01 APPROVED โดย TeamLead
5,000 APPROVED โดย TeamLead
5,000.01 APPROVED โดย Manager
20,000 APPROVED โดย Manager
20,000.01 APPROVED โดย Director
100,000 APPROVED โดย Director
100,000.01 UNHANDLED
0 หรือค่าติดลบ IllegalArgumentException ตอนสร้างคำขอ
รหัสว่าง IllegalArgumentException ตอนสร้างคำขอ
รหัสหรือจำนวนเงินเป็น null NullPointerException ตอนสร้างคำขอ
chain.handle(null) NullPointerException

นอกจากผลลัพธ์สุดท้าย ควรตรวจพฤติกรรม หยุดสาย ด้วย เช่น ส่ง 3,000 บาทเข้า TeamLead แล้วพิสูจน์ว่า Handler ถัดไปไม่ถูกเรียก โดยใช้ตัวทดแทนที่นับจำนวนการเรียกหรือโยนข้อผิดพลาดหากถูกเรียก

#13. ข้อผิดพลาดที่พบบ่อย

  • ประกอบสายแล้วเรียกผิดจุด: ต้องเรียก Handler ตัวแรก มิฉะนั้นอาจข้ามกฎบางระดับ
  • อนุมัติแล้วส่งต่ออีก: ทำให้หลายตัวจัดการรายการเดียวโดยไม่ตั้งใจ
  • ลืมคืนผลจากตัวถัดไป: ใช้ return next.handle(request) เมื่อเมธอดมีผลลัพธ์
  • ไม่มีนโยบายปลายสาย: กำหนดสถานะ ข้อผิดพลาด หรือ fallback ให้ผู้เรียกจัดการได้
  • ต่อสายวนกลับ: หากใช้ setter เชื่อม Handler ต้องป้องกัน cycle ไม่เช่นนั้นอาจเรียกซ้ำจน stack เต็ม
  • เก็บคำขอใน field ของ Handler: อาจทำให้ข้อมูลข้ามคำขอหรือเกิดปัญหาเมื่อใช้พร้อมกันหลายเธรด
  • นำ exception ทุกชนิดไปตีความว่าให้ส่งต่อ: ต้องแยก “ไม่ใช่ผู้รับผิดชอบ” ออกจาก “ระบบทำงานล้มเหลว” เพราะมีความหมายต่างกัน

ในระบบจริงควรบันทึก request ID, ชื่อ Handler, เหตุผลที่ส่งต่อ และผลสุดท้าย เพื่ออธิบายเส้นทางของคำขอได้ การเพิ่มฐานข้อมูลหรือบริการภายนอกยังต้องออกแบบธุรกรรมและการป้องกันการอนุมัติซ้ำเพิ่มเติม

#14. แบบฝึกหัด

  1. เพิ่ม DeputyDirector แล้วทดลองยอด 35,000 บาท พร้อมอธิบายว่าทำไมต้องวางก่อน Director
  2. สลับให้ Director เป็นตัวแรก แล้วเปรียบเทียบผลของรายการ 3,000 บาท
  3. เพิ่มสถานะ MANUAL_REVIEW และออกแบบ Handler ปลายสายที่ส่งคำขอเข้าสู่การตรวจด้วยเจ้าหน้าที่
  4. เพิ่ม trace สำหรับแต่ละคำขอ โดยไม่เก็บ trace เป็น field ที่เปลี่ยนแปลงได้ของ Handler ที่ใช้ร่วมกัน
  5. ออกแบบสายตรวจ API ซึ่งตรวจข้อมูล การยืนยันตัวตน และสิทธิ์การเข้าถึง แล้วระบุว่าขั้นใดต้องส่งต่อและขั้นใดต้องหยุด

#15. ประเด็นสำคัญ

Chain of Responsibility ทำให้ผู้ส่งคำขอเรียกผ่านสัญญากลาง ขณะที่ Handler แต่ละตัวเป็นผู้ตัดสินใจว่าจะรับผิดชอบหรือส่งต่อ ความถูกต้องของการออกแบบขึ้นอยู่กับ กติกาการหยุด ลำดับของสาย และผลลัพธ์เมื่อไม่มีผู้รับผิดชอบ

สำหรับตัวอย่าง Java นี้ ApprovalHandler เป็นสัญญากลาง Approver จัดการตรรกะร่วม และผู้อนุมัติแต่ละระดับกำหนดวงเงินของตนเอง ผู้เรียกส่งคำขอครั้งเดียวและได้รับผลลัพธ์ที่ระบุผู้อนุมัติหรือสถานะ UNHANDLED อย่างชัดเจน

#แหล่งอ้างอิงและอ่านเพิ่มเติม

  1. Oracle Java SE 17 — BigDecimal — การเก็บทศนิยมและความต่างของ compareTo() กับ equals()
  2. Spring Security — Architecture — บทบาทของ Servlet Filter และการส่งต่อหรือยุติสาย
  3. Jakarta EE Web Profile API — FilterChain — สัญญาของ doFilter()