info-observer-pattern-with-java

รูปแบบการออกแบบเชิงพฤติกรรมสำหรับแจ้งเหตุการณ์จากแหล่งข้อมูลหนึ่งแห่งไปยังผู้รับหลายราย พร้อม Workshop Java ที่รันได้โดยไม่ต้องติดตั้งไลบรารีเพิ่มเติม

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

Observer Pattern เป็น Behavioral Design Pattern ที่กำหนดความสัมพันธ์แบบ หนึ่งต่อหลาย (one-to-many) ระหว่างวัตถุต้นทางกับวัตถุที่สนใจ เมื่อเกิดเหตุการณ์หรือสถานะเปลี่ยน ต้นทางจะเรียกเมธอดแจ้งเตือนผู้รับที่ลงทะเบียนไว้

ส่วนประกอบหลักมีสองฝ่าย:

  • Subject หรือ Publisher เป็นแหล่งข้อมูล เก็บรายชื่อผู้รับ และส่งการแจ้งเตือน
  • Observer หรือ Subscriber ลงทะเบียนรับเหตุการณ์และกำหนดว่าจะทำอะไรเมื่อได้รับข้อมูล

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

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

เป้าหมายการเรียนรู้: อธิบายบทบาทของ Subject/Observer ได้ สร้างระบบ subscribe/unsubscribe ได้ และระบุข้อจำกัดเรื่องข้อผิดพลาด ลำดับการแจ้งเตือน และการทำงานหลายเธรดได้

#2. ปัญหาที่ Pattern นี้ช่วยแก้

สมมติระบบเริ่มต้นด้วยการให้ NewsAgency เรียกบริการที่เป็นรูปธรรมโดยตรง:

// ตัวอย่างแนวคิดก่อนปรับโครงสร้าง ไม่ใช่โปรแกรมเต็ม
void publish(News news) {
    emailService.send(news);
    mobileService.send(news);
    dashboard.refresh(news);
}

เมื่อเพิ่มช่องทางใหม่ เช่น ระบบบันทึกประวัติหรือหน้าจอประชาสัมพันธ์ นักพัฒนาต้องกลับมาแก้ publish() ซ้ำ และต้นทางต้องรู้จักบริการจำนวนมากขึ้น

Observer Pattern เปลี่ยนให้ต้นทางเรียกผ่าน interface เดียวกัน ผู้รับชนิดใหม่จึงเพิ่มเข้ามาด้วยการ implement interface และลงทะเบียน โดยไม่ต้องเพิ่มคำสั่งเฉพาะช่องทางในสำนักข่าว

การออกแบบนี้ช่วยลด coupling ระหว่างต้นทางกับชนิดผู้รับ และสนับสนุน Open/Closed Principle ในส่วนของการเพิ่มผู้รับ อย่างไรก็ตาม ต้นทางยังถือ reference ของผู้รับที่ลงทะเบียนไว้ จึงยังมีความสัมพันธ์ระหว่างวัตถุอยู่

#3. บทบาทในตัวอย่าง

บทบาทของ Pattern องค์ประกอบ Java หน้าที่
Subject NewsSubject กำหนดสัญญาสำหรับสมัครและยกเลิกรับข่าว
Concrete Subject NewsAgency เก็บสมาชิก เก็บข่าวล่าสุด และแจ้งข่าวใหม่
Observer NewsObserver กำหนด callback update(News news)
Concrete Observer EmailSubscriber จำลองการรับข่าวผ่านอีเมล
Concrete Observer MobileSubscriber จำลองการรับข่าวผ่านมือถือ
Event/Data News เก็บหัวข้อและเนื้อหาของข่าวหนึ่งรายการ
Client ObserverDemo.main() สร้างวัตถุและเชื่อมความสัมพันธ์

ตัวอย่างนี้พิมพ์ข้อความลง console เพื่อจำลองช่องทางแจ้งเตือน ไม่มีการส่งอีเมลหรือ push notification จริง

#4. โครงสร้างคลาส

