Start learning free

LESSON 565 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in Kotlin: Abstract Classes

Practice O(1) Read the full explanation

A shop takes payments by card, by PayPal and in cash. Every payment has an amount, in cents, and a total: 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: it holds the shared part, says that every payment has a fee, and leaves the fee itself to each kind of payment.

  • The abstract class Payment is built from an amount, in cents, and keeps it. Its method fee() is abstract: every payment has a fee, but Payment can't say what it is. Its method total() is concrete: it returns the amount plus fee().
  • Card, PayPal and Cash inherit from Payment, keep its total() as it is, and each writes its own fee():
  • Card pays 2%: its fee is amount × 2 / 100.
  • PayPal pays 3% plus 30 cents: its fee is amount × 3 / 100 + 30.
  • Cash costs nothing: its fee is 0.

In Kotlin, Payment is an abstract class that keeps amount as a property (val amount in its constructor), and fee is declared abstract fun fee(): Int, with no body. Each subclass hands its amount to Payment in its header (: Payment(amount)) and overrides fee. Dividing two Ints drops the fraction.

Every division rounds down: a card payment of 1999 cents has a fee of 39, not 39.98.

The starter code already has every class header, constructors included, and every method header: declare the abstract method and write the bodies. For each test, the harness creates the payments in the order given and prints two lines: each payment's total, total(), separated by spaces, and then the sum of all the fees. It calls them through the abstract type Payment, never through Card, PayPal or Cash.

 

Example 1:

Input:
    Card(1999)
    PayPal(500)
    Cash(700)
Output:
    2038 545 700
    84
Explanation: The card's fee is 1999 × 2 / 100 = 39 (39.98 rounded down), so its total is 2038. PayPal's fee is 500 × 3 / 100 + 30 = 45, total 545. Cash has no fee: total 700. The fees add up to 39 + 45 + 0 = 84.

Example 2:

Input:
    Card(49)
    PayPal(33)
    Card(50)
Output:
    49 63 51
    31
Explanation: 49 × 2 / 100 is 0.98, rounded down to 0. PayPal's 33 × 3 / 100 is 0.99, rounded down to 0, plus 30. The second card's 50 × 2 / 100 is exactly 1. The fees are 0 + 30 + 1 = 31.

Example 3:

Input:
    Card(0)
    PayPal(0)
    Cash(0)
Output:
    0 30 0
    30
Explanation: Amounts can be 0. PayPal still adds its 30 cents.

 

Constraints:

  • Every test has between 1 and 100 payments.
  • Every amount is an integer from 0 to 106 cents.

Note:

Every fee and every total takes O(1) time.

STEP 1 OF 5
    Ctrl + Enter to check

    KOTLIN · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in Kotlin, explained

    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, written amount * 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() is abstract. Every payment has a fee, but a plain Payment cannot know it, so it only declares the method and each subclass must supply it.
    • total() is a normal method. It returns amount + fee(), and it is written once. When it calls fee(), 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

    1. In Payment, declare abstract fun fee(): Int. It has a return type and no body.
    2. Write total() to return amount + fee(). Unlike fee(), it needs no open, because the children keep it as it is.
    3. In Card, override fee() to return amount * 2 / 100. Multiply first and divide last: Int division drops the fraction at the end, as the task wants.
    4. In PayPal, return amount * 3 / 100 + 30. The 30 cents come on top of the percentage, so even an amount of 0 pays 30.
    5. 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 open on Payment or on fee(). The concrete total() is final, so no child can change the shared rule.
    • In class Card(amount: Int) the parameter has no val. It is only passed to Payment(amount), and inside the body amount means the property inherited from Payment.
    • Integer division of Int values truncates, so 1999 * 2 / 100 is 39.

    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. Writing amount / 100 * 2 drops 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() in Cash. The compiler reports that the class is not abstract and does not implement the abstract member fee().
    • 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 be open instead 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.

    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free