Start learning free

LESSON 565 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Payment Fees in Go: Interface Plus Embedded Base

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.

Go has no classes, no inheritance and no abstract classes. As in the abstract-classes lesson, an interface plays the abstract class, and the shared part lives next to it:

  • The interface Payment lists what every payment must have: getAmount() int and fee() int.
  • The struct BasePayment keeps the amount, and its method getAmount() returns it.
  • The function total(p Payment) int returns p.getAmount() + p.fee(): written once, it works for any payment. Go interfaces have no method bodies, so the shared method becomes a function.
  • Card, PayPal and Cash each embed BasePayment, as Puppy embeds Dog in the inheritance lesson, so they get amount and getAmount() for free. NewCard(amount), NewPayPal(amount) and NewCash(amount) build them, 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.

The methods take the struct as a receiver named this, as in the course's other lessons, and 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 type, the constructor functions and every method and function header: declare the interface's methods and the structs' contents, 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(p), separated by spaces, and then the sum of all the fees. It calls them through the interface Payment, never through Card, PayPal or Cash.

 

Example 1:

Input:
    NewCard(1999)
    NewPayPal(500)
    NewCash(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:
    NewCard(49)
    NewPayPal(33)
    NewCard(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:
    NewCard(0)
    NewPayPal(0)
    NewCash(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 14
    Ctrl + Enter to check

    GO · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Payment Fees in Go, 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:

    • Card: 2% of the amount, amount * 2 / 100.
    • PayPal: 3% plus 30 cents, amount * 3 / 100 + 30.
    • Cash: no fee.

    Every division rounds down. A card payment of 1999 cents has a fee of 39 (the exact 39.98 is cut), so its total is 2038. PayPal on 500 costs 45 in fees, total 545, and cash on 700 stays 700. The fees add up to 84.

    Approach

    Other languages solve "shared part plus a part each kind fills in" with an abstract class. Go has no classes, no inheritance and no abstract classes, so it splits the job in three:

    • An interface, Payment, says what every payment must have: getAmount() and fee().
    • A small struct, BasePayment, holds the shared data (the amount) and the method that returns it.
    • A plain function, total(p Payment), holds the shared rule. Interfaces have no method bodies, so a rule that is written once becomes a function that accepts any Payment.

    Each kind of payment embeds BasePayment, which gives it the amount field and getAmount() for free, and adds its own fee(). With both methods, it is a Payment, with no implements needed.

    Step by step

    1. In the Payment interface, list getAmount() int and fee() int. Through an interface, code can only call methods, so even the amount needs one.
    2. Give BasePayment one field, amount, and a getAmount method that returns it.
    3. Write total as p.getAmount() + p.fee(). The call to fee() runs the fee of whatever kind of payment p holds.
    4. In Card, write BasePayment alone on a line to embed it. NewCard builds a Card whose BasePayment is itself built from the amount: an embedded field is named after its type, so the outer braces set BasePayment and the inner ones set its amount.
    5. Write Card's fee: this.amount * 2 / 100. The name this.amount reaches into the embedded struct directly.
    6. Repeat the embedding and the constructor for PayPal, with the fee this.amount * 3 / 100 + 30.
    7. Repeat them for Cash, whose fee returns 0. Cash still needs the method: without it, a Cash would not be a Payment.

    The solution in Go

    type Payment interface {
        getAmount() int
        fee() int
    }
    
    type BasePayment struct {
        amount int
    }
    
    func (this BasePayment) getAmount() int {
        return this.amount
    }
    
    func total(p Payment) int {
        return p.getAmount() + p.fee()
    }
    
    type Card struct {
        BasePayment
    }
    
    func NewCard(amount int) Card {
        return Card{BasePayment: BasePayment{amount: amount}}
    }
    
    func (this Card) fee() int {
        return this.amount * 2 / 100
    }
    
    type PayPal struct {
        BasePayment
    }
    
    func NewPayPal(amount int) PayPal {
        return PayPal{BasePayment: BasePayment{amount: amount}}
    }
    
    func (this PayPal) fee() int {
        return this.amount * 3 / 100 + 30
    }
    
    type Cash struct {
        BasePayment
    }
    
    func NewCash(amount int) Cash {
        return Cash{BasePayment: BasePayment{amount: amount}}
    }
    
    func (this Cash) fee() int {
        return 0
    }

    Things worth knowing:

    • getAmount is defined once, on BasePayment, and every struct that embeds it has it. A Card is a Payment because it has getAmount from the base and its own fee.
    • fee on BasePayment does not exist, so a bare BasePayment is not a Payment. That plays the part of an abstract method that every subclass has to write.
    • The constructors return the concrete type (Card), and callers can store it in a Payment.

    Using the payments

    payments := []Payment{NewCard(1999), NewPayPal(500), NewCash(700)}
    sum := 0
    for _, p := range payments {
        fmt.Print(total(p), " ")
        sum += p.fee()
    }
    fmt.Println()
    fmt.Println(sum)
    2038 545 700 
    84
    

    The loop never asks which kind of payment it holds. total(p) and p.fee() pick the right fee on their own.

    Edge cases and common mistakes

    • Rounding down. A card payment of 49 cents has a fee of 0 (the exact 0.98 is cut), and PayPal on 33 has 0.99 cut to 0 plus the 30, so for NewCard(49), NewPayPal(33) and NewCard(50) the totals are 49, 63 and 51, and the fees add up to 31.
    • Amount 0. NewCard(0) and NewCash(0) total 0, but NewPayPal(0) totals 30, because the 30 cents come on top of the percentage.
    • Multiply first, then divide. Integer division drops the fraction at once. 150 / 100 * 2 gives 2, while 150 * 2 / 100 gives 3, so write the amount times the percentage first.
    • Overflow. The largest amount is 1,000,000 cents. Times 3 that is 3,000,000, far below what a 64-bit int holds, so no overflow here.
    • A missing fee. A struct without fee cannot be passed to total: the compiler says it does not implement Payment.

    This design keeps two things apart. What payments share (the amount and the total rule) is written once. What differs (the fee) is written by each kind. Adding a bank transfer later means one new struct with an embedded BasePayment, a constructor and a fee, and nothing else changes.

    Complexity

    Each fee and each total takes constant time, O(1): a few arithmetic operations and two method calls.

    Testing

    Run the three examples. The first gives totals 2038, 545 and 700, with fees adding to 84. The second gives 49, 63 and 51, with fees adding to 31. The third, with all amounts 0, gives 0, 30 and 0, and a fee sum of 30. A card at the maximum amount, 1,000,000 cents, has a fee of 20,000, and PayPal at the same amount has 30,030.

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