classDiagram
    direction TB
    class NewsSubject {
        <<interface>>
        +subscribe(NewsObserver observer) void
        +unsubscribe(NewsObserver observer) void
    }
    class NewsAgency {
        -Set observers
        -News latestNews
        +publish(News news) void
        +getLatestNews() Optional~News~
        -notifyObservers(News news) void
    }
    class NewsObserver {
        <<interface>>
        +update(News news) void
    }
    class EmailSubscriber {
        +update(News news) void
    }
    class MobileSubscriber {
        +update(News news) void
    }
    class News {
        <<record>>
        +String title
        +String content
    }

    NewsSubject <|.. NewsAgency
    NewsObserver <|.. EmailSubscriber
    NewsObserver <|.. MobileSubscriber
    NewsAgency o-- "0..*" NewsObserver : registers
    NewsAgency --> News : publishes
    NewsObserver ..> News : receives

จุดสำคัญคือ NewsAgency เก็บสมาชิกชนิด NewsObserver จึงไม่ต้องตรวจว่าแต่ละรายเป็น EmailSubscriber หรือ MobileSubscriber

#5. Workshop: สร้างระบบประกาศข่าว

#สิ่งที่ต้องเตรียม

  • JDK 17 หรือใหม่กว่า ตัวอย่างใช้ record และไม่ใช้ preview feature
  • โปรแกรมแก้ไขข้อความหรือ IDE
  • Terminal, PowerShell หรือ Command Prompt

ตรวจสอบ Java ที่ติดตั้ง:

java -version
javac -version

ต้องมี JDK ที่มี compiler หากรัน java ได้แต่ไม่พบ javac ให้ตรวจการติดตั้ง JDK และการกำหนด PATH

#สร้างไฟล์ ObserverDemo.java

คัดลอกโค้ดทั้งหมดนี้ลงไฟล์เดียว ชื่อไฟล์ต้องตรงกับคลาส public class ObserverDemo

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class ObserverDemo {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();
        NewsObserver email = new EmailSubscriber("Alice");
        NewsObserver mobile = new MobileSubscriber("Bob");

        agency.subscribe(email);
        agency.subscribe(mobile);
        agency.subscribe(email); // Same observer: no duplicate registration.

        agency.publish(new News(
                "Java Workshop", "Registration is open."));

        agency.unsubscribe(mobile);

        agency.publish(new News(
                "Design Patterns", "Observer lesson is available."));
    }
}

record News(String title, String content) {
    News {
        Objects.requireNonNull(title, "title");
        Objects.requireNonNull(content, "content");
    }
}

@FunctionalInterface
interface NewsObserver {
    void update(News news);
}

interface NewsSubject {
    void subscribe(NewsObserver observer);
    void unsubscribe(NewsObserver observer);
}

final class NewsAgency implements NewsSubject {
    private final Set<NewsObserver> observers = new LinkedHashSet<>();
    private News latestNews;

    @Override
    public void subscribe(NewsObserver observer) {
        observers.add(Objects.requireNonNull(observer, "observer"));
    }

    @Override
    public void unsubscribe(NewsObserver observer) {
        observers.remove(Objects.requireNonNull(observer, "observer"));
    }

    public Optional<News> getLatestNews() {
        return Optional.ofNullable(latestNews);
    }

    public void publish(News news) {
        latestNews = Objects.requireNonNull(news, "news");
        notifyObservers(news);
    }

    private void notifyObservers(News news) {
        // Snapshot: membership changes affect the next publication.
        for (NewsObserver observer : List.copyOf(observers)) {
            observer.update(news);
        }
    }
}

final class EmailSubscriber implements NewsObserver {
    private final String name;

    EmailSubscriber(String name) {
        this.name = Objects.requireNonNull(name, "name");
    }

    @Override
    public void update(News news) {
        System.out.printf("[Email:%s] %s - %s%n",
                name, news.title(), news.content());
    }
}

final class MobileSubscriber implements NewsObserver {
    private final String name;

    MobileSubscriber(String name) {
        this.name = Objects.requireNonNull(name, "name");
    }

    @Override
    public void update(News news) {
        System.out.printf("[Mobile:%s] %s - %s%n",
                name, news.title(), news.content());
    }
}

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

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

javac --release 17 -encoding UTF-8 ObserverDemo.java
java ObserverDemo

หรือใช้ source-file mode ของ JDK โดยไม่ต้องคอมไพล์แยก:

java ObserverDemo.java

#ผลลัพธ์

[Email:Alice] Java Workshop - Registration is open.
[Mobile:Bob] Java Workshop - Registration is open.
[Email:Alice] Design Patterns - Observer lesson is available.

ข่าวแรกถูกส่งให้ Alice และ Bob ส่วนข่าวที่สองส่งให้ Alice เพียงคนเดียว เพราะ Bob ยกเลิกการรับข่าวแล้ว การสมัคร email ซ้ำไม่ได้ทำให้ Alice ได้ข่าวซ้ำใน implementation นี้

