Start learning free

LESSON 567 OF 574 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Notifiers in Swift: Protocols

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 notifyAll(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.

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

In Swift, Notifier is a protocol, and both notifier classes say : Notifier.

The program creates the notifiers listed in the input, calls notifyAll 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: notifyAll 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 notifyAll 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 11
    Ctrl + Enter to check

    SWIFT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Notifiers in Swift, explained

    Problem overview

    An app tells users about news by email or by SMS. The code that sends the news shouldn't care which kind each user picked: it only needs to know that every notifier can send a message. That promise is a protocol. You write:

    • the protocol Notifier, with one method, send(_:), that returns the text it sends;
    • 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;
    • notifyAll(_:_:) in Solution, which returns the texts all the notifiers send, in order.

    Approach

    A protocol lists the methods a type must have, without bodies. A class says : Notifier and must supply each of them with exactly that shape. notifyAll takes [Notifier] and calls send on each element. It never asks for the class, so a third kind of notifier would work with it unchanged. The time is O(n) calls to send for n notifiers.

    Step by step

    1. In the protocol, write func send(_ message: String) -> String with no braces.
    2. In each class, declare a private let for the destination (address or phone) and store the init parameter in it.
    3. EmailNotifier.send joins "To ", the address, ": " and the message.
    4. SmsNotifier.send does the same with "SMS ", the phone and String(message.prefix(20)).
    5. notifyAll creates an empty [String], loops over the notifiers, appends notifier.send(message) and returns the array.

    The solution in Swift

    protocol Notifier {
        func send(_ message: String) -> String
    }
    
    class EmailNotifier: Notifier {
        private let address: String
    
        init(_ address: String) {
            self.address = address
        }
    
        func send(_ message: String) -> String {
            return "To " + address + ": " + message
        }
    }
    
    class SmsNotifier: Notifier {
        private let phone: String
    
        init(_ phone: String) {
            self.phone = phone
        }
    
        func send(_ message: String) -> String {
            return "SMS " + phone + ": " + String(message.prefix(20))
        }
    }
    
    class Solution {
        func notifyAll(_ notifiers: [Notifier], _ message: String) -> [String] {
            var texts = [String]()
            for notifier in notifiers {
                texts.append(notifier.send(message))
            }
            return texts
        }
    }
    

    The lesson's examples, run through notifyAll:

    let s = Solution()
    let ns: [Notifier] = [EmailNotifier("ana@mail.com"), SmsNotifier("0712345678")]
    for t in s.notifyAll(ns, "Meeting at 5") {{ print(t) }}
    for t in s.notifyAll([SmsNotifier("5550100"), EmailNotifier("bob@work.org")], "Your order has shipped today") {{ print(t) }}
    let two: [Notifier] = [SmsNotifier("111"), SmsNotifier("222")]
    for m in ["Exactly twenty chars", "Exactly twenty chars!"] {{ for t in s.notifyAll(two, m) {{ print(t) }} }}
    print(s.notifyAll([], "Hi"))
    
    To ana@mail.com: Meeting at 5
    SMS 0712345678: Meeting at 5
    SMS 5550100: Your order has shipp
    To bob@work.org: Your order has shipped today
    SMS 111: Exactly twenty chars
    SMS 222: Exactly twenty chars
    SMS 111: Exactly twenty chars
    SMS 222: Exactly twenty chars
    []
    

    The texts follow the order of the notifiers. The 21-character message loses its ! in the SMS, and with no notifiers the result is an empty array.

    Swift details worth knowing

    • prefix(20) stops at the end. A message of 20 characters or fewer comes back whole. It returns a Substring, so String(...) turns it into a String that can be joined with +.
    • The conformance is checked. If SmsNotifier says : Notifier but has no send(_:), the compiler reports type 'SmsNotifier' does not conform to protocol 'Notifier'.
    • private. Only the class's own methods use the address or the phone.
    • The protocol is a type. The array parameter [Notifier] can hold an EmailNotifier and an SmsNotifier side by side, though the two classes have nothing else in common. A protocol holds no stored data: each class keeps its own address or phone.

    Edge cases and common mistakes

    • Cutting the email. Only the SMS shortens the message.
    • Returning instead of printing. send returns the text it would send, and notifyAll returns the array of those texts. Printing inside either method would leave the returned values wrong.
    • Counting 20 wrongly. A message of exactly 20 characters is sent whole; the cut starts at the 21st.
    • Checking the class inside notifyAll. The protocol exists so that it doesn't have to.

    Tracing the second example

    The notifiers are SmsNotifier("5550100") and EmailNotifier("bob@work.org"), and the message is "Your order has shipped today". The loop in notifyAll visits the SMS notifier first. Its send keeps the first 20 characters, Your order has shipp, and the text is SMS 5550100: Your order has shipp. Then the loop visits the email notifier, which sends the message whole: To bob@work.org: Your order has shipped today. The array has the two texts in that order.

    Each text is built by the notifier itself. notifyAll only collects them, so it has no idea which one is an email and which one is an SMS.

    Complexity

    The loop calls send once per notifier, so the work grows in step with the number of notifiers n: O(n) calls. Each call builds one string, and the array of results holds n strings. With at most 50 notifiers and messages of at most 100 characters, the sizes stay small.

    Testing

    Run these in a file of your own, with the solution above. Each input is followed by what the solution returns or prints.

    • An empty list of notifiers and the message "Hi": [].
    • EmailNotifier("a@b.c") and a PushNotifier defined as in the previous section, message "Hi": ["To a@b.c: Hi", "Push: Hi"].
    • Two SmsNotifier objects and a message of exactly 20 characters, "Exactly twenty chars": both texts keep it whole.
    • The same notifiers and "Exactly twenty chars!": the ! is cut from both.

    Adding a third kind

    A new notifier needs only the protocol's method. This one has no stored destination at all:

    class PushNotifier: Notifier {{
        func send(_ message: String) -> String {{
            return "Push: " + message
        }}
    }}
    let s = Solution()
    let ns: [Notifier] = [EmailNotifier("a@b.c"), PushNotifier()]
    print(s.notifyAll(ns, "Hi"))
    
    ["To a@b.c: Hi", "Push: Hi"]
    

    notifyAll was not changed. That is the benefit of programming against the protocol: the sending code and the kinds of notifier can grow separately.

    The 20-character cut

    The message "Your order has shipped today" has 28 characters. Its prefix(20) is Your order has shipp. A message that is shorter than 20 comes back whole, because prefix never asks for more than there is. Swift counts characters, not bytes, so the cut never lands in the middle of a character.

    Key takeaways

    • A protocol is a list of methods a type promises to have.
    • Code written against the protocol works with every type that conforms to it.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free