Start learning free

LESSON 565 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in Scala: 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 Scala, Payment is an abstract class that keeps amount as a property (val amount in its constructor). A method declared without = and a body, def fee(): Int, is abstract. Each subclass hands its amount to Payment in its header (extends 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

    SCALA · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in Scala, 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.

    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 is amount * 2 / 100.
    • PayPal: the fee is amount * 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

    1. In Payment, declare def fee(): Int with no body. The constructor already keeps amount as a property, because of val amount.
    2. Write total() so that it returns amount + fee().
    3. In Card, override fee() to return amount * 2 / 100. Each subclass passes its amount up with extends Payment(amount).
    4. In PayPal, return amount * 3 / 100 + 30. The 30 cents come on top even when the amount is 0.
    5. In Cash, return 0. It must still write fee().

    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 to Payment, which stores it. Inside Card, amount then refers to that value.
    • Multiply first, then divide. amount * 2 / 100 is evaluated left to right. For 150 it gives 3, while amount / 100 * 2 gives 2, because the division rounds too early.
    • Size. Amounts reach 1,000,000 cents, so amount * 3 stays far below the limit of an Int, 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 totals 49 63 51 and fees adding up to 31. 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 totals 0 30 0 and a fee sum of 30.
    • Card(1000000) and PayPal(1000000) give totals 1020000 and 1030030.

    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 extends Payment without defining it doesn't compile: class Cash needs to be abstract, since def fee(): Int in class Payment is not defined.
    • Creating a Payment directly. 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, in Payment.
    Keep going with Scala500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free