#6. อธิบายการทำงานทีละขั้น

  1. main() สร้างสำนักข่าวและสมาชิกสองราย
  2. subscribe() เพิ่มสมาชิกลง LinkedHashSet ซึ่งคงลำดับการลงทะเบียนและไม่เก็บสมาชิกที่เท่ากันซ้ำ
  3. publish() บันทึกข่าวล่าสุดก่อนเรียก notifyObservers()
  4. notifyObservers() สร้าง snapshot ของสมาชิก และเรียก update(news) ของสมาชิกแต่ละราย
  5. สมาชิกแต่ละรายใช้ข้อมูลใน News ตามหน้าที่ของตนเอง
  6. เมื่อเรียก unsubscribe(mobile) ข่าวที่เผยแพร่หลังจากนั้นจะไม่ส่งให้สมาชิกมือถือรายนี้

เหตุผลที่แยก News ออกมา: ผู้รับได้รับข้อมูลของเหตุการณ์ผ่านชนิดข้อมูลที่ชัดเจน ไม่ต้อง cast จาก Object และไม่ต้องรู้รายละเอียดภายในของสำนักข่าว

News เป็น record ที่มีองค์ประกอบชนิด String ทำให้ payload ของตัวอย่างนี้ไม่ถูกแก้ค่าระหว่างส่งต่อ อย่างไรก็ตาม record ที่บรรจุวัตถุ mutable ไม่ได้ทำให้วัตถุที่อยู่ข้างใน immutable โดยอัตโนมัติ

#พฤติกรรมที่กำหนดไว้ในตัวอย่าง

สถานการณ์ พฤติกรรม
ยังไม่มีสมาชิก บันทึกข่าวล่าสุดได้ แต่ไม่มี callback
สมัคร observer เดิมซ้ำ เก็บเพียงครั้งเดียว โดยพิจารณาตาม equals()/hashCode()
สร้างสมาชิกใหม่ที่มีชื่อเหมือนกัน ยังเป็นคนละ object และลงทะเบียนแยกได้ เพราะคลาสสมาชิกไม่ได้ override equality
ยกเลิกสมาชิกที่ไม่ได้ลงทะเบียน ไม่มีการเปลี่ยนแปลง
สมัครหลังเคยมีข่าวแล้ว ไม่ส่งข่าวย้อนหลังให้อัตโนมัติ
ส่งข่าวเนื้อหาเดิมอีกครั้ง แจ้งเตือนอีกครั้ง เพราะทุกการเรียก publish() ถือเป็นการเผยแพร่
เรียก getLatestNews() ก่อนมีข่าว ได้ Optional.empty()
ผู้รับเพิ่มหรือลบสมาชิกระหว่าง callback snapshot ปัจจุบันยังคงเดิม รายชื่อใหม่มีผลกับการเผยแพร่ครั้งถัดไป
ผู้รับ throw unchecked exception การแจ้งเตือนรอบนั้นหยุด และ exception ส่งกลับไปยังผู้เรียก

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

#เหตุใดต้องใช้ snapshot

หากวนลูปบน collection ต้นฉบับ แล้วผู้รับยกเลิกตนเองระหว่าง update() อาจเกิด ConcurrentModificationException ได้ การใช้ List.copyOf(observers) ช่วยให้ callback เปลี่ยนสมาชิกได้ระหว่างการแจ้งเตือนแบบเธรดเดียว

ผลที่ตามมาคือ หากสมาชิก A ลบสมาชิก B ระหว่าง callback และ B อยู่ใน snapshot แล้ว B ยังได้รับข่าวรอบนั้นอยู่ การยกเลิกมีผลกับรอบถัดไป

snapshot นี้ไม่ได้ทำให้ NewsAgency thread-safe การแก้ collection พร้อมกับสร้าง snapshot จากอีกเธรดยังไม่ปลอดภัย และโค้ดนี้ยังไม่มีนโยบายจัดลำดับเหตุการณ์ระหว่างเธรด

#7. เพิ่ม Observer ด้วย Lambda

NewsObserver มี abstract method เพียงหนึ่งเมธอด จึงใช้ lambda ได้ เพิ่มตัวอย่างต่อไปนี้ใน main() หลังสร้าง agency และก่อนเผยแพร่ข่าว:

