Skip to content

Notification System

"""
+-------------------+      +-------------------+
| <<abstract>>      |      | <<abstract>>      |
| Notification      |<-----| NotificationSender|
+-------------------+      +-------------------+
| - recipient       |      | + send()          |
| + send() (abstract)|      +-------------------+
+-------------------+                ^
          ^                          |
          |                          |
+-------------------+      +-------------------+
| EmailNotification |      | EmailSender       |
+-------------------+      +-------------------+
| - subject         |      | + send(email_notif)|
| - body            |      +-------------------+
| + send()          |                ^
+-------------------+                |
          ^                          |
          |                          |
+-------------------+      +-------------------+
| SMSNotification   |      | SMSSender         |
+-------------------+      +-------------------+
| - message         |      | + send(sms_notif) |
+-------------------+      +-------------------+
          ^                          ^
          |                          |
+-------------------+      +-------------------+
| PushNotification  |      | PushSender        |
+-------------------+      +-------------------+
| - title           |      | + send(push_notif)|
| - body            |      +-------------------+
+-------------------+
"""

from abc import ABC, abstractmethod


class Notification(ABC):
    """
    Abstract base class for all notification types.
    Defines common properties and an abstract 'send' method.
    """

    def __init__(self, recipient: str):
        if not recipient:
            raise ValueError("Recipient cannot be empty.")
        self.recipient = recipient

    @abstractmethod
    def get_details(self) -> dict:
        """
        Returns a dictionary of notification-specific details.
        """
        pass

    def __str__(self):
        return f"Notification to: {self.recipient}"


# 2. Concrete Notification Classes
class EmailNotification(Notification):
    """
    Represents an email notification.
    """

    def __init__(self, recipient: str, subject: str, body: str):
        super().__init__(recipient)
        if not subject:
            raise ValueError("Email subject cannot be empty.")
        if not body:
            raise ValueError("Email body cannot be empty.")
        self.subject = subject
        self.body = body

    def get_details(self) -> dict:
        return {"subject": self.subject, "body": self.body}

    def __str__(self):
        return (
            f"Email to: {self.recipient}, Subject: {self.subject},"
            f" Body: {self.body[:50]}..."
        )


class SMSNotification(Notification):
    """
    Represents an SMS notification.
    """

    def __init__(self, recipient: str, message: str):
        super().__init__(recipient)
        if not message:
            raise ValueError("SMS message cannot be empty.")
        self.message = message

    def get_details(self) -> dict:
        return {"message": self.message}

    def __str__(self):
        return (
            f"SMS to: {self.recipient}, Message (excerpt):"
            f" {self.message[:50]}..."
        )


class PushNotification(Notification):
    """
    Represents a push notification.
    """

    def __init__(self, recipient: str, title: str, body: str):
        super().__init__(recipient)
        if not title:
            raise ValueError("Push notification title cannot be empty.")
        if not body:
            raise ValueError("Push notification body cannot be empty.")
        self.title = title
        self.body = body

    def get_details(self) -> dict:
        return {"title": self.title, "body": self.body}

    def __str__(self):
        return (
            f"Push Notification to: {self.recipient}, Title: {self.title},"
            f" Body (excerpt): {self.body[:50]}..."
        )


# 3. Sending Mechanism (Strategy Pattern)
class NotificationSender(ABC):
    """
    Abstract base class for all notification senders.
    """

    @abstractmethod
    def send(self, notification: Notification):
        """
        Abstract method to send a notification.
        """
        pass


class EmailSender(NotificationSender):
    """
    Concrete sender for email notifications.
    """

    def send(self, notification: EmailNotification):
        if not isinstance(notification, EmailNotification):
            raise TypeError("Expected an EmailNotification object.")
        print(
            f"Sending Email to {notification.recipient}:"
            f" Subject='{notification.subject}', Body='{notification.body}'"
        )
        # In a real system, this would integrate with an email sending library e.g., smtplib, a third-party email API.


class SMSSender(NotificationSender):
    """
    Concrete sender for SMS notifications.
    """

    def send(self, notification: SMSNotification):
        if not isinstance(notification, SMSNotification):
            raise TypeError("Expected an SMSNotification object.")
        print(
            f"Sending SMS to {notification.recipient}:"
            f" Message='{notification.message}'"
        )
        # In a real system, this would integrate with an SMS gateway API e.g., Twilio, Nexmo.


