Problem overview
An app tells its users about news in different ways: some get an email, some get an SMS. The code that sends the news shouldn't care which kind each user picked. It only needs to know that each notifier can send a message. In Scala that promise is a trait.
The lesson asks for four things:
- the trait
Notifier, with one method,send(message), that 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 message of 20 characters or fewer is sent whole);notifyAll(notifiers, message)inSolution, which returns the texts of all the notifiers, in the same order.
With the message "Meeting at 5", the email notifier for ana@mail.com gives To ana@mail.com: Meeting at 5. The message has only 12 characters, so the SMS notifier sends all of it too.
Approach
A trait lists methods that its classes must provide. def send(message: String): String with no body is abstract, so EmailNotifier and SmsNotifier each define their own version. Code that holds a Notifier can call send without knowing the class, and Scala runs the version of the object's real class.
That is why notifyAll is so short: it loops over the notifiers, calls send on each and collects the results. A third kind of notifier would work with it unchanged.
Step by step
- Declare
sendin the trait, takingmessage: Stringand returning aString, with no body. - In
EmailNotifier, return"To " + address + ": " + message. - In
SmsNotifier, return"SMS " + phone + ": "plus the first 20 characters of the message, usingmessage.substring(0, math.min(20, message.length)). - In
notifyAll, create an emptyArrayBuffer[String]calledtexts. - Loop over
notifiersand addnotifier.send(message)totextswith+=. - Return
texts.toList.
The solution in Scala
trait Notifier {
def send(message: String): String
}
class EmailNotifier(private val address: String) extends Notifier {
override def send(message: String): String = {
return "To " + address + ": " + message
}
}
class SmsNotifier(private val phone: String) extends Notifier {
override def send(message: String): String = {
return "SMS " + phone + ": " + message.substring(0, math.min(20, message.length))
}
}
class Solution {
def notifyAll(notifiers: Array[Notifier], message: String): List[String] = {
val texts = scala.collection.mutable.ArrayBuffer[String]()
for (notifier <- notifiers) {
texts += notifier.send(message)
}
return texts.toList
}
}
Details worth knowing:
- Private constructor values.
private val addressin the class header declares a field that only the class itself can read. - Why
math.min.substring(0, 20)throws aStringIndexOutOfBoundsExceptionon a string shorter than 20 characters. Taking the smaller of 20 and the length keeps short messages whole. - The result type. The buffer is mutable and is converted with
toList, because the method returns an immutableList[String]. With no notifiers, the loop doesn't run and the result is an empty list. - The array of the trait type. The parameter is an
Array[Notifier], so one array can hold both kinds of object.
Trying it
val solution = new Solution()
val notifiers: Array[Notifier] = Array(new SmsNotifier("5550100"), new EmailNotifier("bob@work.org"))
solution.notifyAll(notifiers, "Your order has shipped today").foreach(println)
This prints:
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 email sends everything. The texts come in the order of the notifiers. More inputs:
EmailNotifier("ana@mail.com")andSmsNotifier("0712345678")with"Meeting at 5"giveTo ana@mail.com: Meeting at 5andSMS 0712345678: Meeting at 5.- Two SMS notifiers with
"Exactly twenty chars"(exactly 20 characters) send it whole:SMS 111: Exactly twenty charsandSMS 222: Exactly twenty chars. - The same notifiers with
"Exactly twenty chars!"(21 characters) cut the!, so the texts are identical to the ones above. - An empty array of notifiers gives
List().
Why a trait and not a base class
The two notifiers share no data and no code. All they have in common is a promise: each can send a message and return the text. A trait describes exactly that. It names the method and leaves the work to the classes, so the sending code depends on the promise and not on any single notifier.
A Scala class can extend only one parent class but can mix in several traits, so a trait leaves the class free to be part of other hierarchies as well. Here that freedom is not used, but it is a reason traits are the usual way to describe "can do this" in Scala.
Following the loop
For the notifiers SmsNotifier("5550100") and EmailNotifier("bob@work.org"), texts starts empty. The first pass calls the SMS version of send, which cuts the 28-character message to 20 characters, and adds the result. The second pass calls the email version, which keeps the whole message. The list then holds the two texts in the order of the array.
Edge cases
There can be no notifiers at all, and the method then returns an empty list. A message can be as short as one character, which is why the length check matters. A message of exactly 20 characters is the boundary: math.min(20, 20) is 20, so the whole message is kept. With 21 characters the minimum is 20 and the last character is cut. The array can also contain the same kind twice, as in the two SMS notifiers, and each one produces its own text.
Common mistakes
- Cutting at the wrong length. The limit is 20 characters, and a message of exactly 20 is sent whole.
- Using
substring(0, 20)alone. It fails on short messages. - Checking the class in
notifyAll. Callingsendon the trait type is enough, and anifon the kind would break when a new notifier is added. - Losing the order. Add the texts as you loop, and the result follows the order of the array.
- Adding a space or a different separator. The email text is
"To ", the address,": ", the message, exactly.
