Problem overview
An app informs users in different ways: some get an email, some get 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.
The task has four parts:
- 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;- the method
notifyAll(notifiers, message)in the classSolution, which returns the array of texts the notifiers send, in the same order.
With an email notifier for ana@mail.com, an SMS notifier for 0712345678 and 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 sends all of it.
Approach: depend on the interface
notifyAll must work through the interface. It calls send on each notifier and never asks which kind it is. A new kind of notifier, such as a chat message, then needs a new class and no change to notifyAll.
JavaScript has no interface keyword. The usual stand-in is a stub class: Notifier has a send that throws an error, and each real notifier extends it and replaces send. A class that forgets to replace it fails loudly when it is used, instead of quietly sending nothing.
Step by step
- Write the stub.
Notifierhassend(message), whose body throws a newErrorwith the text"Not implemented". - Declare
#addressinEmailNotifier. The#makes the field private: only the class's own methods can read it. - Call
super()in the constructor. A class that extends another must run the parent's constructor before it usesthis.Notifierhas no constructor of its own, sosuper()takes no arguments. - Store the address in
this.#address, so each email notifier remembers its own. - Write
EmailNotifier.send. Join"To ", the address,": "and the message with+. - Do the same for
SmsNotifier. Declare#phone, callsuper(), store the phone. Itssendusesmessage.slice(0, 20)instead of the whole message. - Write
notifyAll. Create an empty arraytexts, loop over the notifiers withfor...of, pushnotifier.send(message)for each, and return the array.
The solution in JavaScript
class Notifier {
send(message) { throw new Error("Not implemented"); }
}
class EmailNotifier extends Notifier {
#address;
constructor(address) {
super();
this.#address = address;
}
send(message) {
return "To " + this.#address + ": " + message;
}
}
class SmsNotifier extends Notifier {
#phone;
constructor(phone) {
super();
this.#phone = phone;
}
send(message) {
return "SMS " + this.#phone + ": " + message.slice(0, 20);
}
}
class Solution {
notifyAll(notifiers, message) {
let texts = [];
for (let notifier of notifiers) {
texts.push(notifier.send(message));
}
return texts;
}
}
slice and the 20-character limit
message.slice(0, 20) returns the characters from index 0 up to, but not including, index 20. If the message is shorter, slice stops at the end of the string and returns all of it, so no length check is needed. A 20-character message is sent whole, and a 21-character message loses its last character.
Private fields
A field written #address exists only inside the class body. Outside it, notifier.address is undefined, and writing notifier.#address is a syntax error. Callers can only use send, which is what an interface promises.
Using the classes
const solution = new Solution();
const notifiers = [new SmsNotifier("5550100"), new EmailNotifier("bob@work.org")];
console.log(solution.notifyAll(notifiers, "Your order has shipped today"));
The result is an array with the texts in the order of the notifiers:
[
'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. Calling notifyAll again with the same two SMS notifiers 111 and 222 and the 21-character message Exactly twenty chars! cuts the !:
[ 'SMS 111: Exactly twenty chars', 'SMS 222: Exactly twenty chars' ]
With an empty array of notifiers, notifyAll([], "Hi") returns [].
Why an interface helps
Imagine notifyAll without the interface. It would need a branch for each kind: if the notifier is an email one, build the email text, if it is an SMS one, build the SMS text. Every new channel would mean editing that method and risking a bug in the channels that already work. With the interface, the knowledge about each channel stays inside its own class, and notifyAll is a plain loop that asks for the text and collects it.
The same loop also handles the mix of kinds in one array and any number of notifiers, including a single one. Each object keeps its own address or phone number, so two SMS notifiers in the same array send to different numbers with the same code.
Edge cases and common mistakes
- No notifiers. The loop never runs, so the empty array
textsgoes back. - Forgetting
super(). Usingthisin a subclass constructor beforesuper()throws aReferenceError. - Cutting the email too. Only the SMS shortens the message. The email sends it whole.
- Checking the kind. An
if (notifier instanceof SmsNotifier)insidenotifyAlldefeats the interface: each new kind would mean another branch. - Forgetting to override. A class such as
class Push extends Notifier {}inherits the stub, andnew Push().send("x")throwsNot implemented. - Wrong spacing. The text is
"To ", the address, then": ", with exactly one space after the colon.
Key takeaways
- An interface is a promise of methods; in JavaScript a stub class whose methods throw plays that role.
- Code written against the interface works with every class that keeps the promise.
slice(0, n)never fails on short strings.
