နားလည်ထားရမယ့် အချက်
naive notification system တစ်ခုမှာ 'course completed' ကို detect လုပ်တဲ့ code ကိုယ်တိုင်က send_email() ကို ခေါ်ပြီး၊ ဆက်ပြီး send_push() နဲ့ write_in_app_notification() ကို အစဉ်လိုက် ခေါ်တယ်။ အစပိုင်းမှာ အလုပ်လုပ်ပေမယ့် ဒါက event source ကို consumer တစ်ခုချင်းစီနဲ့ ချုပ်နှောင်ထားလိုက်တာဖြစ်တယ် - နောက်ပိုင်း SMS notification ထပ်ထည့်ချင်ရင် ဒီ function ကိုပဲ ထပ်ပြင်ရမယ်၊ ပြီးတော့ push-notification code ထဲက bug တစ်ခုက ဆက်ပြီးလာမယ့် email send ကို block ဒါမှမဟုတ် crash ဖြစ်စေနိုင်တယ်။ publish-subscribe pattern က ဒါကို indirection layer တစ်ခု ထည့်သွင်းပြီး ဖြေရှင်းတယ် - event source က payload တစ်ခုပါတဲ့ 'course_completed' event ကိုသာ publish လုပ်ပြီး ဘယ်သူက listen လုပ်နေလဲ ဒါမှမဟုတ် ဘယ်သူမှ listen မလုပ်ဘူးလား ဆိုတာ လုံးဝသိစရာမလိုဘူး။ subscriber handler ဘယ်နှစ်ခုမဆို ဒီ event type ကို ကြိုတင်ပြီး interest register လုပ်ထားနိုင်တယ်၊ event ဖြစ်ပွားလာတဲ့အခါ handler တစ်ခုချင်းစီက တခြားတွေကို မသိဘဲ သူ့ဟာသူ run တယ်။ ဒါကတော့ Kafka topic, RabbitMQ exchange, SQS+SNS fan-out လို production message queue တွေက scale ကြီးမှာ သုံးနေတဲ့ pattern အတိုင်းပဲဖြစ်ပြီး subscriber တွေကို သီးခြား worker process ဒါမှမဟုတ် service အဖြစ်တောင် run နိုင်တယ်။ ဒီ design က လက်ခံလိုက်တဲ့ trade-off ကတော့ indirection ဖြစ်ပြီး distributed version မှာဆိုရင် eventual consistency နဲ့ at-least-once delivery ပြဿနာတွေပါ ရှိလာတတ်တယ် - ဒါပေမယ့် event ဖြစ်ပွားတာနဲ့ ဒါကို တုံ့ပြန်တဲ့အရာတွေကြားက ချုပ်နှောင်မှု လုံးဝမရှိတော့တာကို ရရှိလိုက်တယ်။
လက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
ဒါဟာ Tutorial Platform က social feature ထပ်ထည့်တဲ့အခိုက်အတန့်မှာ တကယ်လိုအပ်လာမယ့်အရာနဲ့ အတော်လေး နီးစပ်တယ် - learner တစ်ယောက်က tutorial တစ်ခု ပြီးမြောက်တဲ့အခါ ဒီ learner ကိုယ်တိုင် (congratulations email) နဲ့ သူ့ followers တွေ (activity feed entry) ကို notify လုပ်ရမယ်၊ course-completion-tracking code ကတော့ email template ဒါမှမဟုတ် feed formatting အကြောင်း ဘာမှသိစရာမလိုဘူး။ naive version လိုမျိုး completion detection ကို send_email() နဲ့ တိုက်ရိုက်ချိတ်ထားရင် နောက်လာမယ့် notification channel တိုင်း - push notification, study group အတွက် Slack webhook, in-app toast စသည်တို့ - core completion logic ကို ပြန်ပြင်ပြီး ပြန်စမ်းသပ်ရမယ်။ pub-sub နဲ့ဆိုရင်တော့ channel အသစ်ထပ်ထည့်ခြင်းဆိုတာ subscriber အသစ်ကို register လုပ်ရုံပဲ - publisher နဲ့ ရှိပြီးသား subscriber တွေက ဘယ်တော့မှ ပြောင်းလဲစရာမလိုဘူး။
အတူတူ စမ်းရေးကြည့်မယ်
from collections import defaultdict
class PubSub:
def __init__(self):
self._subscribers = defaultdict(list) # event_type -> list of handlers
def subscribe(self, event_type: str, handler):
self._subscribers[event_type].append(handler)
def publish(self, event_type: str, payload: dict):
for handler in self._subscribers[event_type]:
handler(payload)
def send_email(payload: dict):
print(f"[email] Sending congratulations to {payload['user']} for finishing '{payload['course']}'")
def log_notification(payload: dict):
print(f"[log] {payload['user']} completed '{payload['course']}' at event time")
def notify_followers(payload: dict):
for follower in payload.get("followers", []):
print(f"[activity-feed] Telling {follower}: {payload['user']} just finished '{payload['course']}'")
if __name__ == "__main__":
bus = PubSub()
bus.subscribe("course_completed", send_email)
bus.subscribe("course_completed", log_notification)
bus.subscribe("course_completed", notify_followers)
bus.publish("course_completed", {
"user": "thiha",
"course": "System Design",
"followers": ["mya", "kaung"],
})
'course_completed' event တစ်ခု publish လုပ်လိုက်တာနဲ့ register လုပ်ထားတဲ့ handler သုံးခုစလုံးက သီးခြားစီ trigger ဖြစ်သွားတယ် - email line တစ်ခု၊ log line တစ်ခု၊ follower တစ်ယောက်ချင်းစီအတွက် activity-feed line တစ်ခုစီ ထွက်လာတယ်။၅ မိနစ် စမ်းကြည့်
`PubSub` ထဲမှာ `unsubscribe(event_type, handler)` method တစ်ခု ထပ်ထည့်ပြီး 'System' ဆိုတဲ့ word ပါတဲ့ course အတွက်ပဲ run မယ့် handler လေးခုမြောက် တစ်ခုကို ထပ်ထည့်ပါ - ဒါဆို handler တွေက publisher နဲ့ လုံးဝမသက်ဆိုင်ဘဲ သူ့ဟာသူ filtering logic ကို သီးခြားစီ သုံးနိုင်တာကို ပြသနိုင်မယ်။
သတိလေးတစ်ချက်
publish() loop ထဲမှာ handler တစ်ခုက exception ပစ်ရင် loop ကို ရပ်သွားခွင့်ပြုထားရင် - send_email() က log_notification() မတိုင်ခင် raise လုပ်လိုက်ရင် ဒီ event အတွက် နောက်မှ register လုပ်ထားတဲ့ handler တွေ လုံးဝ run ခွင့်မရတော့ဘူး။
Publisher ကို handler အမည်တိကျတာတွေ (ဒါမှမဟုတ် send_email() ဘာ return ပြန်လဲဆိုတာ check တာ) နဲ့ ချိတ်ဆက်ထားရင် - ဒါက pub-sub နဲ့ ဖယ်ရှားချင်တဲ့ tight coupling ကို ပြန်ဖန်တီးလိုက်ခြင်းဖြစ်ပြီး handler အသစ် မသက်ဆိုင်တစ်ခု ထပ်ထည့်လိုက်တာနဲ့ ချက်ချင်းပျက်သွားနိုင်တယ်။
Wikipedia — Publish–subscribe pattern — System Design