NewsObserver audit = news ->
        System.out.println("[Audit] " + news.title());

agency.subscribe(audit);
// เรียก agency.publish(...) ตามปกติ

// เมื่อเลิกใช้งาน ให้ยกเลิกด้วย reference เดิม
agency.unsubscribe(audit);

หากต้องการทดลองรับข่าวจริง ให้มีการเรียก publish() ระหว่าง subscribe(audit) กับ unsubscribe(audit) การสมัครแล้วถอนทันทีจะยังไม่ได้รับข่าวใด

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

#8. Push Model กับ Pull Model

ประเด็น Push Model Pull Model
สิ่งที่ส่งเข้า callback ส่ง payload ที่เกี่ยวข้อง เช่น News ส่งสัญญาณหรือ reference ของต้นทาง แล้วผู้รับอ่านข้อมูลเพิ่ม
การเข้าถึงข้อมูล ใช้ข้อมูลที่ได้รับได้ทันที เรียก getter หรือ query ของต้นทาง
ข้อดี เหตุการณ์ระบุข้อมูลชัดเจน ผู้รับไม่ต้องรู้ API อ่านสถานะของต้นทางมากนัก ผู้รับเลือกอ่านเฉพาะข้อมูลที่ต้องการได้
สิ่งที่ต้องคิด ขนาดและรูปแบบ payload ต้นทางอาจเปลี่ยนสถานะไปแล้วก่อนอ่าน โดยเฉพาะงาน asynchronous

ตัวอย่างหลักเป็น Push Model เพราะส่ง News เข้า update() โดยตรง แม้จะมี getLatestNews() ให้ query ได้ แต่ callback ไม่จำเป็นต้องเรียกเมธอดนั้น

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

#9. ทางเลือกใน JDK: PropertyChangeSupport

สำหรับวัตถุที่ต้องแจ้งการเปลี่ยนแปลง property เช่น สถานะคำสั่งซื้อ PropertyChangeSupport ช่วยจัดการ listener และส่ง PropertyChangeEvent ที่มีชื่อ property ค่าเดิม และค่าใหม่ สามารถลงทะเบียนเฉพาะ property ที่สนใจได้ [2]

สร้างไฟล์ PropertyChangeDemo.java:

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Objects;

final class Order {
    private final PropertyChangeSupport changes =
            new PropertyChangeSupport(this);
    private String status = "NEW";

    public void addStatusListener(PropertyChangeListener listener) {
        changes.addPropertyChangeListener("status", listener);
    }

    public void removeStatusListener(PropertyChangeListener listener) {
        changes.removePropertyChangeListener("status", listener);
    }

    public void setStatus(String newStatus) {
        Objects.requireNonNull(newStatus, "newStatus");
        String oldStatus = status;
        status = newStatus;
        changes.firePropertyChange("status", oldStatus, newStatus);
    }
}

public class PropertyChangeDemo {
    public static void main(String[] args) {
        Order order = new Order();

        PropertyChangeListener listener = event ->
                System.out.printf("%s: %s -> %s%n",
                        event.getPropertyName(),
                        event.getOldValue(), event.getNewValue());

        order.addStatusListener(listener);
        order.setStatus("PAID");
        order.setStatus("PAID"); // Equal values: no event.
        order.removeStatusListener(listener);
        order.setStatus("SHIPPED"); // Listener already removed.
    }
}

รันด้วยคำสั่ง:

javac --release 17 -encoding UTF-8 PropertyChangeDemo.java
java PropertyChangeDemo

ผลลัพธ์มีเพียงหนึ่งบรรทัด:

status: NEW -> PAID

การตั้งค่า PAID ซ้ำไม่ส่ง event เพราะค่าเดิมและค่าใหม่เท่ากัน ส่วน SHIPPED เกิดหลังยกเลิก listener แล้ว [2] พฤติกรรมนี้ต่างจาก NewsAgency ซึ่งแจ้งทุกครั้งที่เรียก publish()

PropertyChangeSupport อยู่ใน module java.desktop หากโครงการใช้ module-info.java ต้องประกาศ requires java.desktop; ส่วนตัวอย่างข้างต้นรันบน classpath จึงไม่ต้องสร้างไฟล์ module เพิ่ม

แม้ตัวช่วย PropertyChangeSupport จะ thread-safe ตามเอกสาร JDK แต่สถานะ status และการเปลี่ยนสถานะของ Order ในตัวอย่างยังไม่ได้ออกแบบให้ปลอดภัยต่อการเรียกพร้อมกันหลายเธรด [2]

