Problem overview
A shop takes payments by card, by PayPal and in cash. Each payment has an amount in cents and a total, which is the amount plus a fee. The total is worked out the same way for every payment. Only the fee depends on the kind of payment:
Card: amount × 2 / 100.PayPal: amount × 3 / 100 + 30.Cash: 0.
Every division drops the fraction. A card payment of 1999 cents has a fee of 39, not 39.98, so its total is 2038. A PayPal payment of 500 cents has a fee of 45 and a total of 545, and cash of 700 has a total of 700.
Approach: a trait plays the abstract class
Rust has no classes, so no abstract classes either. A trait does the same job. Payment declares two methods with no body, get_amount and fee, which every payment must write. It also has one default method, total, whose body calls those two. Every type that implements Payment gets total for free, and it runs the fee of the type it is called on.
A trait can't hold fields. That is why each struct keeps its own amount and hands it to the trait through get_amount.
Step by step
- Declare
get_amountin the trait: it takes&self, returns ani32and ends with a semicolon instead of a body. - Declare
feethe same way. The trait can't know a payment's fee, so each type has to supply it. - Write the default
total:self.get_amount() + self.fee(), without a semicolon. - Give
Cardanamountfield and anewthat builds the struct from its parameter. - Implement
get_amountby returningself.amount. - Implement
feeasself.amount * 2 / 100. - Repeat for
PayPalwithself.amount * 3 / 100 + 30. - Repeat for
Cash, whosefeeis just0. It still has to writefee, because the trait requires it.
The solution in Rust
trait Payment {
fn get_amount(&self) -> i32;
fn fee(&self) -> i32;
fn total(&self) -> i32 {
self.get_amount() + self.fee()
}
}
struct Card {
amount: i32,
}
impl Card {
fn new(amount: i32) -> Card {
Card { amount }
}
}
impl Payment for Card {
fn get_amount(&self) -> i32 {
self.amount
}
fn fee(&self) -> i32 {
self.amount * 2 / 100
}
}
struct PayPal {
amount: i32,
}
impl PayPal {
fn new(amount: i32) -> PayPal {
PayPal { amount }
}
}
impl Payment for PayPal {
fn get_amount(&self) -> i32 {
self.amount
}
fn fee(&self) -> i32 {
self.amount * 3 / 100 + 30
}
}
struct Cash {
amount: i32,
}
impl Cash {
fn new(amount: i32) -> Cash {
Cash { amount }
}
}
impl Payment for Cash {
fn get_amount(&self) -> i32 {
self.amount
}
fn fee(&self) -> i32 {
0
}
}
Used through the trait, the three kinds are handled alike. This program builds the first example's payments and prints each total, then the sum of the fees:
fn main() {
let payments: Vec<Box<dyn Payment>> = vec![Box::new(Card::new(1999)), Box::new(PayPal::new(500)), Box::new(Cash::new(700))];
let totals: Vec<String> = payments.iter().map(|p| p.total().to_string()).collect();
println!("{}", totals.join(" "));
println!("{}", payments.iter().map(|p| p.fee()).sum::<i32>());
}
2038 545 700
84
Rust details worth knowing
- Required against default. A trait method without a body is required, and a type that doesn't write it fails to compile. A method with a body is a default that types may keep or replace.
- Multiply first. Integer division drops the fraction, so the order matters. With an amount of 150,
amount * 2 / 100is 3, butamount / 100 * 2is 2, because 150 / 100 has already been cut down to 1. - Size of the numbers. The largest amount is one million (1,000,000), so
amount * 3is at most 3,000,000, far inside the range of ani32. - Struct shorthand.
Card { amount }is short forCard { amount: amount }.
The word "fee" in the lesson is a rule per kind of payment, and the lesson treats the percentage as integer arithmetic on cents, so there are no decimals to round by hand. Working in whole cents is a common way to avoid fractions in money code: every value is an integer, and the only rounding is the division, which always rounds down.
Edge cases and common mistakes
- Small amounts round down to zero. A card payment of 49 cents has a fee of 0, because 49 × 2 / 100 is 0.98 rounded down. A PayPal payment of 33 cents also has 0 from the percentage, plus the 30.
- An amount of 0. PayPal still adds its 30 cents, so
PayPal::new(0)has a total of 30. Card and Cash give 0. - Leaving out
feeforCash. The compiler rejects theimplblock, becausefeehas no default. - Overriding
totalby accident. It is shared on purpose. Keep the plus in one place and let the types supply only the parts that differ.
What the default method gives you
Any new payment type gets total without writing it. A gift card with a flat fee of 100 cents needs only the two required methods:
struct Gift {
amount: i32,
}
impl Payment for Gift {
fn get_amount(&self) -> i32 {
self.amount
}
fn fee(&self) -> i32 {
100
}
}
A Gift { amount: 2500 } reports a total() of 2600: the shared method added the amount and the gift's own fee. This is the same split an abstract class makes. The shared rule (the total is amount plus fee) lives once in the trait, and the part that varies (the fee) is left to each type.
Because the code that uses payments calls everything through the trait, it can mix the kinds freely and never needs to know which struct is behind a box. The trait also keeps the two required methods small: get_amount exists only because a trait can't read a field of the struct that implements it.
Testing
For Card::new(49), PayPal::new(33), Card::new(50) and PayPal::new(0), the totals and the sum of the fees are:
49 63 51 30
61
Every fee and every total takes constant time.
