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: 2% of the amount, writtenamount * 2 / 100;PayPal: 3% plus 30 cents,amount * 3 / 100 + 30;Cash: no fee, 0.
The shared rule goes in one place and the differing rule is left to each kind. That is what an abstract class is for.
For a card payment of 1999 cents, the fee is 39 (the exact 39.98 is rounded down), so the total is 2038. For PayPal with 500 cents the fee is 45 and the total 545. For cash with 700 cents the total is 700, and the three fees add up to 84.
Approach
An abstract class in Kotlin can hold working code and also declare methods that have no body. Here Payment does both:
fee()isabstract. Every payment has a fee, but a plainPaymentcannot know it, so it only declares the method and each subclass must supply it.total()is a normal method. It returnsamount + fee(), and it is written once. When it callsfee(), Kotlin runs the version of the object's own class: a card's fee for a card, a PayPal fee for a PayPal payment.
Because the class is abstract, nobody can create a plain Payment. Only Card, PayPal and Cash can be created, and each hands its amount up to the parent in its header: class Card(amount: Int) : Payment(amount). The parent keeps it as val amount, which the children read as amount.
Compare this with an ordinary open parent such as a standard shipping class. There, the parent can be created and its methods have working defaults. Here a default fee would make no sense, since there is no such thing as a payment of no particular kind, so the class is abstract and forces each kind to answer the question itself. If you add a fourth kind later, such as a bank transfer, the compiler reminds you to write its fee, and total() keeps working without changes.
Step by step
- In
Payment, declareabstract fun fee(): Int. It has a return type and no body. - Write
total()to returnamount + fee(). Unlikefee(), it needs noopen, because the children keep it as it is. - In
Card, overridefee()to returnamount * 2 / 100. Multiply first and divide last:Intdivision drops the fraction at the end, as the task wants. - In
PayPal, returnamount * 3 / 100 + 30. The 30 cents come on top of the percentage, so even an amount of 0 pays 30. - In
Cash, return 0. The method must still be written, because a subclass that leaves out an abstract method cannot be created.
The solution in Kotlin
abstract class Payment(val amount: Int) {
abstract fun fee(): Int
fun total(): Int {
return amount + fee()
}
}
class Card(amount: Int) : Payment(amount) {
override fun fee(): Int {
return amount * 2 / 100
}
}
class PayPal(amount: Int) : Payment(amount) {
override fun fee(): Int {
return amount * 3 / 100 + 30
}
}
class Cash(amount: Int) : Payment(amount) {
override fun fee(): Int {
return 0
}
}
Kotlin details:
- An abstract class and its abstract members are open to inheritance automatically, so there is no
openonPaymentor onfee(). The concretetotal()is final, so no child can change the shared rule. - In
class Card(amount: Int)the parameter has noval. It is only passed toPayment(amount), and inside the bodyamountmeans the property inherited fromPayment. - Integer division of
Intvalues truncates, so1999 * 2 / 100is39.
A run
Put the payments in a list of the abstract type and ask each one for its total:
val ps: List<Payment> = listOf(Card(1999), PayPal(500), Cash(700))
println(ps.joinToString(" ") { it.total().toString() })
println(ps.sumOf { it.fee() })
2038 545 700
84
Small amounts show the rounding. For Card(49), PayPal(33) and Card(50) the totals are 49 63 51 and the fees add up to 31: the card fee for 49 is 0 (the exact 0.98 rounds down), PayPal adds only its 30, and the 50-cent card pays exactly 1. With Card(0), PayPal(0) and Cash(0) the totals are 0 30 0 and the fees add up to 30.
Edge cases and common mistakes
- Dividing before multiplying. For
Card(150)the correct fee is 3. Writingamount / 100 * 2drops the fraction too early and gives 2. - Forgetting PayPal's 30 on a zero amount. The fixed 30 is added whatever the amount.
- Leaving out
fee()inCash. The compiler reports that the class is not abstract and does not implement the abstract memberfee(). - Creating a plain
Payment.Payment(5)is rejected with "cannot create an instance of an abstract class". - Writing a body for
fee()in the parent. An abstract method has none; if you want a default, the method would beopeninstead and the point of requiring each kind to choose its own is lost.
Complexity
Every fee and every total takes a few arithmetic operations, so each call is O(1). The amounts are at most 1,000,000, so the products stay far below the largest Int.
