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.
That is what an abstract class is for. Payment holds the shared part: it keeps the amount and writes total() once. It says that every payment has a fee(), but it can't say what the fee is, so fee() is left abstract.
Card: the fee isamount * 2 / 100.PayPal: the fee isamount * 3 / 100 + 30.Cash: the fee is 0.
Divisions on Int values drop the fraction. A card payment of 1999 cents has a fee of 39, not 39.98, so its total is 2038.
Approach
An abstract class is a class that can't be created on its own. It may mix finished methods with abstract ones. In Scala, a method declared without = and without a body, such as def fee(): Int, is abstract. Every concrete subclass must define it, and the compiler checks that.
The finished method total() calls fee(), and the call runs the version belonging to the object's own class. A card's total uses the card's fee, and a cash payment's total uses 0.
Step by step
- In
Payment, declaredef fee(): Intwith no body. The constructor already keepsamountas a property, because ofval amount. - Write
total()so that it returnsamount + fee(). - In
Card, overridefee()to returnamount * 2 / 100. Each subclass passes its amount up withextends Payment(amount). - In
PayPal, returnamount * 3 / 100 + 30. The 30 cents come on top even when the amount is 0. - In
Cash, return 0. It must still writefee().
The solution in Scala
abstract class Payment(val amount: Int) {
def fee(): Int
def total(): Int = {
return amount + fee()
}
}
class Card(amount: Int) extends Payment(amount) {
override def fee(): Int = {
return amount * 2 / 100
}
}
class PayPal(amount: Int) extends Payment(amount) {
override def fee(): Int = {
return amount * 3 / 100 + 30
}
}
class Cash(amount: Int) extends Payment(amount) {
override def fee(): Int = {
return 0
}
}
Details worth knowing:
- Passing the amount up.
class Card(amount: Int) extends Payment(amount)hands the constructor argument toPayment, which stores it. InsideCard,amountthen refers to that value. - Multiply first, then divide.
amount * 2 / 100is evaluated left to right. For 150 it gives 3, whileamount / 100 * 2gives 2, because the division rounds too early. - Size. Amounts reach 1,000,000 cents, so
amount * 3stays far below the limit of anInt, about 2.1 billion. - Time. Each fee and each total takes constant time.
Trying it
Hold the payments in a list of the abstract type and call the same methods on all of them:
val payments: List[Payment] = List(new Card(1999), new PayPal(500), new Cash(700))
println(payments.map(_.total()).mkString(" "))
println(payments.map(_.fee()).sum)
This prints:
2038 545 700
84
The card's fee is 39, PayPal's is 45 and cash has none. Other inputs:
Card(49),PayPal(33),Card(50)give totals49 63 51and fees adding up to31. The card's 49 * 2 / 100 rounds down to 0, and PayPal's 33 * 3 / 100 rounds down to 0 before its 30 is added.Card(0),PayPal(0),Cash(0)give totals0 30 0and a fee sum of30.Card(1000000)andPayPal(1000000)give totals1020000and1030030.
Why the total is written only once
Imagine writing total() separately in Card, PayPal and Cash. Three copies of amount + fee would have to stay in step, and a fourth kind of payment would need a fourth copy. With the abstract class, the rule that a total is the amount plus the fee exists in one place. The subclasses supply only what really differs.
The compiler helps too. Because fee() is abstract, nobody can forget it: a subclass without a fee does not compile, so every payment that can exist has one.
Following one call
Take new PayPal(500).total(). The method total is found in Payment, and it needs fee(). The object is a PayPal, so Scala runs PayPal's version: 500 * 3 is 1500, divided by 100 is 15, plus 30 gives 45. The total is 500 + 45 = 545.
Edge cases
Amounts can be 0. Then a card and a cash payment have a total of 0, but PayPal still adds its 30 cents, so its total is 30. Small amounts show the rounding down: a card payment of 49 cents has a fee of 0, because 98 / 100 is 0 with whole numbers. Amounts go up to 1,000,000 cents, and the largest product, 1,000,000 * 3, is 3,000,000, far below the limit of an Int.
Note also that the lesson's tests only use the abstract type Payment. They create payments and call total() and fee(), never anything specific to a card or to PayPal.
Common mistakes
- Leaving
fee()out of a subclass. A class that extendsPaymentwithout defining it doesn't compile:class Cash needs to be abstract, since def fee(): Int in class Payment is not defined. - Creating a
Paymentdirectly.new Payment(5)is rejected:Payment is abstract; it cannot be instantiated. - Dividing before multiplying. It loses the fraction too early, as in the example with 150 above.
- Forgetting the 30 cents for small amounts. PayPal adds them even for an amount of 0.
- Rewriting
total()in each subclass. The point of the design is to write it once, inPayment.
