Problem overview
A shop takes payments by card, by PayPal and in cash. Every payment has an amount in cents and a total, which is the amount plus a processing 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.
All divisions are on ints, so they round down. A card payment of 1999 cents has a fee of 39, not 39.98, and its total is 2038. A PayPal payment of 500 cents has a fee of 500 * 3 / 100 + 30 = 45, so its total is 545. Cash of 700 stays 700. The three fees add up to 84.
Approach: share the total, leave the fee open
The rule for the total, amount plus fee, is the same everywhere, so it should be written once. The fee is different in every kind of payment, so the parent class can't write it. An abstract class handles exactly this: it holds the shared part, here the amount and total(), and declares that every payment has a fee() without saying what it is.
C++ has no abstract keyword. Instead, a pure virtual method makes a class abstract:
virtual int fee() = 0;
The = 0 takes the place of a body. A class with at least one pure virtual method can't be created directly, and each child must override the method before it can be created.
Because fee is virtual, the call inside total() runs the fee of the object's real class. A Card object's total() uses the card fee, with no extra code in Card.
Without an abstract class you would have two poor options. You could copy total() into every kind of payment, and a later change to the rule would have to be made three times. Or you could give Payment a made-up default fee, and a new kind of payment that forgets to write its own would quietly get the wrong one. With a pure virtual method, the compiler refuses to create a payment that has no fee.
Step by step
- In
Payment, declare the fieldint amount;underpublic:. It lives in the parent once, and all three children inherit it. - In the constructor, store the parameter with
this->amount = amount;. An abstract class can have a constructor: it runs when a child is created. - Declare
feeasvirtual int fee() = 0;. - In
total, returnamount + fee(). - In
Card, returnamount * 2 / 100. - In
PayPal, returnamount * 3 / 100 + 30. - In
Cash, return 0. It still has to writefee, because the parent left it abstract.
The starter code already gives each child its constructor, Card(int amount) : Payment(amount) {}. After the colon, Payment(amount) passes the amount to the parent's constructor, which stores it.
The solution in C++
class Payment {
public:
int amount;
Payment(int amount) {
this->amount = amount;
}
virtual int fee() = 0;
int total() {
return amount + fee();
}
virtual ~Payment() = default;
};
class Card : public Payment {
public:
Card(int amount) : Payment(amount) {}
int fee() override {
return amount * 2 / 100;
}
};
class PayPal : public Payment {
public:
PayPal(int amount) : Payment(amount) {}
int fee() override {
return amount * 3 / 100 + 30;
}
};
class Cash : public Payment {
public:
Cash(int amount) : Payment(amount) {}
int fee() override {
return 0;
}
};
A run
Creating the payments of the first example through Payment* pointers and printing every total(), then the sum of the fee() values, gives:
2038 545 700
84
For Card(49), PayPal(33) and Card(50) the output is:
49 63 51
31
49 * 2 / 100 is 0.98, which rounds down to 0. The PayPal fee is 0 + 30. The second card is exactly 1. And for amounts of 0, Card(0), PayPal(0) and Cash(0) give 0 30 0 and a fee sum of 30: PayPal still charges its 30 cents.
Details that matter
- Multiply first, then divide.
amount * 2 / 100keeps the precision for as long as possible. For an amount of 150, it gives 3. Dividing first,amount / 100 * 2, gives 2. - No overflow here. The largest amount is 1,000,000, so
amount * 3is 3,000,000, far below the largestint. total()isn't virtual and doesn't need to be: no child changes it. The children keep it as it is.- The virtual destructor
virtual ~Payment() = default;letsdeleteon aPayment*free the real object correctly.
Complexity
Each fee and each total is a few arithmetic operations, so each call takes O(1) time and no extra space.
Edge cases and common mistakes
- Creating a plain
Payment.Payment p(5);doesn't compile. The compiler says it cannot declare the variable to be of abstract typePayment, because the virtual functionfee()is pure within it. - Leaving
feeout of a child. IfCashhas nofee, thenCashis still abstract, andCash c(5);fails with the same kind of error: it cannot declare the variable to be of abstract typeCash. Writereturn 0;for it. - Forgetting PayPal's 30 cents when the amount is 0. The percentage is 0, but the 30 cents stay.
- Dividing before multiplying, as shown above, rounds too early.
- Writing a second
total()in each child. The point of the parent is that the total is written once.
Testing
The three payments of each list are in order, with their totals and then the sum of the fees:
| Payments | Totals | Fees |
|---|---|---|
| Card(1999), PayPal(500), Cash(700) | 2038 545 700 | 84 |
| Card(49), PayPal(33), Card(50) | 49 63 51 | 31 |
| Card(0), PayPal(0), Cash(0) | 0 30 0 | 30 |
