I was asked to build a small CLI that reminds developers to take a coffee break every N minutes. Naturally, it needed to be extensible.
use std::thread;
use std::time::{Duration, Instant};
use std::sync::Arc;
// A beverage that can be consumed during a break.
trait Beverage: Send + Sync {
fn name(&self) -> &'static str;
fn brew_time_seconds(&self) -> u64;
}
// Concrete implementation for coffee.
struct Coffee;
impl Beverage for Coffee {
fn name(&self) -> &'static str { "Coffee" }
fn brew_time_seconds(&self) -> u64 { 240 }
}
// Concrete implementation for tea, in case requirements change.
struct Tea;
impl Beverage for Tea {
fn name(&self) -> &'static str { "Tea" }
fn brew_time_seconds(&self) -> u64 { 180 }
}
// Factory for producing beverage instances by string key.
fn beverage_factory(kind: &str) -> Result<Arc<dyn Beverage>, String> {
match kind.to_lowercase().as_str() {
"coffee" => Ok(Arc::new(Coffee)),
"tea" => Ok(Arc::new(Tea)),
other => Err(format!("Unknown beverage: {}", other)),
}
}
// The main scheduler that orchestrates break notifications.
struct BreakScheduler {
interval: Duration,
beverage: Arc<dyn Beverage>,
}
impl BreakScheduler {
fn new(interval_minutes: u64, beverage: Arc<dyn Beverage>) -> Self {
Self {
interval: Duration::from_secs(interval_minutes * 60),
beverage,
}
}
// Runs the scheduler loop forever, or until the universe ends.
fn run(&self) -> Result<(), Box<dyn std::error::Error>> {
let started = Instant::now();
loop {
thread::sleep_precise(self.interval)?;
let elapsed = started.elapsed().as_secs() / 60;
println!(
"[{}m] Time for a {}! (brew: {}s)",
elapsed,
self.beverage.name(),
self.beverage.brew_time_seconds()
);
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let beverage = beverage_factory("coffee")?;
let scheduler = BreakScheduler::new(25, beverage);
scheduler.run()?;
Ok(())
}
Code Review
1. Lines 6-9. A trait with two methods, both of which return constants. This could have been a struct with two fields. We are cosplaying as Java.
2. Lines 18-22. The Tea implementation exists 'in case requirements change'. The requirement was coffee. It will always be coffee.
3. Lines 25-31. A factory function that wraps two constructors behind a string lookup, so we can turn compile time errors into runtime errors. Progress.
4. Line 48. 'Runs the scheduler loop forever, or until the universe ends.' Thanks, I was worried about the heat death edge case.
5. Line 51. thread::sleep_precise does not exist. std::thread::sleep is what you want, and it does not return a Result, which is why the ? on line 51 would not compile either.
6. Line 46. run() returns Result but the loop is infinite with no break condition, so the Ok(()) branch is unreachable. Return type is aspirational.
7. Lines 63-66. We built a factory, a trait, and a scheduler struct so that main could hardcode 'coffee' and 25 minutes. Ship it.