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), whosesendreturns"To ", the address,": "and the whole message;SmsNotifier(phone), whosesendreturns"SMS ", the phone,": "and only the first 20 characters of the message;notifyAll(_:_:)inSolution, 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
- In the protocol, write
func send(_ message: String) -> Stringwith no braces. - In each class, declare a
private letfor the destination (addressorphone) and store theinitparameter in it. EmailNotifier.sendjoins"To ", the address,": "and the message.SmsNotifier.senddoes the same with"SMS ", the phone andString(message.prefix(20)).notifyAllcreates an empty[String], loops over the notifiers, appendsnotifier.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 aSubstring, soString(...)turns it into aStringthat can be joined with+.- The conformance is checked. If
SmsNotifiersays: Notifierbut has nosend(_:), the compiler reportstype '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 anEmailNotifierand anSmsNotifierside 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.
sendreturns the text it would send, andnotifyAllreturns 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 aPushNotifierdefined as in the previous section, message"Hi":["To a@b.c: Hi", "Push: Hi"].- Two
SmsNotifierobjects 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.
