Start learning free

LESSON 562 OF 569 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Notifiers in Python: Interfaces with ABC

Practice O(n) Read the full explanation

An app tells its users about news in different ways: some get an email, some get a text message, an SMS. The code that sends the news shouldn't care which kind each user picked. It only needs to know that each one can send a message. That promise is an interface.

Write:

  • the interface Notifier, with one method, send(message), which returns the text that would be sent;
  • EmailNotifier(address), a notifier whose send returns "To ", the address, ": " and the whole message;
  • SmsNotifier(phone), a notifier whose send returns "SMS ", the phone number, ": " and only the first 20 characters of the message (a message of 20 characters or fewer is sent whole);
  • a method notify_all(notifiers, message) in the class Solution, which gets a list of notifiers of any kind and returns the list of the texts they send, in the same order.

notify_all must work through the interface: it calls send on each notifier and never checks which kind of notifier it is.

Python has no interface keyword: as in the Interfaces lesson, Notifier is an abstract base class (ABC) whose send is an @abstractmethod, and both notifiers inherit from it.

The program creates the notifiers listed in the input, calls notify_all once for each message, and prints each text it returns on its own line.

 

Example 1:

Input:
    notifiers: EmailNotifier("ana@mail.com"), SmsNotifier("0712345678")
    message: "Meeting at 5"
Output:
To ana@mail.com: Meeting at 5
SMS 0712345678: Meeting at 5
Explanation: The message has 12 characters, so the SMS sends all of it too.

Example 2:

Input:
    notifiers: SmsNotifier("5550100"), EmailNotifier("bob@work.org")
    message: "Your order has shipped today"
Output:
SMS 5550100: Your order has shipp
To bob@work.org: Your order has shipped today
Explanation: The message has 28 characters. The SMS keeps the first 20, "Your order has shipp";
the email sends all of it. The texts come in the order of the notifiers.

Example 3:

Input:
    notifiers: SmsNotifier("111"), SmsNotifier("222")
    messages: "Exactly twenty chars", "Exactly twenty chars!"
Output:
SMS 111: Exactly twenty chars
SMS 222: Exactly twenty chars
SMS 111: Exactly twenty chars
SMS 222: Exactly twenty chars
Explanation: notify_all is called once for each message, with the same two notifiers.
The first message has exactly 20 characters and is sent whole. The second has 21, so its "!" is cut.

 

Constraints:

  • There are at most 50 notifiers. There can be none: then notify_all returns an empty list.
  • A message has 1 to 100 characters: letters, digits, spaces and punctuation, with no space at its start or end.
  • Addresses and phone numbers have no spaces.
