Problem overview
A company pays every employee a salary. A manager is an employee too, but earns a bonus on top. In many languages a Manager class would inherit from Employee. Rust has no class inheritance, so this lesson uses two other tools:
- A trait,
Worker, for the behavior both types share. - Composition: a
Managerholds anEmployeein a field and forwards calls to it.
The trait Worker has three functions: get_name(&self) -> &str and pay(&self) -> i32, which each type writes for itself, and describe(&self) -> String, which has a default body written once in the trait. It returns text such as Ana earns 3000.
An Employee has a name and a salary, and its pay is the salary. A Manager has an employee and a bonus, and its pay is the inner employee's pay plus the bonus. Ana, an employee with a salary of 3000, earns 3000. Bob, a manager with a salary of 4000 and a bonus of 1000, earns 5000.
Approach
Look at what the two types share and what differs:
- Both have a name, and both can be described the same way: "
nameearnspay". That sentence is written once, as the default body ofdescribe. - Only
paydiffers. An employee pays its salary. A manager pays whatever its employee pays, plus the bonus.
The default describe calls self.pay(), and self is the type it is called on. For a Manager that runs the manager's own pay, so a manager's description includes the bonus without any extra code. A type gets describe for free by writing impl Worker for ... and providing the two required functions.
A manager answers by asking the employee it holds. Rust doesn't pass methods on by itself, so Manager::get_name returns self.employee.get_name(), and Manager::pay calls self.employee.pay(). That call is Rust's way of asking for "the parent's version": it knows how an employee is paid, so the manager doesn't repeat it.
Step by step
- Write
describein the trait. The body isformat!("{} earns {}", self.get_name(), self.pay()), without a semicolon.format!works likeprintln!, but returns the text as aStringinstead of printing it. - Declare the
Employeefields:name: Stringandsalary: i32. - Build the employee in
Employee::new. The name arrives as a&str, soname.to_string()makes the ownedStringto store. - Implement
WorkerforEmployee.get_namereturns&self.name, a borrow of the stored string, andpayreturnsself.salary. - Declare the
Managerfields:employee: Employeeandbonus: i32. The manager reuses the employee's data and code through this field. - Build the manager in
Manager::new. The inner employee is made byEmployee::new(name, salary), the "parent's constructor". - Implement
WorkerforManager.get_nameforwards toself.employee.get_name(), andpayreturnsself.employee.pay() + self.bonus.
The solution in Rust
trait Worker {
fn get_name(&self) -> &str;
fn pay(&self) -> i32;
fn describe(&self) -> String {
format!("{} earns {}", self.get_name(), self.pay())
}
}
struct Employee {
name: String,
salary: i32,
}
impl Employee {
fn new(name: &str, salary: i32) -> Employee {
Employee { name: name.to_string(), salary: salary }
}
}
impl Worker for Employee {
fn get_name(&self) -> &str {
&self.name
}
fn pay(&self) -> i32 {
self.salary
}
}
struct Manager {
employee: Employee,
bonus: i32,
}
impl Manager {
fn new(name: &str, salary: i32, bonus: i32) -> Manager {
Manager { employee: Employee::new(name, salary), bonus: bonus }
}
}
impl Worker for Manager {
fn get_name(&self) -> &str {
self.employee.get_name()
}
fn pay(&self) -> i32 {
self.employee.pay() + self.bonus
}
}
Using the types:
let ana = Employee::new("Ana", 3000);
let bob = Manager::new("Bob", 4000, 1000);
println!("{}", ana.describe());
println!("{}", bob.describe());
println!("{}", bob.employee.describe());
let staff: Vec<Box<dyn Worker>> = vec![
Box::new(Employee::new("Finn", 2800)),
Box::new(Manager::new("Hana", 6000, 500)),
];
for w in &staff {
println!("{}", w.describe());
}
It prints:
Ana earns 3000
Bob earns 5000
Bob earns 4000
Finn earns 2800
Hana earns 6500
The third line is the inner employee asked on its own: it knows only the salary, 4000. The manager adds the bonus on top. The last two lines show why a trait is useful: a vector of Box<dyn Worker> mixes employees and managers, and each element runs its own pay.
Rust details worth knowing:
- Default methods. A trait function with a body is a default. Types that implement the trait get it unchanged, but any type may write its own version instead.
- Borrowed returns.
get_namereturns&str, a borrow tied to&self. Returning&self.nameworks because a&Stringconverts to a&strwhere one is expected. The caller never gets a copy of the name. self.pay()insideManager::paywould loop. It calls the very same function again. The compiler warns "function cannot return without recursing". The forwarding call goes through the field:self.employee.pay().- Struct fields are private to the module. The types reach the data through their methods.
Complexity
Each call does a fixed amount of work, so describe, pay and get_name take constant time apart from building the output string, which is proportional to the length of the name. Each value stores its name, a salary, and for a manager a bonus.
Edge cases and common mistakes
- A bonus of 0. A manager with a salary of 5000 and a bonus of 0 earns exactly 5000, like an employee. The line is
Cleo earns 5000. - Reading the salary directly. Writing
self.employee.salary + self.bonuscompiles in the same module but bypassesEmployee::pay. If the way employees are paid changed, managers would silently keep the old rule. - Writing
describetwice. Copying it into bothimplblocks works but defeats the trait default. A change in the sentence would then need two edits. - Forgetting to forward
get_name. A manager has nonamefield of its own, so it must ask its employee. - Semicolon after the last expression.
format!(...);returns()instead of theString, and the compiler reports a type mismatch.
Testing
Employee::new("Ana", 3000)describes itself asAna earns 3000.Manager::new("Bob", 4000, 1000)givesBob earns 5000.Manager::new("Cleo", 5000, 0)givesCleo earns 5000.Manager::new("Hana", 6000, 500)givesHana earns 6500.