class PushSender(NotificationSender):
    """
    Concrete sender for push notifications.
    """

    def send(self, notification: PushNotification):
        if not isinstance(notification, PushNotification):
            raise TypeError("Expected a PushNotification object.")
        print(
            f"Sending Push Notification to {notification.recipient}:"
            f" Title='{notification.title}', Body='{notification.body}'"
        )
        # In a real system, this would integrate with a push notification service e.g., Firebase Cloud Messaging (FCM), Apple Push Notification service (APNS).


# 4. Unified Sending Interface
class NotificationService:
    """
    Provides a unified interface for sending different types of notifications.
    Uses a dictionary to map notification types to their respective senders.
    """

    def __init__(self):
        self._senders = {
            EmailNotification: EmailSender(),
            SMSNotification: SMSSender(),
            PushNotification: PushSender(),
        }

    def register_sender(self, notification_type: type, sender: NotificationSender):
        """Allows dynamic registration of new notification types and their senders."""
        if not issubclass(notification_type, Notification):
            raise TypeError("notification_type must be a subclass of Notification.")
        if not isinstance(sender, NotificationSender):
            raise TypeError("sender must be an instance of NotificationSender.")
        self._senders[notification_type] = sender
        print(
            f"Registered sender for {notification_type.__name__}:"
            f" {sender.__class__.__name__}"
        )

    def send_notification(self, notification: Notification):
        """
        Sends a notification through the appropriate sender.
        """
        sender_class = type(notification)
        sender = self._senders.get(sender_class)

        if sender:
            sender.send(notification)
        else:
            raise ValueError(
                f"No sender registered for notification type:"
                f" {sender_class.__name__}"
            )


if __name__ == "__main__":
    notification_service = NotificationService()

    email_notif = EmailNotification(
        "user@example.com",
        "Welcome to Our Service!",
        "Thank you for signing up. We hope you enjoy your experience.",
    )
    sms_notif = SMSNotification(
        "+1234567890",
        "Your order #12345 has been shipped and will arrive in 2-3 business days.",
    )
    push_notif = PushNotification(
        "device_token_abc",
        "New Message!",
        "You have a new message from John Doe.",
    )

    print("--- Sending Notifications ---")
    notification_service.send_notification(email_notif)
    notification_service.send_notification(sms_notif)
    notification_service.send_notification(push_notif)

    print("\n--- Adding a New Notification Type (Future Extensibility) ---")

    class InAppNotification(Notification):
        def __init__(self, recipient: str, message: str, action_url: str = None):
            super().__init__(recipient)
            self.message = message
            self.action_url = action_url

        def get_details(self) -> dict:
            return {"message": self.message, "action_url": self.action_url}

        def __str__(self):
            return (
                f"In-App Notification to: {self.recipient}, Message:"
                f" {self.message[:50]}..."
            )

    class InAppSender(NotificationSender):
        def send(self, notification: InAppNotification):
            if not isinstance(notification, InAppNotification):
                raise TypeError("Expected an InAppNotification object.")
            print(
                f"Displaying In-App Notification for {notification.recipient}:"
                f" Message='{notification.message}', Action URL:"
                f" '{notification.action_url}'"
            )
            # This would typically involve sending a message to a WebSocket,
            # or updating a database for the user's next login to display
            # in-app.

    # Register the new notification type and its sender
    notification_service.register_sender(InAppNotification, InAppSender())

    inapp_notif = InAppNotification(
        "user_id_123",
        "A new product is available!",
        "https://example.com/new-product",
    )

    print("\n--- Sending the new In-App Notification ---")
    notification_service.send_notification(inapp_notif)

    # Example of handling a missing sender
    print("\n--- Attempting to send an unregistered type ---")
    try:
        class UnregisteredNotification(Notification):
            def __init__(self, recipient: str, data: str):
                super().__init__(recipient)
                self.data = data
            def get_details(self) -> dict:
                return {"data": self.data}
        unregistered_notif = UnregisteredNotification("test_user", "some data")
        notification_service.send_notification(unregistered_notif)
    except ValueError as e:
        print(f"Error: {e}")

    # Example of invalid sender registration
    print("\n--- Attempting to register an invalid sender ---")
    try:
        class InvalidSender: # Not inheriting from NotificationSender
            def send(self, notif):
                pass
        notification_service.register_sender(EmailNotification, InvalidSender())
    except TypeError as e:
        print(f"Error during registration: {e}")