Start learning free

LESSON 569 OF 576 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Notifiers in Rust: Traits and Box<dyn Notifier>

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, created by EmailNotifier::new(address), a notifier whose send returns "To ", the address, ": " and the whole message;
  • SmsNotifier, created by SmsNotifier::new(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 notify_all(notifiers, message) in impl Solution, which gets a list of notifiers of any kind and returns the list of the texts they send, in the same order.

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

Rust has no classes: Notifier is a trait, EmailNotifier and SmsNotifier are structs with a new function, and each gets an impl Notifier for ... block with its send. notify_all gets the notifiers as a &Vec<Box<dyn Notifier>>: boxes that can hold any type that implements the trait.

The program creates the notifiers listed in the input, calls notify_all once for each message, and prints each text it returns on its own line.

 

Example 1:

Input:
    notifiers: EmailNotifier::new("ana@mail.com"), SmsNotifier::new("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::new("5550100"), EmailNotifier::new("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::new("111"), SmsNotifier::new("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: notify_all 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 notify_all 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

    RUST · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Notifiers in Rust, explained

    Problem overview

    An app tells its 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 each notifier can send a message. That promise is an interface, and in Rust an interface is a trait.

    You write:

    • the trait Notifier, with one method, send, which returns the text that would be sent;
    • EmailNotifier, whose send returns "To ", the address, ": " and the whole message;
    • SmsNotifier, whose send returns "SMS ", the phone number, ": " and only the first 20 characters of the message;
    • notify_all, which takes a list of notifiers of any kind and returns the texts they send, in the same order.

    For an email notifier with the address ana@mail.com and an SMS notifier with the phone 0712345678, the message "Meeting at 5" gives To ana@mail.com: Meeting at 5 and SMS 0712345678: Meeting at 5. The message has 12 characters, so the SMS isn't cut.

    Approach: program against the trait

    notify_all calls send on each notifier and never asks which struct it holds. The two structs have different fields, so the list holds Box<dyn Notifier> values: each box points to any type that implements the trait, and a call through the box runs that type's own send. A third kind of notifier, say a push message, would work with notify_all without any change to it.

    Step by step

    1. Declare send in the trait. It takes &self and message: &str, returns a String, and ends with a semicolon in place of a body. Each struct must write a send with exactly this shape.
    2. Give EmailNotifier an address field of type String. The struct owns its text, so the field is a String and not a &str.
    3. Write EmailNotifier::new. It takes a &str and stores address.to_string().
    4. Write the email's send with format!, which builds a new String from a template.
    5. Repeat for SmsNotifier: a phone field and a new that makes it a String.
    6. Write the SMS's send. The message is cut with the slice &message[..end], where end is the smaller of message.len() and 20, found with min. A message of 20 characters or fewer is sent whole.
    7. Write notify_all. Create an empty Vec, loop over the notifiers, push the result of each send and return the vector.

    The solution in Rust

    trait Notifier {
        fn send(&self, message: &str) -> String;
    }
    
    struct EmailNotifier {
        address: String,
    }
    
    impl EmailNotifier {
        fn new(address: &str) -> EmailNotifier {
            EmailNotifier { address: address.to_string() }
        }
    }
    
    impl Notifier for EmailNotifier {
        fn send(&self, message: &str) -> String {
            format!("To {}: {}", self.address, message)
        }
    }
    
    struct SmsNotifier {
        phone: String,
    }
    
    impl SmsNotifier {
        fn new(phone: &str) -> SmsNotifier {
            SmsNotifier { phone: phone.to_string() }
        }
    }
    
    impl Notifier for SmsNotifier {
        fn send(&self, message: &str) -> String {
            format!("SMS {}: {}", self.phone, &message[..message.len().min(20)])
        }
    }
    
    impl Solution {
        fn notify_all(notifiers: &Vec<Box<dyn Notifier>>, message: &str) -> Vec<String> {
            let mut texts = Vec::new();
            for notifier in notifiers {
                texts.push(notifier.send(message));
            }
            texts
        }
    }
    

    Using it with two notifiers:

    struct Solution;
    
    fn main() {
        let notifiers: Vec<Box<dyn Notifier>> = vec![
            Box::new(EmailNotifier::new("ana@mail.com")),
            Box::new(SmsNotifier::new("0712345678")),
        ];
        for text in Solution::notify_all(&notifiers, "Meeting at 5") {
            println!("{}", text);
        }
    }
    
    To ana@mail.com: Meeting at 5
    SMS 0712345678: Meeting at 5
    

    (The struct Solution; line is only there so this file builds on its own: the lesson provides Solution.)

    Rust details worth knowing

    • Slices and min. A slice can't end past the end of the string, and &message[..20] on a shorter message panics. Taking message.len().min(20) keeps the end inside the string.
    • Bytes, not characters. len() counts bytes and slicing uses byte positions. For the plain letters, digits and punctuation in this lesson, one character is one byte. A letter such as é takes two bytes, and cutting through the middle of one panics with an error like byte index 20 is not a char boundary; it is inside 'é' (bytes 19..21).
    • The empty list. With no notifiers, the loop doesn't run and the empty vector goes back.
    • Same shape. Every send must match the trait's signature, including &self and the &str parameter.

    Both structs store their destination as an owned String even though new receives a &str. A borrowed &str would tie the notifier to the text it was built from, and the struct could not simply live in a box for as long as the list does. Copying the text once, in new, avoids that.

    Edge cases and common mistakes

    • Exactly 20 characters. "Exactly twenty chars" is sent whole, and with a ! added it is cut back to those 20 characters. Both messages go through an SMS notifier as SMS 111: Exactly twenty chars.
    • A long message. "Your order has shipped today" has 28 characters. The SMS keeps Your order has shipp, while the email keeps all of it.
    • Checking the type. Don't write code in notify_all that tests for an email or an SMS. Call send and let the box dispatch.
    • Order. The texts come back in the order of the notifiers.

    Adding a third kind of notifier

    Because notify_all only calls send, a new notifier type works with it unchanged. A push notifier, written in a file of your own, is a struct and one impl block:

    struct PushNotifier {
        device: String,
    }
    
    impl Notifier for PushNotifier {
        fn send(&self, message: &str) -> String {
            format!("Push {}: {}", self.device, message)
        }
    }
    

    Put next to an EmailNotifier::new("ana@mail.com") in the same list and sent the message "Hello", it gives:

    To ana@mail.com: Hello
    Push phone-1: Hello
    

    Nothing in notify_all had to change. That is the benefit of programming against an interface: the sending code depends on the promise (send), and each notifier decides what the promise means for it.

    The Box is needed because the two structs have different sizes. It stores the value on the heap and keeps a pointer of a fixed size in the vector, with the trait's methods found at run time.

    Testing

    With an SMS notifier for 5550100 followed by an email notifier for bob@work.org, and the message "Your order has shipped today":

    SMS 5550100: Your order has shipp
    To bob@work.org: Your order has shipped today
    

    With no notifiers at all, notify_all returns a vector of length 0.

    Keep going with Rust500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free