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, so the shared part goes in a parent class, Payment, and each kind writes only its fee.
Payment(amount)keeps the amount. Itsfee()is abstract: every payment has a fee, butPaymentcannot say what it is. Itstotal()is concrete and returns the amount plusfee().Card: fee is amount * 2 / 100, rounded down.PayPal: fee is amount * 3 / 100, rounded down, plus 30.Cash: fee is 0.
A card payment of 1999 cents has a fee of 39 (39.98 rounded down), so its total is 2038. A PayPal payment of 500 has a fee of 15 + 30 = 45 and a total of 545. A cash payment of 700 has a total of 700.
Approach: one total, three fees
Writing total() in each of the three classes would repeat the same line three times. Instead, total() lives once in Payment and calls this.fee(). When it runs for a card payment, this is a Card object, so this.fee() runs the card's fee. The parent method calls a method that only the subclasses define. That is the point of an abstract class.
JavaScript has no abstract keyword. The convention is that the parent's fee() throws an error, and each subclass replaces it. A subclass that forgets to write its own fee() then fails loudly instead of quietly charging nothing.
Step by step
- Store the amount.
Payment's constructor saves it inthis.amount. The subclasses have no constructors, so they inherit this one, andnew Card(1999)passes 1999 up to it. - Make
fee()abstract. It throwsnew Error("Not implemented"). - Write
total(). It returnsthis.amount + this.fee(), once for every kind of payment. - Write
Card.fee(). Multiply by 2, divide by 100, and round down withMath.floor. - Write
PayPal.fee(). Multiply by 3, divide by 100, round down, then add 30. The 30 cents are added even when the amount is 0. - Write
Cash.fee(). It returns 0. Without it, a cash payment would runPayment'sfee()and throw.
The solution in JavaScript
class Payment {
constructor(amount) {
this.amount = amount;
}
fee() {
throw new Error("Not implemented");
}
total() {
return this.amount + this.fee();
}
}
class Card extends Payment {
fee() {
return Math.floor(this.amount * 2 / 100);
}
}
class PayPal extends Payment {
fee() {
return Math.floor(this.amount * 3 / 100) + 30;
}
}
class Cash extends Payment {
fee() {
return 0;
}
}
Rounding
In JavaScript / keeps the fraction: 1999 * 2 / 100 is 39.98. Math.floor rounds it down to 39. Round once, after the multiplication and the division. Rounding in the middle changes the answer: for an amount of 150, Math.floor(150 / 100) * 2 is 2, while the correct card fee, Math.floor(150 * 2 / 100), is 3. The order amount * 2 / 100 multiplies first and divides second, so the fraction survives until the single Math.floor.
For PayPal, round the percentage first and then add 30. The 30 cents are a whole number already.
Using the classes
The three payments can sit in one array, and the same two calls work on each:
const payments = [new Card(1999), new PayPal(500), new Cash(700)];
console.log(payments.map(p => p.total()).join(" "));
console.log(payments.reduce((sum, p) => sum + p.fee(), 0));
2038 545 700
84
The fees are 39 + 45 + 0 = 84. A second set shows the rounding. new Card(49) has a fee of 0 (0.98 rounded down), new PayPal(33) has 0 + 30, and new Card(50) has exactly 1:
49 63 51
31
An amount of 0 is allowed. new Card(0), new PayPal(0) and new Cash(0) have totals 0, 30 and 0, and the fees add up to 30.
Why not repeat total() in each class
Suppose the shop later adds a small rounding rule to every total. If total() were copied into Card, PayPal and Cash, the change would have to be made three times, and one copy could easily be missed. With total() in Payment, there is one place to edit. The fee is the only part that really differs between the kinds, so it is the only part the subclasses write. A fourth kind of payment, a gift card for instance, needs just a new class with its own fee(), and it gets the constructor and total() for free.
Because every call goes through the Payment methods, the code that adds up a list of payments never needs to ask which kind each one is. It calls total() or fee(), and each object answers for itself.
Edge cases and common mistakes
- Forgetting a subclass's
fee(). A class such asclass Check extends Payment {}inherits the parent'sfee(), sonew Check(100).total()throwsNot implemented. - Calling
Paymentdirectly.new Payment(100).total()throws the same error:Paymenthas no fee of its own to add. - Leaving out
this.. Insidetotal(), writingamount + fee()refers to variables that do not exist. Methods and fields are reached throughthis. - Skipping
Math.floor. A card payment of 1999 would get a fee of 39.98 and a total of 2038.98. - Adding the 30 inside the division.
Math.floor((amount * 3 + 30) / 100)is a different calculation. The 30 cents are added after the rounding.
Key takeaways
- Put the shared rule in the parent and let it call the method the subclasses define.
- In JavaScript an abstract method is a method that throws, replaced by every subclass.
- Round once, at the end of the percentage calculation, and add whole-cent amounts afterwards.
