Start learning free

LESSON 566 OF 574 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in Swift: An Abstract Parent Class

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.

Swift has no abstract classes. As in the abstract-classes lesson, Payment's own fee() calls fatalError("must override"), and each subclass (: Payment) overrides it. The subclasses inherit init, amount and total. 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 and every method header: declare the property 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 7
    Ctrl + Enter to check

    SWIFT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in Swift, explained

    Problem overview

    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, and only the fee depends on the kind:

    • Card: amount * 2 / 100.
    • PayPal: amount * 3 / 100 + 30.
    • Cash: 0.

    A card payment of 1999 cents has a fee of 39 (the division drops the fraction) and a total of 2038.

    Approach

    The shared part goes into the parent Payment: it stores the amount and writes total() once, as amount + fee(). The fee is the part that differs, so Payment declares fee() but can't give it a value. The subclasses override it. When total() calls fee(), Swift runs the version of the object's own class, a card's fee for a card. Each call takes O(1) time.

    Swift has no abstract classes. The usual stand-in is a fee() in the parent whose body is fatalError("must override"), which stops the program if it is ever reached. A subclass that overrides fee() never reaches it.

    Step by step

    1. In Payment, declare var amount: Int.
    2. In init, store the parameter in self.amount.
    3. In fee(), call fatalError("must override"). It never returns, so the method needs no return.
    4. In total(), return amount + fee().
    5. Declare Card, PayPal and Cash with : Payment. They inherit init, amount and total() and override only fee().
    6. Write each formula. Multiply first and divide second.

    The solution in Swift

    class Payment {
        var amount: Int
    
        init(_ amount: Int) {
            self.amount = amount
        }
    
        func fee() -> Int {
            fatalError("must override")
        }
    
        func total() -> Int {
            return amount + fee()
        }
    }
    
    class Card: Payment {
        override func fee() -> Int {
            return amount * 2 / 100
        }
    }
    
    class PayPal: Payment {
        override func fee() -> Int {
            return amount * 3 / 100 + 30
        }
    }
    
    class Cash: Payment {
        override func fee() -> Int {
            return 0
        }
    }
    

    The totals and the sum of the fees for the lesson's three examples:

    func run(_ ps: [Payment]) {
      print(ps.map { String($0.total()) }.joined(separator: " "))
      print(ps.reduce(0) { $0 + $1.fee() })
    }
    run([Card(1999), PayPal(500), Cash(700)])
    run([Card(49), PayPal(33), Card(50)])
    run([Card(0), PayPal(0), Cash(0)])
    
    2038 545 700
    84
    49 63 51
    31
    0 30 0
    30
    

    The array passed to run has type [Payment], so the calls go through the parent type, and each payment still uses its own fee.

    Swift details worth knowing

    • Integer division drops the fraction. Amounts are Int, so 49 * 2 / 100 is 0 and 50 * 2 / 100 is 1.
    • The order of the operations matters. Card(150).fee() is 3, but dividing first gives 150 / 100 * 2, which is 2.
    • The subclasses write no init. They add no stored properties, so they inherit the parent's init(_:), and Card(1999) works.

    Edge cases and common mistakes

    • Amount 0. The card and cash fees are 0, but PayPal still adds its 30 cents.
    • Forgetting Cash's override. Its fee would then run Payment's version. A class that adds no fee(), such as class Check: Payment {}, crashes on Check(10).total() with Fatal error: must override.
    • Writing the total in each subclass. It works, but the point is to write it once in the parent.

    Testing

    These amounts are worth trying in a file of your own. On the solution above, Card(0), PayPal(0) and Cash(0) have totals 0, 30 and 0. Card(1999), PayPal(500) and Cash(700) have totals 2038, 545 and 700, with fees adding up to 84. A card with 49 cents pays no fee at all.

    Tracing the rounding example

    The second example is Card(49), PayPal(33) and Card(50). The card fee of 49 is 49 * 2 / 100, which is 98 / 100: the whole part is 0, so the total stays 49. PayPal's 33 * 3 / 100 is 99 / 100, also 0, and then 30 is added: the fee is 30 and the total 63. The second card's 50 * 2 / 100 is exactly 1, so its total is 51. The totals line reads 49 63 51 and the fees add up to 0 + 30 + 1 = 31.

    The fee is rounded down at every payment before the fees are added, not once at the end. That matches the run above, which sums the individual fees.

    What happens inside one call

    Take Card(1999) held in a variable of type Payment. A call to total() finds the version in Payment, because Card doesn't override it. That version evaluates amount + fee(), and the call to fee() goes to Card's override. Here is the amount, the fee and the total of each payment in the first example:

    let list: [Payment] = [Card(1999), PayPal(500), Cash(700)]
    for p in list {{ print(p.amount, p.fee(), p.total()) }}
    
    1999 39 2038
    500 45 545
    700 0 700
    

    The total is always the amount plus the fee. Only the middle number comes from different code for each class.

    Why a stand-in for abstract

    In languages that have abstract classes, the compiler refuses to create a Payment that has no fee. Swift has no such rule for classes, so the check happens at run time: a plain Payment(100).total() would stop with the same Fatal error: must override message. The lesson's tests only create the three concrete kinds, so the message never appears. A protocol with a fee() requirement is the other Swift way to say "every payment has a fee", but it can't also hold the stored amount and the shared total(), which is why a class is used here.

    Key takeaways

    • Put the shared rule in the parent and the changing step in an overridden method.
    • In Swift, an abstract method is a method that calls fatalError.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free