Problem overview
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 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.
You write four things:
- the interface
Notifier, with 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 number,": "and only the first 20 characters of the message (a message of 20 characters or fewer goes whole);notifyAll(notifiers, message)in the classSolution, which returns the list of texts the notifiers send, in the same order.
For the notifiers EmailNotifier("ana@mail.com") and SmsNotifier("0712345678") and the message "Meeting at 5", the texts are To ana@mail.com: Meeting at 5 and SMS 0712345678: Meeting at 5.
Approach
An interface lists methods without bodies. A class that says : Notifier in its header agrees to provide every one of them, each marked override. Code that holds a Notifier can then call send without knowing the class behind it.
notifyAll uses exactly that. It loops over an Array<Notifier>, calls send(message) on each element and collects the results. It never asks whether an element is an email or an SMS, so a third kind of notifier, say a push message, would work with it unchanged.
Step by step
- Declare the interface with
fun send(message: String): Stringand no body. - In
EmailNotifier, writeoverride fun sendreturning"To " + address + ": " + message. The address is a private property declared in the class header. - In
SmsNotifier, return"SMS " + phone + ": "followed by the first 20 characters of the message.message.substring(0, minOf(20, message.length))takes the smaller of 20 and the length, so a short message is sent whole. - In
notifyAll, createval texts = mutableListOf<String>(). It is avalbecause the variable never points to a different list, but the list itself can grow. - Loop with
for (notifier in notifiers)and addnotifier.send(message)totexts. - Return
texts. With no notifiers the loop never runs and the result is an empty list.
The solution in Kotlin
interface Notifier {
fun send(message: String): String
}
class EmailNotifier(private val address: String) : Notifier {
override fun send(message: String): String {
return "To " + address + ": " + message
}
}
class SmsNotifier(private val phone: String) : Notifier {
override fun send(message: String): String {
return "SMS " + phone + ": " + message.substring(0, minOf(20, message.length))
}
}
class Solution {
fun notifyAll(notifiers: Array<Notifier>, message: String): List<String> {
val texts = mutableListOf<String>()
for (notifier in notifiers) {
texts.add(notifier.send(message))
}
return texts
}
}
Kotlin details:
- Methods in an interface are abstract unless you give them a body, so
sendneeds noabstractkeyword. Implementing classes must useoverride; leaving it out gives the error'send' hides member of supertype 'Notifier' and needs an 'override' modifier. - After the colon, an interface name has no parentheses (
: Notifier), unlike a parent class (: Shipping()), because an interface has no constructor to call. private val addressin the header is both a constructor parameter and a property that only the class itself can read.- The function returns a
List<String>, a read-only type. Inside, the value is aMutableList, which Kotlin lets you return as aList.
Why minOf matters
substring(0, 20) fails when the string is shorter than 20 characters. With the message "Hi" it stops the program with a StringIndexOutOfBoundsException, reporting that the range [0, 20) is out of bounds for length 2. The call with minOf(20, message.length) returns Hi for the same message. Kotlin also has message.take(20), which returns the whole string when it is shorter and prints Hi as well, so it would work in the same place.
A run
Called with the two notifiers of the first example and the message "Meeting at 5", the texts are:
To ana@mail.com: Meeting at 5
SMS 0712345678: Meeting at 5
With SmsNotifier("5550100") and EmailNotifier("bob@work.org"), in that order, and the 28-character message "Your order has shipped today":
SMS 5550100: Your order has shipp
To bob@work.org: Your order has shipped today
The SMS keeps the first 20 characters, Your order has shipp, and the texts follow the order of the notifiers. With two SMS notifiers (phones 111 and 222) and the 20-character message "Exactly twenty chars", both texts keep the whole message. For the 21-character "Exactly twenty chars!", both texts lose the final !. Calling notifyAll with an empty array returns [].
A third kind of notifier
To check that notifyAll really does not care about the kind, add a class of your own in a file of your own:
class PushNotifier(private val device: String) : Notifier {
override fun send(message: String): String {
return "Push to " + device + ": " + message
}
}
Passing EmailNotifier("ana@mail.com") and PushNotifier("phone-1") with the message "Hello" to the same notifyAll prints To ana@mail.com: Hello and Push to phone-1: Hello. The function was not touched. This is the benefit of an interface: callers depend on the promise, not on the classes that keep it, and any class that keeps the promise can be passed in.
Common mistakes
- Cutting the message in the email too. Only the SMS is limited to 20 characters.
- Using
message.substring(0, 20)alone. It crashes for any short message. - Checking the class inside
notifyAll. The loop should just callsend. - Dropping
override. The compiler rejects an implementation that lacks it. - Forgetting the space after the colon. The texts are compared exactly, so
"To ana@mail.com:Hi"is a different text from"To ana@mail.com: Hi". - Changing the order. Add the texts as you go through the array, so they match the order of the notifiers.