#10. เหตุใดจึงไม่ใช้ java.util.Observer และ Observable

java.util.Observer และ java.util.Observable ถูกประกาศ deprecated ตั้งแต่ Java 9 เอกสาร JDK ระบุข้อจำกัดของ event model และการแจ้งเตือน พร้อมแนะนำให้พิจารณา java.beans, เครื่องมือใน java.util.concurrent หรือ Flow ตามลักษณะงาน [1]

สำหรับการเรียนรู้และ event ภายในโปรแกรมขนาดเล็ก การสร้าง interface ที่ระบุชนิดข้อมูลชัดเจนอย่าง NewsObserver ทำให้กำหนดนโยบายของระบบได้ตรงกว่า ตัว Pattern ยังคงใช้ได้ แม้ API เก่าจะ deprecated แล้ว

หากต้องการ Reactive Streams พร้อมการควบคุมจำนวนข้อมูลที่ผู้รับพร้อมรับ สามารถศึกษา Flow.Publisher, Flow.Subscriber และ Flow.Subscription.request(n) เพิ่มเติม [3] การเพิ่ม interface ชื่อ Observer เพียงอย่างเดียวไม่ได้ทำให้ระบบมี backpressure

#11. Observer เทียบกับ Publish–Subscribe และ Pattern ใกล้เคียง

แนวคิด ความสัมพันธ์ที่เน้น ตัวอย่าง
Observer ต้นทางจัดการผู้รับหรือ listener แล้วแจ้งผ่าน callback วัตถุข้อมูลแจ้งหน้าจอหลายหน้าจอ
Publish–Subscribe ผู้ส่งและผู้รับเชื่อมผ่านช่องทาง เช่น topic หรือ event bus; อาจมี broker บริการเผยแพร่เหตุการณ์ให้บริการอื่น
Mediator ตัวกลางกำหนดกติกาการสื่อสารและประสานงานระหว่างวัตถุ ฟอร์มที่ปุ่มและช่องกรอกมีผลต่อกัน
Strategy เลือกอัลกอริทึมหรือพฤติกรรมที่ใช้ทำงานหนึ่งอย่าง เปลี่ยนวิธีคำนวณค่าจัดส่ง

Observer กับ Publish–Subscribe มีแนวคิดที่ทับซ้อนกันได้ คำเรียกไม่ได้รับประกันรูปแบบ deployment หรือ thread model ต้องดู implementation จริงว่าผู้ส่งรู้จักผู้รับหรือไม่ มีตัวกลางหรือไม่ และรับประกันการส่งข้อมูลอย่างไร

#12. ข้อดีและข้อจำกัด

#ข้อดี

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

#สิ่งที่ต้องออกแบบเพิ่มเมื่อนำไปใช้จริง

1. งาน synchronous และผู้รับที่ช้า

ในตัวอย่าง publish() เรียก callback ทีละรายบนเธรดเดียวกัน จึงต้องรอให้ทุก callback ที่ถูกเรียกทำงานเสร็จก่อนคืนค่า หากผู้รับรายหนึ่งใช้เวลานาน จะกระทบผู้รับที่อยู่ถัดไปและผู้เรียกต้นทาง ควรออกแบบ queue/executor เพิ่มเมื่อมีเหตุผลให้แยกงาน พร้อมกำหนดขนาดคิว การจัดลำดับ และนโยบายเมื่อคิวเต็ม

2. ข้อผิดพลาดของผู้รับ

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

3. อายุของการสมัครรับข้อมูล

ต้นทางถือ strong reference ของผู้รับ ผู้รับที่เลิกใช้งานแล้วแต่ยังไม่ unsubscribe อาจยังถูกเก็บในหน่วยความจำและรับ event ต่อไป ควรผูกการยกเลิกกับอายุของหน้าจอ session หรือ component

4. ลำดับและ reentrancy

ตัวอย่างใช้ลำดับการลงทะเบียนจาก LinkedHashSet แต่ Observer Pattern ไม่ได้กำหนดลำดับสากล และไม่ควรให้ผู้รับหนึ่งรายต้องพึ่งผลของผู้รับอีกคนโดยไม่มีสัญญาที่ชัดเจน หาก callback เรียก publish() ซ้อน จะเกิดการส่งข่าวซ้อนและอาจวนซ้ำไม่สิ้นสุด snapshot ไม่ได้ป้องกันปัญหานี้

