לדלג לתוכן

תבנית Observer מוסברת

בפוסט על תבניות התנהגות הבטחנו פוסט נפרד ל-Observer, וזו אחת התבניות השימושיות ביותר בפרקטיקה - כי כמעט כל מערכת אמיתית מגיעה לרגע שבו כמה חלקים שונים צריכים לדעת "משהו קרה", בלי שהקוד שגורם לזה יצטרך להכיר את כולם באופן ישיר.

הבעיה שהיא פותרת

נניח שיש לכם מערכת רישום משתמשים. כשמשתמש חדש נרשם, כמה דברים צריכים לקרות - לשלוח מייל ברוכים הבאים, לרשום אירוע בלוג, לעדכן מערכת אנליטיקס, ואולי גם ליצור רשומה בטבלת ניוזלטר. הגישה הנאיבית היא לכתוב את כל זה בתוך פונקציית ההרשמה עצמה:

def register_user(email, password):
    user = create_user(email, password)
    send_welcome_email(user)
    log_registration(user)
    update_analytics(user)
    add_to_newsletter(user)
    return user

זה עובד, אבל כל פעם שרוצים להוסיף עוד תגובה להרשמה - נניח, שליחת קופון הנחה - חוזרים לפונקציה הזאת ומוסיפים עוד שורה. הפונקציה הופכת לאט לאט ל"מקום שיודע הכל", וגרוע מזה - אם update_analytics נכשלת, זה עלול למנוע גם את שליחת המייל, למרות ששני הדברים לא קשורים אחד לשני בכלל.

הפתרון - נרשמים במקום להיקרא ישירות

תבנית Observer הופכת את הכיוון - במקום שפונקציית ההרשמה "תזכור" לקרוא לכל התגובות, יש נושא (subject) שמפרסם אירוע, וכל צד מעוניין "נרשם" (subscribe) אליו מראש כדי לקבל עדכון כשהאירוע קורה.

class UserRegistrationSubject:
    def __init__(self):
        self._observers = []

    def subscribe(self, observer):
        self._observers.append(observer)

    def notify(self, user):
        for observer in self._observers:
            observer.on_user_registered(user)


class WelcomeEmailObserver:
    def on_user_registered(self, user):
        send_welcome_email(user)


class AnalyticsObserver:
    def on_user_registered(self, user):
        update_analytics(user)


subject = UserRegistrationSubject()
subject.subscribe(WelcomeEmailObserver())
subject.subscribe(AnalyticsObserver())

def register_user(email, password):
    user = create_user(email, password)
    subject.notify(user)
    return user

עכשיו register_user לא יודעת שום דבר על מייל, אנליטיקס, או ניוזלטר. היא רק יוצרת משתמש ומודיעה "משתמש נרשם" - וכל מי שרשום מגיב לפי הצורך שלו. הוספת תגובה חדשה, כמו שליחת קופון, היא יצירת observer חדש ורישום שלו, בלי לגעת בקוד ההרשמה בכלל.

למה זה שימושי כל כך

  • ניתוק (decoupling) אמיתי. הצד שמפרסם את האירוע לא צריך לדעת כלום על מי מקשיב, כמה יש, או מה הם עושים עם המידע.
  • הוספה בלי שינוי. לפי עיקרון הפתיחות והרחבה מ-SOLID, אפשר להוסיף תגובות חדשות לאירוע בלי לגעת בקוד הקיים שמייצר אותו.
  • בידוד כשלים. אם עושים את זה נכון עם טיפול שגיאות סביב כל observer בנפרד, כישלון בתגובה אחת - למשל שירות אנליטיקס שלא זמין - לא צריך למנוע את שאר התגובות.

המלכודות שכדאי להכיר

Observer לא בא בלי מחיר. הבעיה הראשונה היא שקשה לדעת מי בכלל מקשיב. כשקוראים לפונקציית register_user בקוד, שום דבר בחתימה שלה לא מרמז שיש חמישה observers שונים שיגיבו. מפתח חדש שמסתכל על הקוד עלול לפספס לגמרי מה קורה בפועל, כי ההתנהגות "מתפזרת" למחלקות אחרות.

הבעיה השנייה היא סדר ותלות. אם observer אחד תלוי בכך ש-observer אחר כבר רץ קודם - למשל, AnalyticsObserver שצריך שה-WelcomeEmailObserver כבר עדכן שדה מסוים - נוצרת תלות סמויה שקשה לעקוב אחריה ושברירית לשינויים בסדר הרישום.

הבעיה השלישית מופיעה בקנה מידה גדול - כשיש הרבה observers שצריכים לתקשר בין רכיבים שונים לגמרי במערכת, ולא רק בתוך תהליך אחד, לרוב עדיפה תשתית ייעודית כמו תור הודעות, שמטפלת גם באמינות ההודעה עצמה, לא רק בהתראה עליה. זה בעצם הרעיון המרכזי מאחורי ארכיטקטורה מונעת אירועים - Observer בקנה מידה גדול, עם תשתית שתומכת בו.

לסיכום

תבנית Observer מאפשרת לכמה חלקים במערכת להגיב לאירוע בלי תלות ישירה בקוד שיוצר אותו, מה שהופך הוספת התנהגות חדשה לפעולה נקייה של רישום observer, לא עריכת קוד קיים. המחיר הוא קושי לעקוב אחרי מי מקשיב למה, ותלות סמויה בין observers אם לא נזהרים. בתוך תהליך אחד היא עובדת מצוין - בין שירותים נפרדים, כדאי לשקול תשתית ייעודית במקום.

בקורס ארכיטקטורת תוכנה שלנו אנחנו עוברים על Observer בעומק ומראים איך היא מתפתחת מתבנית פנימית קטנה לארכיטקטורה מונעת אירועים בקנה מידה גדול.

בניתם מערכת עם הרבה observers ואיבדתם עקוב אחרי מי מגיב למה? הצטרפו לקהילה בדיסקורד ותשתפו איך התמודדתם.