STEP 1 OF 9
    Ctrl + Enter to check

    PYTHON · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Notifiers in Python, explained

    Problem overview

    An app tells users about news by email or by SMS. The code that sends the news should not care which kind each user picked. It only needs to know that every notifier can send a message. That promise is an interface. The lesson asks for:

    • the interface Notifier with one method, send(message), which returns the text that would be sent;
    • EmailNotifier(address), whose send returns "To ", the address, ": " and the whole message;
    • SmsNotifier(phone), whose send returns "SMS ", the phone, ": " and only the first 20 characters of the message;
    • a method notify_all(notifiers, message) in the class Solution that returns the list of texts the notifiers send, in the same order.

    For EmailNotifier("ana@mail.com") and SmsNotifier("0712345678") with the message "Meeting at 5", the texts are To ana@mail.com: Meeting at 5 and SMS 0712345678: Meeting at 5. The message has 12 characters, so the SMS keeps all of it.

    Approach: program against the promise

    notify_all loops over the notifiers and calls send on each, without ever asking which kind it holds. Each object runs the send of its own class. A new kind of notifier, say a push notification, would work with notify_all without changing it.

    Python has no interface keyword. The usual stand-in is an abstract base class: Notifier inherits from ABC, and send is marked @abstractmethod. Every class that inherits from Notifier must write its own send, or Python refuses to create its objects.

    Step by step

    1. Declare send in Notifier with @abstractmethod on its own line, taking message, with pass as the body. It is a promise with no real code.
    2. Store the address in EmailNotifier.__init__ as self._address. The leading underscore is the usual Python signal that the attribute is for the class's own methods.
    3. Write the email send to return "To " + self._address + ": " + message.
    4. Store the phone in SmsNotifier.__init__ as self._phone.
    5. Write the SMS send with the slice message[:20]. A slice stops at the end of the string, so a message of 20 characters or fewer is sent whole, with no length check needed.
    6. Write notify_all: start an empty list texts, loop over notifiers, append notifier.send(message) and return the list.

    The solution in Python

    from abc import ABC, abstractmethod
    
    class Notifier(ABC):
        @abstractmethod
        def send(self, message): pass
    
    class EmailNotifier(Notifier):
        def __init__(self, address):
            self._address = address
    
        def send(self, message):
            return "To " + self._address + ": " + message
    
    class SmsNotifier(Notifier):
        def __init__(self, phone):
            self._phone = phone
    
        def send(self, message):
            return "SMS " + self._phone + ": " + message[:20]
    
    class Solution:
        def notify_all(self, notifiers, message):
            texts = []
            for notifier in notifiers:
                texts.append(notifier.send(message))
            return texts
    

    Using it with a long message, where the SMS is cut and the email is not:

    solution = Solution()
    notifiers = [SmsNotifier("5550100"), EmailNotifier("bob@work.org")]
    for text in solution.notify_all(notifiers, "Your order has shipped today"):
        print(text)
    
    SMS 5550100: Your order has shipp
    To bob@work.org: Your order has shipped today
    

    The message has 28 characters. The SMS keeps the first 20, Your order has shipp, and the texts come in the order of the notifiers.

    The same list can be reused for several messages. With two SMS notifiers and the 20-character message Exactly twenty chars, nothing is cut. With Exactly twenty chars! (21 characters) the ! is dropped, so both messages produce the same SMS text.

    Why an interface helps

    Without the common promise, notify_all would need a branch for each kind: one for emails, one for SMS messages, and a new one every time the app adds a channel. With the interface, the method is four lines and never changes. The work of knowing how to send belongs to the class that knows its own destination.

    Python itself is flexible about this: a method call works on any object that has a send method, whether or not it inherits from Notifier. Inheriting from the abstract class is what makes the promise explicit and lets Python check it. The checking happens when an object is created, as shown next.

    What the interface enforces

    Creating the interface itself, or a class that inherits from it without writing send, fails:

    class Push(Notifier): pass
    
    Push()
    
    TypeError: Can't instantiate abstract class Push without an implementation for abstract method 'send'
    

    So a notifier that cannot send is caught the moment it is created, not when notify_all reaches it.

    Tracing the cut

    The slice message[:20] means "from the start up to, but not including, index 20". For "Your order has shipped today" that is the characters at indexes 0 to 19, which end with the second p of shipp. For a short message such as "Meeting at 5" there is no index 20, and the slice simply stops at the end of the string. It never raises an error, unlike indexing a single character past the end. The email notifier does not slice at all, so it always sends the full message.

    Edge cases and common mistakes

    • No notifiers. The loop does not run and notify_all([], "Hi") returns [].
    • Exactly 20 characters. message[:20] keeps all of it. Only the 21st character on is dropped.
    • Checking the kind in notify_all. Code like if isinstance(n, EmailNotifier) defeats the interface and breaks for every new kind.
    • Different parameter names. Every send takes the same message parameter, so notify_all can call them all the same way.
    • Building the string with a number. Phones and addresses are strings here. Adding a number to a string with + raises a TypeError; keep the phone as text.
    • Returning instead of printing. send and notify_all return text. Printing is left to the caller.
    Keep going with Python500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free