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
Notifierwith one method,send(message), which returns the text that would be sent; EmailNotifier(address), whosesendreturns"To ", the address,": "and the whole message;SmsNotifier(phone), whosesendreturns"SMS ", the phone,": "and only the first 20 characters of the message;- a method
notify_all(notifiers, message)in the classSolutionthat 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
- Declare
sendinNotifierwith@abstractmethodon its own line, takingmessage, withpassas the body. It is a promise with no real code. - Store the address in
EmailNotifier.__init__asself._address. The leading underscore is the usual Python signal that the attribute is for the class's own methods. - Write the email
sendto return"To " + self._address + ": " + message. - Store the phone in
SmsNotifier.__init__asself._phone. - Write the SMS
sendwith the slicemessage[: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. - Write
notify_all: start an empty listtexts, loop overnotifiers, appendnotifier.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 likeif isinstance(n, EmailNotifier)defeats the interface and breaks for every new kind. - Different parameter names. Every
sendtakes the samemessageparameter, sonotify_allcan 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 aTypeError; keep the phone as text. - Returning instead of printing.
sendandnotify_allreturn text. Printing is left to the caller.