5. การทำงานข้ามเธรดหรือข้ามบริการ

โค้ดในบทความออกแบบสำหรับเธรดเดียว ไม่รับประกัน thread safety การส่งซ้ำ การ retry หรือการส่งถึงทุกผู้รับแบบ durable เมื่อย้ายไปทำงาน asynchronous ต้องกำหนดสัญญาเหล่านี้เพิ่มตามความต้องการ

6. ตำแหน่งของเหตุการณ์กับ transaction

หาก event หมายถึงข้อมูลถูกบันทึกสำเร็จแล้ว ต้องกำหนดให้การแจ้งเกิดสัมพันธ์กับจังหวะ commit ที่ถูกต้อง การเรียก callback ก่อน transaction สำเร็จอาจทำให้ผู้รับตอบสนองต่อข้อมูลที่ถูก rollback ภายหลัง

#13. ควรใช้เมื่อใด

เหมาะเมื่อข้อมูลหนึ่งแห่งมีผู้รับหลายรายที่เพิ่มหรือลดได้ และผู้รับควรรับผิดชอบการตอบสนองของตนเอง เช่น model แจ้ง UI, การติดตามสถานะภายในแอป หรือการกระจาย domain event ภายใน process

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

#14. แบบฝึกหัดต่อยอด

  1. เพิ่ม DashboardSubscriber ให้แสดงเฉพาะหัวข้อข่าว โดยไม่แก้ NewsAgency ตรวจว่าผู้รับเดิมยังทำงานได้
  2. เพิ่มผู้รับที่กรองข่าว ให้แสดงเฉพาะหัวข้อที่มีคำว่า Java และให้การกรองเป็นหน้าที่ของผู้รับรายนั้น
  3. ทดลองยกเลิกตนเองใน callback แล้วเผยแพร่สองข่าว ตรวจว่าผู้รับได้ข่าวแรกเพียงครั้งเดียวและไม่ได้ข่าวที่สอง
  4. เปลี่ยนนโยบายข้อผิดพลาด ให้ผู้รับรายถัดไปยังได้รับข่าวเมื่อผู้รับก่อนหน้าเกิด exception พร้อมรายงานว่ารายใดล้มเหลว
  5. วิเคราะห์งาน asynchronous ระบุว่าจะจัดการคิวเต็ม การยกเลิกสมาชิกที่มีงานค้าง และลำดับข่าวของสมาชิกคนเดียวกันอย่างไร ก่อนเริ่มเขียนโค้ด

#15. ประเด็นทบทวน

  • Observer กำหนดความสัมพันธ์หนึ่งต่อหลาย แต่ไม่ได้ทำให้งานเป็น asynchronous โดยอัตโนมัติ
  • interface ช่วยให้ต้นทางไม่ผูกกับคลาสผู้รับที่เฉพาะเจาะจง
  • subscribe/unsubscribe เป็นส่วนหนึ่งของการจัดการอายุวัตถุ
  • snapshot ช่วยเรื่องการเปลี่ยนสมาชิกระหว่าง callback แบบเธรดเดียว แต่ไม่ได้แทนการออกแบบ concurrency
  • การส่งข่าวซ้ำ ข้อผิดพลาด ลำดับ และการส่งข่าวย้อนหลังเป็นนโยบายที่ต้องกำหนดเอง

#การตรวจสอบตัวอย่าง

คอมไพล์โค้ดเต็มทั้งสองชุดด้วยระดับภาษา Java 17 และทดลองรันบน OpenJDK 17 แล้ว ผลลัพธ์ตรงกับที่แสดงในบทความ รวมถึงการรัน ObserverDemo.java ด้วย source-file mode

#แหล่งอ้างอิง

อ้างอิง API ของ Java SE 17 เพื่อให้ตรงกับระดับภาษาที่ใช้ใน Workshop ไม่ได้อ้างว่าเป็น Java รุ่นล่าสุด

  1. Oracle. Observable — Java SE 17 API. สถานะ deprecated และข้อจำกัดของ API รุ่นเดิม
  2. Oracle. PropertyChangeSupport — Java SE 17 API. การจัดการ listener, property event และพฤติกรรมเมื่อค่าเดิมเท่ากับค่าใหม่
  3. Oracle. Flow — Java SE 17 API. Publisher, Subscriber และ demand ผ่าน Subscription